sillyspec 3.29.2 → 3.29.3
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/analyze-transcript.cjs +38 -0
- package/package.json +1 -1
- package/src/ceremony-config.js +64 -0
- package/src/ceremony-tier.js +66 -13
- package/src/config-schema.js +16 -3
- package/src/fr-index.js +22 -0
- package/src/review-tier.js +5 -2
- package/src/run/verify-quality-scan.js +131 -3
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +1 -0
- package/src/stages/plan-postcheck.js +66 -0
- package/src/stages/plan.js +1 -0
- package/src/verify-postcheck.js +83 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 转录命令级分析 v2:response.toolCalls 提取
|
|
2
|
+
// 用法: node analyze-transcript.cjs <model-io.jsonl>
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const file = process.argv[2];
|
|
5
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
6
|
+
const calls = [];
|
|
7
|
+
const slowReqs = [];
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
let rec;
|
|
10
|
+
try { rec = JSON.parse(line); } catch { continue; }
|
|
11
|
+
const dur = rec.durationMs || 0;
|
|
12
|
+
const ts = rec.startedAt ? new Date(rec.startedAt).toTimeString().slice(0, 8) : '?';
|
|
13
|
+
if (dur > 30000) slowReqs.push({ ts, dur: Math.round(dur / 1000), n: (rec.response && rec.response.toolCalls || []).length });
|
|
14
|
+
for (const tc of (rec.response && rec.response.toolCalls) || []) {
|
|
15
|
+
const inp = tc.input || {};
|
|
16
|
+
let arg = '';
|
|
17
|
+
if (tc.name === 'Bash') arg = String(inp.command || '').replace(/\s+/g, ' ').slice(0, 130);
|
|
18
|
+
else if (tc.name === 'Read') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
|
|
19
|
+
else if (tc.name === 'Edit' || tc.name === 'Write') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
|
|
20
|
+
else if (tc.name === 'Grep') arg = String(inp.pattern || '').slice(0, 40) + ' @' + String(inp.path || '').split(/[\\/]/).slice(-1);
|
|
21
|
+
else if (tc.name === 'Agent') arg = 'dispatch: ' + String(inp.description || '').slice(0, 50);
|
|
22
|
+
else arg = JSON.stringify(inp).slice(0, 80);
|
|
23
|
+
calls.push({ ts, tool: tc.name, arg });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const byTool = {};
|
|
27
|
+
const byArg = {};
|
|
28
|
+
for (const c of calls) {
|
|
29
|
+
byTool[c.tool] = (byTool[c.tool] || 0) + 1;
|
|
30
|
+
byArg[c.tool + ' :: ' + c.arg] = (byArg[c.tool + ' :: ' + c.arg] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
console.log('请求行:', lines.length, '| 工具调用:', calls.length, '| 分布:', JSON.stringify(byTool));
|
|
33
|
+
console.log('\n== 重复 ≥2 次调用 top20 ==');
|
|
34
|
+
for (const [k, n] of Object.entries(byArg).filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20)) console.log(n + 'x |', k.slice(0, 160));
|
|
35
|
+
console.log('\n== 慢请求 >30s ==');
|
|
36
|
+
for (const s of slowReqs.sort((a, b) => b.dur - a.dur)) console.log(s.ts, '|', s.dur + 's', '| 本轮工具数:', s.n);
|
|
37
|
+
console.log('\n== 全部 Bash 命令时间线 ==');
|
|
38
|
+
for (const c of calls.filter((c) => c.tool === 'Bash')) console.log(c.ts, '|', c.arg);
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ceremony-config.js — ceremony 定价项目级配置读取器(local.yaml `ceremony:` 段 pricing 五键)
|
|
3
|
+
*
|
|
4
|
+
* 2026-09-20 ceremony 项目化定价(对话裁决:每个项目体系不一样,S0~S3 判定阈值应可配置):
|
|
5
|
+
* 引擎(ceremony-tier.js)保持纯函数零 IO——配置由本读取器读 local.yaml 注入调用方。
|
|
6
|
+
* 可配的是「起点与阈值」(default_tier / span 两阈值 / friction 起爆线 / 五级词映射部分覆写);
|
|
7
|
+
* 「只升不降」全局纪律(friction 升档、显式降档须理由)无配置出口——防项目把自己配裸奔。
|
|
8
|
+
*
|
|
9
|
+
* 语义对齐 readCeremonyLocalConfig(src/run/prompt.js,force_tier/shadow 读取器):
|
|
10
|
+
* best-effort 绝不抛、非法值 warn 后回退默认、缺段返回空对象(引擎侧 normalize 回内置值)。
|
|
11
|
+
* blast 危险面 / span 风险路径**不在本读取器**——它们是项目共享声明(_module-map.yaml 顶层
|
|
12
|
+
* blast/span_risk 段,进 git),local.yaml 是逐机配置,语义不同不混载。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import jsYaml from 'js-yaml';
|
|
17
|
+
|
|
18
|
+
import { CEREMONY_TIERS } from './ceremony-tier.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 读 local.yaml `ceremony:` 段的 pricing 五键(camelCase 归一出参,与 normalizeTierConfig 入参对齐)。
|
|
22
|
+
* @param {string} specBase - .sillyspec 目录(local.yaml 所在);空/null → 空配置
|
|
23
|
+
* @returns {{ defaultTier?: string, spanFilesThreshold?: number, spanModulesThreshold?: number, frictionThreshold?: number, riskTierMap?: object }}
|
|
24
|
+
* 仅含合法键(非法/缺失键不发——引擎侧回退内置值)
|
|
25
|
+
*/
|
|
26
|
+
export function readCeremonyPricingConfig(specBase) {
|
|
27
|
+
const out = {};
|
|
28
|
+
if (!specBase) return out;
|
|
29
|
+
let raw = null;
|
|
30
|
+
try {
|
|
31
|
+
const p = join(specBase, 'local.yaml');
|
|
32
|
+
if (!existsSync(p)) return out;
|
|
33
|
+
raw = jsYaml.load(readFileSync(p, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
return out; // 坏 yaml → 空配置(引擎回内置值),不拦定价
|
|
36
|
+
}
|
|
37
|
+
const c = raw && typeof raw === 'object' && raw.ceremony && typeof raw.ceremony === 'object'
|
|
38
|
+
? raw.ceremony
|
|
39
|
+
: null;
|
|
40
|
+
if (!c) return out;
|
|
41
|
+
|
|
42
|
+
if (CEREMONY_TIERS.includes(c.default_tier)) out.defaultTier = c.default_tier;
|
|
43
|
+
else if (c.default_tier != null) console.warn(`[sillyspec] ceremony.default_tier 非法「${c.default_tier}」已忽略(可选:${CEREMONY_TIERS.join(' | ')})`);
|
|
44
|
+
|
|
45
|
+
for (const [key, outKey] of [['span_files_threshold', 'spanFilesThreshold'], ['span_modules_threshold', 'spanModulesThreshold'], ['friction_escalation_threshold', 'frictionThreshold']]) {
|
|
46
|
+
const v = c[key];
|
|
47
|
+
if (Number.isInteger(v) && v >= 1) out[outKey] = v;
|
|
48
|
+
else if (v != null) console.warn(`[sillyspec] ceremony.${key} 非法「${v}」已忽略(≥1 整数)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (c.risk_tier_map != null) {
|
|
52
|
+
if (typeof c.risk_tier_map === 'object' && !Array.isArray(c.risk_tier_map)) {
|
|
53
|
+
const map = {};
|
|
54
|
+
for (const [level, tier] of Object.entries(c.risk_tier_map)) {
|
|
55
|
+
if (CEREMONY_TIERS.includes(tier)) map[level] = tier;
|
|
56
|
+
else console.warn(`[sillyspec] ceremony.risk_tier_map.${level} 档位非法「${tier}」已忽略(可选:${CEREMONY_TIERS.join(' | ')})`);
|
|
57
|
+
}
|
|
58
|
+
if (Object.keys(map).length > 0) out.riskTierMap = map;
|
|
59
|
+
} else {
|
|
60
|
+
console.warn('[sillyspec] ceremony.risk_tier_map 非对象已忽略');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
package/src/ceremony-tier.js
CHANGED
|
@@ -156,7 +156,12 @@ function matchModuleForFile(posix, modulesObj) {
|
|
|
156
156
|
* friction 分量=升档后的档(未起爆为 S0,不参与封顶);reasons 逐分量留痕(含阈值与
|
|
157
157
|
* 命中明细),供消费方审计打印与双跑比对
|
|
158
158
|
*/
|
|
159
|
-
export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLevel, declaredFiles, moduleIndex, spanRiskPatterns = [], frictionCounts } = {}) {
|
|
159
|
+
export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLevel, declaredFiles, moduleIndex, spanRiskPatterns = [], frictionCounts, config } = {}) {
|
|
160
|
+
// ── 项目级配置覆写(2026-09-20 ceremony 项目化定价):引擎保持纯函数零 IO,配置由调用方
|
|
161
|
+
// 读 local.yaml ceremony: 段注入(readCeremonyPricingConfig)。可配的是「起点与阈值」:
|
|
162
|
+
// 缺省档 / span 两阈值 / friction 起爆线 / 五级词映射。「只升不降」全局纪律(friction 升档、
|
|
163
|
+
// 显式降档须理由)不开放配置——防项目把自己配裸奔。非法值逐项回退默认并在 reasons 留痕。
|
|
164
|
+
const cfg = normalizeTierConfig(config)
|
|
160
165
|
const reasons = []
|
|
161
166
|
let explicitDowngradeAccepted = false
|
|
162
167
|
|
|
@@ -167,22 +172,22 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
|
|
|
167
172
|
reasons.push(`blast=${blast}(声明面直入 blastTier——resolveChangeRisk 声明危险面判级,D-008)`)
|
|
168
173
|
} else {
|
|
169
174
|
const detectedLevel = riskDetection != null && typeof riskDetection === 'object' ? riskDetection.level : undefined
|
|
170
|
-
const mappedTier =
|
|
175
|
+
const mappedTier = cfg.riskTierMap[detectedLevel]
|
|
171
176
|
if (mappedTier) {
|
|
172
177
|
blast = mappedTier
|
|
173
|
-
reasons.push(`blast=${blast}(riskDetection.level=${detectedLevel} 经
|
|
178
|
+
reasons.push(`blast=${blast}(riskDetection.level=${detectedLevel} 经 risk_tier_map 映射)`)
|
|
174
179
|
} else {
|
|
175
|
-
blast =
|
|
180
|
+
blast = cfg.defaultTier
|
|
176
181
|
const cause = detectedLevel === undefined ? 'riskDetection.level 缺失' : `riskDetection.level=${String(detectedLevel)} 未知`
|
|
177
|
-
reasons.push(`blast
|
|
182
|
+
reasons.push(`blast=${cfg.defaultTier}(保守缺省:${cause}${cfg.defaultTierSource === 'config' ? ',项目配置 default_tier' : ',brownfield 不静默降级'})`)
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
const explicit = normalizeExplicit(explicitRiskLevel)
|
|
182
187
|
if (explicit) {
|
|
183
|
-
const explicitTier =
|
|
188
|
+
const explicitTier = cfg.riskTierMap[explicit.level]
|
|
184
189
|
if (!explicitTier) {
|
|
185
|
-
reasons.push(`显式 risk_level 非法被忽略:${JSON.stringify(explicitRiskLevel)}(合法值:${Object.keys(
|
|
190
|
+
reasons.push(`显式 risk_level 非法被忽略:${JSON.stringify(explicitRiskLevel)}(合法值:${Object.keys(cfg.riskTierMap).join(' / ')})`)
|
|
186
191
|
} else if (tierRank(explicitTier) > tierRank(blast)) {
|
|
187
192
|
reasons.push(`blast 升档尊重显式声明 risk_level=${explicit.level} → ${explicitTier}(高于判级 ${blast})`)
|
|
188
193
|
blast = explicitTier
|
|
@@ -203,9 +208,9 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
|
|
|
203
208
|
.map((f) => String(f).replace(/\\/g, '/'))
|
|
204
209
|
.filter(Boolean)
|
|
205
210
|
let span = 'S0'
|
|
206
|
-
if (files.length >=
|
|
211
|
+
if (files.length >= cfg.spanFilesThreshold) {
|
|
207
212
|
span = 'S2'
|
|
208
|
-
reasons.push(`span=S2(声明文件 ${files.length} ≥ 阈值 ${
|
|
213
|
+
reasons.push(`span=S2(声明文件 ${files.length} ≥ 阈值 ${cfg.spanFilesThreshold}${cfg.spanFilesSource === 'config' ? '(项目配置)' : ''})`)
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
const modulesObj = resolveModulesObject(moduleIndex)
|
|
@@ -215,9 +220,9 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
|
|
|
215
220
|
const id = matchModuleForFile(f, modulesObj)
|
|
216
221
|
if (id != null) hitModules.add(id)
|
|
217
222
|
}
|
|
218
|
-
if (hitModules.size >=
|
|
223
|
+
if (hitModules.size >= cfg.spanModulesThreshold) {
|
|
219
224
|
span = 'S2'
|
|
220
|
-
reasons.push(`span=S2(跨模块 ${hitModules.size} ≥ 阈值 ${
|
|
225
|
+
reasons.push(`span=S2(跨模块 ${hitModules.size} ≥ 阈值 ${cfg.spanModulesThreshold}${cfg.spanModulesSource === 'config' ? '(项目配置)' : ''}:${[...hitModules].join('、')})`)
|
|
221
226
|
}
|
|
222
227
|
} // moduleIndex 缺失/空 → 跨模块检查跳过,span 其余两维照常
|
|
223
228
|
|
|
@@ -240,15 +245,63 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
|
|
|
240
245
|
// ── friction 轴:ledger 两键合计达到阈值 → 在 blast/span 结果上 +1 档封顶 S3(只升不降)──
|
|
241
246
|
const total = frictionTotal(frictionCounts)
|
|
242
247
|
let friction = 'S0'
|
|
243
|
-
if (total >=
|
|
248
|
+
if (total >= cfg.frictionThreshold) {
|
|
244
249
|
const base = maxTier(blast, span)
|
|
245
250
|
friction = CEREMONY_TIERS[Math.min(tierRank(base) + 1, CEREMONY_TIERS.length - 1)]
|
|
246
|
-
reasons.push(`friction 升档(gate_rollback+review_rejected 合计 ${total} ≥ 阈值 ${
|
|
251
|
+
reasons.push(`friction 升档(gate_rollback+review_rejected 合计 ${total} ≥ 阈值 ${cfg.frictionThreshold}${cfg.frictionSource === 'config' ? '(项目配置)' : ''},${base} → ${friction},封顶 S3)`)
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
return { tier: maxTier(maxTier(blast, span), friction), components: { blast, span, friction }, reasons, explicitDowngradeAccepted }
|
|
250
255
|
}
|
|
251
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 项目级定价配置归一(computeCeremonyTier 内部消费,纯函数):非法值逐项回退默认。
|
|
259
|
+
* 入参形态即 local.yaml `ceremony:` 段的 pricing 五键(由 readCeremonyPricingConfig 读取注入,
|
|
260
|
+
* 引擎自身不读文件):
|
|
261
|
+
* default_tier('S0'~'S3')/ span_files_threshold(≥1 整数)/ span_modules_threshold(≥1 整数)
|
|
262
|
+
* / friction_escalation_threshold(≥1 整数)/ risk_tier_map(五级词→档位映射的**部分覆写**,
|
|
263
|
+
* 与内置表浅合并——只覆写声明的词,未声明词保持内置值)。
|
|
264
|
+
* 「只升不降」纪律不在此面(friction 升档/显式降档复核为全局语义,无配置出口)。
|
|
265
|
+
*/
|
|
266
|
+
function normalizeTierConfig(config) {
|
|
267
|
+
const c = config != null && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
|
268
|
+
const out = {
|
|
269
|
+
defaultTier: 'S2',
|
|
270
|
+
defaultTierSource: 'builtin',
|
|
271
|
+
spanFilesThreshold: SPAN_FILES_THRESHOLD,
|
|
272
|
+
spanFilesSource: 'builtin',
|
|
273
|
+
spanModulesThreshold: SPAN_MODULES_THRESHOLD,
|
|
274
|
+
spanModulesSource: 'builtin',
|
|
275
|
+
frictionThreshold: FRICTION_ESCALATION_THRESHOLD,
|
|
276
|
+
frictionSource: 'builtin',
|
|
277
|
+
riskTierMap: { ...RISK_TO_TIER },
|
|
278
|
+
}
|
|
279
|
+
if (CEREMONY_TIERS.includes(c.defaultTier)) {
|
|
280
|
+
out.defaultTier = c.defaultTier
|
|
281
|
+
out.defaultTierSource = 'config'
|
|
282
|
+
}
|
|
283
|
+
if (Number.isInteger(c.spanFilesThreshold) && c.spanFilesThreshold >= 1) {
|
|
284
|
+
out.spanFilesThreshold = c.spanFilesThreshold
|
|
285
|
+
out.spanFilesSource = 'config'
|
|
286
|
+
}
|
|
287
|
+
if (Number.isInteger(c.spanModulesThreshold) && c.spanModulesThreshold >= 1) {
|
|
288
|
+
out.spanModulesThreshold = c.spanModulesThreshold
|
|
289
|
+
out.spanModulesSource = 'config'
|
|
290
|
+
}
|
|
291
|
+
if (Number.isInteger(c.frictionThreshold) && c.frictionThreshold >= 1) {
|
|
292
|
+
out.frictionThreshold = c.frictionThreshold
|
|
293
|
+
out.frictionSource = 'config'
|
|
294
|
+
}
|
|
295
|
+
if (c.riskTierMap != null && typeof c.riskTierMap === 'object' && !Array.isArray(c.riskTierMap)) {
|
|
296
|
+
for (const [level, tier] of Object.entries(c.riskTierMap)) {
|
|
297
|
+
if (Object.prototype.hasOwnProperty.call(RISK_TO_TIER, level) && CEREMONY_TIERS.includes(tier)) {
|
|
298
|
+
out.riskTierMap[level] = tier
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out
|
|
303
|
+
}
|
|
304
|
+
|
|
252
305
|
/**
|
|
253
306
|
* friction 升档(阶段门检查点用,D-008):两键合计达到阈值 → min(S3, 当前档+1);只升不降
|
|
254
307
|
* ——未达阈 / 已在顶 / 当前档非法时原样返回不发明新档。escalated=档位实际上移(S3 再超阈
|
package/src/config-schema.js
CHANGED
|
@@ -118,7 +118,7 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
118
118
|
title: '测试策略',
|
|
119
119
|
note: 'verify 阶段 CLI 对账的收窄策略(D-005@v2:skip 接线兑现 + evidence-auto 新增,full/module 语义不变)。',
|
|
120
120
|
keys: [
|
|
121
|
-
{ path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module
|
|
121
|
+
{ path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module 并注记)。缺省:配了 modules: 块 → module(v3.29.3 起缺省收窄,ql-20260920-010);未配 modules → 全量。', example: 'full' },
|
|
122
122
|
],
|
|
123
123
|
},
|
|
124
124
|
{
|
|
@@ -229,9 +229,14 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
229
229
|
{
|
|
230
230
|
id: 'ceremony',
|
|
231
231
|
title: '评审仪式档位(ceremony tier)',
|
|
232
|
-
note: '仪式档位(S0~S3)由 CLI 按 blast/span/friction 三轴风险客观定价(blast 轴输入=项目声明危险面 _module-map.yaml 顶层 blast 段,2026-09-19-ceremony-pricing-five-cuts;span 轴路径模式输入=项目声明 _module-map.yaml 顶层 span_risk 段,2026-09-19-span-risk-pattern-migration,无声明项目该维关闭不回退内置表);完成门声明追赶重定价——无摩擦迁移随声明面可升可降、有摩擦迁移地板不退,force_tier
|
|
232
|
+
note: '仪式档位(S0~S3)由 CLI 按 blast/span/friction 三轴风险客观定价(blast 轴输入=项目声明危险面 _module-map.yaml 顶层 blast 段,2026-09-19-ceremony-pricing-five-cuts;span 轴路径模式输入=项目声明 _module-map.yaml 顶层 span_risk 段,2026-09-19-span-risk-pattern-migration,无声明项目该维关闭不回退内置表);完成门声明追赶重定价——无摩擦迁移随声明面可升可降、有摩擦迁移地板不退,force_tier 仍只升不降;本段是逃生阀/影子期开关/逐机升档面 + 项目化定价阈值(2026-09-20:每个项目体系不一样,起点与阈值可配)。读键按「存在则读、不存在用缺省」容错(定价键读取走 readCeremonyPricingConfig,开关面走 readCeremonyLocalConfig)。',
|
|
233
233
|
keys: [
|
|
234
234
|
{ path: 'ceremony.blast_surfaces', type: 'json', optional: true, status: 'live', readers: ['loadBlastDeclarations (src/blast-surface.js — local 逐机升档,与 map 声明逐文件取 max)'], desc: 'blast 危险面逐机覆盖:[{ prefixes: [...], tier: S0~S3 }]——主声明在 _module-map.yaml 顶层 blast 段(进 git、可挂 evidence 位);本键只升不降(与 map 命中逐文件取 max,压低共享声明无效)、不承载 evidence(证据语义属共享 map,D-009)。非法条目静默跳过。', example: '[{ prefixes: [src/my-daemon/], tier: S3 }]' },
|
|
235
|
+
{ path: 'ceremony.default_tier', type: 'enum', values: ['S0', 'S1', 'S2', 'S3'], optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier blast 轴保守缺省——review-tier/verify-postcheck 双跑两侧同源注入)'], desc: '项目化定价·缺省档(default_tier):blast 轴无 risk 输入时的保守起点(内置 S2)。小工具仓可降 S1、强合规仓可升 S3——每个项目按自己的体系定。只改起点,不影响「只升不降」纪律。', example: 'ceremony: { default_tier: S1 }' },
|
|
236
|
+
{ path: 'ceremony.span_files_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier span 轴文件数阈值)'], desc: '项目化定价·span 文件阈值(span_files_threshold):声明文件数 ≥N → 至少 S2(内置 8)。小仓 3 个文件就算摊子大就配 3。≥1 整数。', example: 'ceremony: { span_files_threshold: 3 }' },
|
|
237
|
+
{ path: 'ceremony.span_modules_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier span 轴跨模块阈值)'], desc: '项目化定价·span 跨模块阈值(span_modules_threshold):跨模块数 ≥N → 至少 S2(内置 3)。≥1 整数。', example: 'ceremony: { span_modules_threshold: 2 }' },
|
|
238
|
+
{ path: 'ceremony.friction_escalation_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier friction 轴起爆线)'], desc: '项目化定价·friction 起爆线(friction_escalation_threshold):gate_rollback+review_rejected 合计 ≥N → 升一档封顶 S3(内置 2,两振出局)。强合规仓配 1 = 一次拦拒即升档。≥1 整数。', example: 'ceremony: { friction_escalation_threshold: 1 }' },
|
|
239
|
+
{ path: 'ceremony.risk_tier_map', type: 'json', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier 五级词映射部分覆写)'], desc: '项目化定价·五级词→档位映射覆写(部分覆写:只覆写声明的词,未声明词保持内置 doc-only→S0 / unit-sufficient→S1 / contract-required→S2 / integration-critical→S3 / deployment-critical→S3)。例:{ unit-sufficient: S2 } = 该仓单测级变更也走独立评审。非法词/档位 warn 后忽略。', example: 'ceremony: { risk_tier_map: { unit-sufficient: S2 } }' },
|
|
235
240
|
{ path: 'ceremony.force_tier', type: 'enum', values: ['S0', 'S1', 'S2', 'S3'], optional: true, status: 'live', readers: ['readCeremonyLocalConfig (src/run/prompt.js — {REVIEW_TIER} 注入档位菜单/强制轻仪审计痕)', '影子派发前置校验 (src/review-dispatch.js — task-06 影子期框架接线)'], desc: '档位逃生阀:强制仪式档 S0~S3,绕过客观定价(过渡期「就是不信这套」用)。prompt 注入面只升不降——强制档低于客观定价档时不降档(防 prompt 面与 gate 侧判定分裂);非法值 warn 后忽略。', example: 'S3' },
|
|
236
241
|
{ path: 'ceremony.shadow', type: 'boolean', optional: true, status: 'live', readers: ['readCeremonyLocalConfig (src/run/prompt.js — S0/S1 轻档注入影子期注记)', 'runReviewDispatch 影子前置校验 (src/review-dispatch.js — task-06 影子期框架接线)'], desc: '影子期开关,默认 true(on):S0/S1 轻档明面轻仪、后台静默派发重仪式只记账不阻断(影子派发框架随 task-06 落地);置 off = 轻仪转正(转正判据由 doctor 影子对照维度公示,CLI 只出判据不出手)。', example: 'true' },
|
|
237
242
|
],
|
|
@@ -446,11 +451,19 @@ docs-check:
|
|
|
446
451
|
# change-ownership:
|
|
447
452
|
# heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
|
|
448
453
|
|
|
449
|
-
# ── 评审仪式档位(ceremony tier:S0~S3 由 CLI 按 blast/span/friction
|
|
454
|
+
# ── 评审仪式档位(ceremony tier:S0~S3 由 CLI 按 blast/span/friction 三轴风险客观定价)──
|
|
450
455
|
# blast 轴输入=项目声明危险面(_module-map.yaml 顶层 blast 段);完成门声明追赶重定价(无摩擦随声明、有摩擦地板不退)。
|
|
456
|
+
# 项目化定价(每个项目体系不一样,起点与阈值可配——只升不降纪律不开放配置):
|
|
457
|
+
# default_tier 缺省档 / span_files_threshold 文件阈值(内置8) / span_modules_threshold 跨模块阈值(内置3)
|
|
458
|
+
# / friction_escalation_threshold 起爆线(内置2) / risk_tier_map 五级词映射部分覆写。
|
|
451
459
|
# force_tier 强制档位绕过客观定价(过渡逃生阀,prompt 注入面只升不降);shadow 影子期默认 on——
|
|
452
460
|
# S0/S1 轻档明面轻仪、后台静默派发重仪式对照只记账不阻断;置 off = 轻仪转正(转正判据见 doctor 影子对照)。
|
|
453
461
|
# ceremony:
|
|
462
|
+
# default_tier: S1 # 缺省档(内置 S2;小仓 S1 / 强合规 S3)
|
|
463
|
+
# span_files_threshold: 3 # 文件数阈值(内置 8)
|
|
464
|
+
# span_modules_threshold: 2 # 跨模块阈值(内置 3)
|
|
465
|
+
# friction_escalation_threshold: 1 # 起爆线(内置 2,两振出局;强合规 1)
|
|
466
|
+
# risk_tier_map: { unit-sufficient: S2 } # 五级词部分覆写(未声明词保持内置)
|
|
454
467
|
# force_tier: S3 # 强制全重仪式(逃生阀;S0 | S1 | S2 | S3)
|
|
455
468
|
# shadow: true # 影子期开关(默认 true)
|
|
456
469
|
# blast_surfaces: # blast 逐机升档(只升不降、不承载 evidence;主声明在 _module-map.yaml blast 段)
|
package/src/fr-index.js
CHANGED
|
@@ -331,6 +331,28 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '' }) {
|
|
|
331
331
|
`superseded_by:${id}`,
|
|
332
332
|
`取代链:${refId} ← ${id}(${changeName} 承接)`,
|
|
333
333
|
...(reason ? [`退役理由:${reason}`] : []));
|
|
334
|
+
// scenario-loss 检测(对标 OpenSpec 同名检查):被取代条目的场景名在新 FR 场景集
|
|
335
|
+
//(scenarios 名字数组 ∪ scenarioBodies 名)无对应 → warning 提示核对(advisory 不阻断
|
|
336
|
+
//——场景名合法漂移存在,人裁)。旧条目无场景信息(摘要「(无场景名)」/空)不比对。
|
|
337
|
+
const oldNames = new Set();
|
|
338
|
+
for (const l of target.lines) {
|
|
339
|
+
const sm = l.match(/^- 场景:(.+?)\s*—/);
|
|
340
|
+
if (sm) oldNames.add(sm[1].trim());
|
|
341
|
+
}
|
|
342
|
+
const summaryLine = target.lines.find((l) => l.startsWith('摘要:'));
|
|
343
|
+
if (summaryLine && !summaryLine.includes('(无场景名)')) {
|
|
344
|
+
for (const n of summaryLine.replace(/^摘要:\s*/, '').split(';')) {
|
|
345
|
+
const t = n.trim();
|
|
346
|
+
if (t) oldNames.add(t);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (oldNames.size > 0) {
|
|
350
|
+
const newNames = new Set([...(fr.scenarios || []), ...((fr.scenarioBodies || []).map((b) => b.name).filter(Boolean))]);
|
|
351
|
+
const dropped = [...oldNames].filter((n) => !newNames.has(n));
|
|
352
|
+
if (dropped.length > 0) {
|
|
353
|
+
warnings.push(`scenario-loss:${refId} 被承接时新 FR 未覆盖旧场景「${dropped.join('、')}」——若非故意丢弃(合并/更名/降维),请在新 FR 场景集补对应场景或在退役理由说明去向`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
334
356
|
dirtyDomains.add(st2Domain);
|
|
335
357
|
superseded.push({ from: refId, to: id, change: changeName });
|
|
336
358
|
}
|
package/src/review-tier.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { parseFileChangeList } from './change-list.js'
|
|
38
38
|
import { computeCeremonyTier, CEREMONY_TIERS } from './ceremony-tier.js'
|
|
39
|
+
import { readCeremonyPricingConfig } from './ceremony-config.js'
|
|
39
40
|
import { resolveChangeRisk, extractExplicitRiskLevel } from './change-risk-profile.js'
|
|
40
41
|
import { readFileSync, existsSync } from 'fs'
|
|
41
42
|
import { join, dirname, basename } from 'path'
|
|
@@ -158,8 +159,10 @@ export function classifyReviewTier({ planLevel, designPath, riskDetection, frict
|
|
|
158
159
|
} catch { /* 坏档当无档(实判仍兜底) */ }
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
// 委托三轴定价引擎取档(ceremony-tier.js
|
|
162
|
-
|
|
162
|
+
// 委托三轴定价引擎取档(ceremony-tier.js 单点真相源)——pricing 配置自 designPath 推导
|
|
163
|
+
// specBase 读 local.yaml ceremony: 段注入(2026-09-20 项目化定价;无 designPath → 空配置回内置值)
|
|
164
|
+
const pricingConfig = designPath ? readCeremonyPricingConfig(join(dirname(designPath), '..')) : {}
|
|
165
|
+
const priced = computeCeremonyTier({ riskDetection: blastInput, explicitRiskLevel: explicitInput, declaredFiles, spanRiskPatterns, frictionCounts, config: pricingConfig })
|
|
163
166
|
let ceremonyTier = priced.tier
|
|
164
167
|
if (tierFileTier && CEREMONY_TIERS.indexOf(tierFileTier) > CEREMONY_TIERS.indexOf(ceremonyTier)) {
|
|
165
168
|
ceremonyTier = tierFileTier
|
|
@@ -99,7 +99,7 @@ export function computeQualityScanFingerprint({ cwd, specBase }) {
|
|
|
99
99
|
* 保持 1——存量记录无 smoke 段照常读回(smokeResult 兜底 null);未配置 smoke 时段形态为
|
|
100
100
|
* configured=false(记录仍写,供 --done PASS 封顶判 not-configured)。
|
|
101
101
|
*/
|
|
102
|
-
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult }) {
|
|
102
|
+
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult, usedSnapshot = false }) {
|
|
103
103
|
const path = qualityScanRecordPath(specBase, changeName)
|
|
104
104
|
if (!path) return null
|
|
105
105
|
const fingerprint = computeQualityScanFingerprint({ cwd, specBase })
|
|
@@ -110,6 +110,10 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
|
|
|
110
110
|
source: 'cli-noai',
|
|
111
111
|
change: changeName,
|
|
112
112
|
fingerprint,
|
|
113
|
+
// 失败签名去重面(ql-20260920-010 修复一 d):additive 字段,存量记录无这两键 →
|
|
114
|
+
// shouldReuseLastFailedScan 判 no-dedup-key 保守重跑一次,之后新记录即携带
|
|
115
|
+
dedupKey: computeQualityScanDedupKey({ cwd, specBase, fingerprint }),
|
|
116
|
+
usedSnapshot: Boolean(usedSnapshot),
|
|
113
117
|
ranAt: new Date().toISOString(),
|
|
114
118
|
testResult,
|
|
115
119
|
lintResult: lintResult || null,
|
|
@@ -143,6 +147,93 @@ export function loadReusableQualityScan({ specBase, cwd, changeName }) {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 去重键(ql-20260920-010 修复一 d,对撞三轮三连假红重试 8~10 分钟×3 的根治):
|
|
152
|
+
* 指纹 + known_failures 块哈希。指纹只哈希 commands 段 + test_strategy 行——不含
|
|
153
|
+
* known_failures;而 agent 对假红的常见补救正是加豁免条目(不改代码、指纹不动)。
|
|
154
|
+
* 去重若只看指纹会把「合法补救后复跑」误判成「什么都没改」而拒绝重跑——键独立补
|
|
155
|
+
* 豁免面哈希,豁免一动即失配。git 不可用 → null(fail-closed 同指纹口径)。
|
|
156
|
+
*/
|
|
157
|
+
export function computeQualityScanDedupKey({ cwd, specBase, fingerprint = null }) {
|
|
158
|
+
const fp = fingerprint ?? computeQualityScanFingerprint({ cwd, specBase })
|
|
159
|
+
if (!fp) return null
|
|
160
|
+
let kf = 'none'
|
|
161
|
+
try {
|
|
162
|
+
const localPath = join(specBase, 'local.yaml')
|
|
163
|
+
if (existsSync(localPath)) {
|
|
164
|
+
const lines = readFileSync(localPath, 'utf8').replace(/\r\n?/g, '\n').split('\n')
|
|
165
|
+
const start = lines.findIndex((l) => /^\s*known_failures:\s*(#.*)?$/.test(l))
|
|
166
|
+
if (start !== -1) {
|
|
167
|
+
const body = []
|
|
168
|
+
for (let i = start + 1; i < lines.length && !/^\S/.test(lines[i]); i++) body.push(lines[i])
|
|
169
|
+
kf = body.join('\n')
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch { /* 读不到按无豁免面处理 */ }
|
|
173
|
+
return createHash('sha256').update(fp + '\n##known-failures##\n' + kf).digest('hex')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 读回最近一条扫描记录(不论成败)——失败去重重放用。loadReusableQualityScan 拒绝
|
|
178
|
+
* failed 记录(复用是优化不是正确性依赖),去重恰恰要消费 failed 态;schema/source/
|
|
179
|
+
* testResult 校验同 load 口径,读不回 → null。
|
|
180
|
+
*/
|
|
181
|
+
export function loadLastQualityScanRecord({ specBase, changeName }) {
|
|
182
|
+
const path = qualityScanRecordPath(specBase, changeName)
|
|
183
|
+
if (!path || !existsSync(path)) return null
|
|
184
|
+
try {
|
|
185
|
+
const rec = JSON.parse(readFileSync(path, 'utf8'))
|
|
186
|
+
if (!rec || rec.schemaVersion !== RECORD_SCHEMA_VERSION || rec.source !== 'cli-noai') return null
|
|
187
|
+
if (!rec.testResult || !rec.testResult.status) return null
|
|
188
|
+
return rec
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 失败签名去重判定(纯函数)。reuse=true 语义:代码指纹 + 豁免面 + 快照口径均未变 →
|
|
196
|
+
* 上次失败必然原样复现,重跑是纯等待浪费。四路不重跑:forceRerun 逃生阀 / 无失败记录 /
|
|
197
|
+
* 存量记录无 dedupKey(本版起才有,老记录保守重跑一次)/ 键或口径失配。
|
|
198
|
+
*/
|
|
199
|
+
export function shouldReuseLastFailedScan({ lastRecord, currentDedupKey, plannedSnapshot, forceRerun = false }) {
|
|
200
|
+
if (forceRerun) return { reuse: false, reason: 'force-rerun(逃生阀 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN)' }
|
|
201
|
+
if (!lastRecord || !lastRecord.testResult || lastRecord.testResult.status !== 'failed') {
|
|
202
|
+
return { reuse: false, reason: 'no-failed-record' }
|
|
203
|
+
}
|
|
204
|
+
if (!lastRecord.dedupKey || !currentDedupKey) return { reuse: false, reason: 'no-dedup-key(存量记录 / git 不可用)' }
|
|
205
|
+
if (lastRecord.dedupKey !== currentDedupKey) return { reuse: false, reason: 'dedup-key-mismatch(代码/配置/豁免面/HEAD 已变化)' }
|
|
206
|
+
if (Boolean(lastRecord.usedSnapshot) !== Boolean(plannedSnapshot)) return { reuse: false, reason: 'snapshot-scope-changed' }
|
|
207
|
+
return { reuse: true, reason: null }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 伪影分诊编排(ql-20260920-010 修复一 a):测试失败 → classify 分类 → 命中且本次是
|
|
212
|
+
* 快照口径 → 主树口径单点复跑对照。复跑过 = 伪影坐实:返回主树结果 + artifactTriage
|
|
213
|
+
* 证据(放行有据,disclosure 走 console + 台账,不改 passed.reason 语义);复跑仍挂 =
|
|
214
|
+
* 真失败:保留快照失败结果 + 两口径对照注记(拦截照常,归因已对照过)。主树口径失败
|
|
215
|
+
* (snap=null)只注记 advice 不复跑(本身就是对照口径)。classify/rerunInMainScope
|
|
216
|
+
* 可注入——纯函数化供测试;分诊永不单独放行(放行必经主树实测复证)。
|
|
217
|
+
*/
|
|
218
|
+
export function applyArtifactTriageRerun({ testCheck, classify, snap, rerunInMainScope }) {
|
|
219
|
+
if (!testCheck || testCheck.status !== 'failed') return testCheck
|
|
220
|
+
const triage = classify(testCheck)
|
|
221
|
+
if (!triage || !triage.matched) return testCheck
|
|
222
|
+
if (!snap) {
|
|
223
|
+
console.warn(`🔬 伪影分诊命中(${triage.kind},本次已主树口径):${triage.advice}`)
|
|
224
|
+
return { ...testCheck, reason: `${testCheck.reason || ''}[伪影分诊命中(${triage.kind}):${triage.advice}]`, artifactTriage: triage }
|
|
225
|
+
}
|
|
226
|
+
console.warn(`🔬 伪影分诊命中(${triage.kind})——快照口径失败疑似沙箱伪影,主树口径单点复跑对照…`)
|
|
227
|
+
for (const ev of triage.evidence) console.warn(` · ${ev}`)
|
|
228
|
+
const mainRetry = rerunInMainScope()
|
|
229
|
+
if (mainRetry.status === 'passed') {
|
|
230
|
+
console.warn(' 主树复跑通过——伪影坐实:按主树口径结果放行(快照失败输出与分诊证据随台账落盘)')
|
|
231
|
+
return { ...mainRetry, artifactTriage: triage }
|
|
232
|
+
}
|
|
233
|
+
console.warn(' 主树复跑仍失败——非沙箱伪影,按真失败拦截(两口径输出均见台账)')
|
|
234
|
+
return { ...testCheck, reason: `${testCheck.reason || ''}[伪影分诊:两口径(快照/主树)均失败,非沙箱伪影]`, artifactTriage: triage }
|
|
235
|
+
}
|
|
236
|
+
|
|
146
237
|
/**
|
|
147
238
|
* 并行 WIP 归因鉴别(前置二):迁自 gates.js verify 实测失败归因 inline 块,输出逐字保持。
|
|
148
239
|
* 判据①他者声明文件命中 dirty 集;判据②本变更 apply 的文件与主仓近期提交重叠且旧内容
|
|
@@ -401,7 +492,37 @@ function warnIfMainRepoDirtyForGate(cwd) {
|
|
|
401
492
|
* `run verify` 复入本步重新实测(指纹随代码修复失配,--done 不会复用旧失败结果)。
|
|
402
493
|
*/
|
|
403
494
|
export async function executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts }) {
|
|
404
|
-
const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint } = await import('../verify-postcheck.js')
|
|
495
|
+
const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint, classifyTestFailureArtifact } = await import('../verify-postcheck.js')
|
|
496
|
+
// ── 失败签名去重(ql-20260920-010 修复一 d)──
|
|
497
|
+
// 复入本步前先对账上次失败:代码指纹 + known_failures 豁免面 + 快照口径都没变 → 失败
|
|
498
|
+
// 必然原样复现,重跑是纯等待(对撞三轮三连 8~10 分钟假红重试的根治)。复用只消费
|
|
499
|
+
// failed 态记录(passed 态 --done 复用归 loadReusableQualityScan),逃生阀
|
|
500
|
+
// SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1。
|
|
501
|
+
if (process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN !== '1') {
|
|
502
|
+
const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
|
|
503
|
+
const verdict = shouldReuseLastFailedScan({
|
|
504
|
+
lastRecord,
|
|
505
|
+
currentDedupKey: computeQualityScanDedupKey({ cwd, specBase }),
|
|
506
|
+
plannedSnapshot: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,
|
|
507
|
+
})
|
|
508
|
+
if (verdict.reuse) {
|
|
509
|
+
const t = lastRecord.testResult
|
|
510
|
+
console.error(`\n🛑 检测到上次质量扫描失败且失败签名未变(代码指纹 / known_failures 豁免面 / 快照口径均未变,${lastRecord.ranAt ? '上次运行 ' + lastRecord.ranAt : ''})——本次跳过重跑,直接复用上次失败结果:`)
|
|
511
|
+
console.error(` 上次实测:\`${t.command}\` — ${(t.reason || '').split('\n')[0]}`)
|
|
512
|
+
if (t.outputTail) {
|
|
513
|
+
for (const line of t.outputTail.split('\n').slice(-8)) console.error(` | ${line}`)
|
|
514
|
+
}
|
|
515
|
+
const replayTriage = classifyTestFailureArtifact(t)
|
|
516
|
+
if (t.artifactTriage) {
|
|
517
|
+
console.error(` 🔬 上次已做过伪影分诊(${t.artifactTriage.kind})且主树对照复跑仍失败——非沙箱伪影,别再换口径重试`)
|
|
518
|
+
} else if (replayTriage.matched) {
|
|
519
|
+
console.error(` 🔬 失败签名命中伪影形态(${replayTriage.kind}):${replayTriage.advice}`)
|
|
520
|
+
console.error(' (上次运行早于分诊机制——本次重跑会自动做主树口径对照;如需立即重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1)')
|
|
521
|
+
}
|
|
522
|
+
console.error(' 出路(按序):① 修代码(指纹即失配,下次自动重测)② 补 known_failures 豁免(豁免面即失配)③ 换快照口径(设/删 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,口径变化即失配)④ 环境确已修好后强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1')
|
|
523
|
+
throw new Error(`noAI 质量扫描复用上次失败结果(失败签名未变,跳过重跑)——${(t.reason || '测试失败').split('\n')[0]}。修复后重跑 sillyspec run verify${changeName ? ` --change ${changeName}` : ''},或按上方出路处置。`)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
405
526
|
// ── 隔离快照定向跑(2026-09-11 驾驭第十二批,用户第三次撞上:noAI 质量扫描是 verify 的
|
|
406
527
|
// 第一实测执行点,第八批只接了 gates.js verify 块——本步仍跑 main 工作区,并行 WIP 在此弄红
|
|
407
528
|
// 无辜变更)。与 gates.js 同款:createVerifyGateSnapshot(HEAD + 本变更文件集,native worktree
|
|
@@ -429,6 +550,13 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
429
550
|
let smokeResult = null
|
|
430
551
|
try {
|
|
431
552
|
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
|
|
553
|
+
// ── 伪影分诊(ql-20260920-010 修复一 a):失败先分诊,命中 → 主树口径单点复跑对照 ──
|
|
554
|
+
testCheck = applyArtifactTriageRerun({
|
|
555
|
+
testCheck,
|
|
556
|
+
classify: classifyTestFailureArtifact,
|
|
557
|
+
snap,
|
|
558
|
+
rerunInMainScope: () => runVerifyTestCheck({ cwd, specBase, changeName }),
|
|
559
|
+
})
|
|
432
560
|
printVerifyTestCheck(testCheck)
|
|
433
561
|
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snap ? 5 * 60 * 1000 : undefined })
|
|
434
562
|
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
@@ -463,7 +591,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
463
591
|
} finally {
|
|
464
592
|
if (snap) { try { snap.cleanup() } catch {} }
|
|
465
593
|
}
|
|
466
|
-
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck })
|
|
594
|
+
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, usedSnapshot: Boolean(snap) })
|
|
467
595
|
const testFailed = testCheck.status === 'failed'
|
|
468
596
|
const lintBlocked = shouldBlockVerifyLint(lintCheck)
|
|
469
597
|
// 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -401,6 +401,12 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
401
401
|
- 时间盒收敛:审查子代理的调研是收敛动作不是发散——必读材料读完即逐条出结论并落盘 review.json;仅个别结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 立即停止扩展、基于已读材料收敛)。
|
|
402
402
|
- 写通道降级:子代理写操作持续被平台拒绝(session not in running turn 类故障)→ 重试 ≤3 次即停,把完整审查结论(含 review.json 全文)作为最终文本回传主代理,由主代理代为落盘并在 reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
403
403
|
|
|
404
|
+
**审查经济学三律(2026-09-20 对撞实验实测驱动——前置阶段 23 分钟审查等待的结构性收口)**:
|
|
405
|
+
- **请求预算硬钳(时间盒的数字化形态)**:子代理审查是「按清单作答」不是「自由调研」——请求预算硬上限 **12 次**(设计审查 ≤8 次必读+≤4 次定向补证);到达即基于已读材料出 verdict,禁止「再确认一下」式续读(2026-09-20 实证:48 请求审一份 287 行 design = 17 分钟纯等待,根因是审查者陷入自由调研)。派发 prompt 必须明示此预算。
|
|
406
|
+
- **FAIL 后增量复审(禁全文重审)**:首轮 verdict=fail → 你修复阻断项后,**resume 同一审查子代理**(SendMessage 回原 agent)只发修复说明与 diff,要求它仅验证「首轮阻断项是否解决」——不重开全文审(2026-09-20 实证:resume 增量复审 64s/8 调用 vs 新子代理全文审 322s/26 调用 = 5 倍差距)。复审 review.json 的 checklist 追加增量复核行(item 含「增量复审:首轮阻断项 N 条」),verdict 按增量结果改判。原 agent 已不可 resume(会话过期等)才允许新开子代理(此时 prompt 里附首轮 review.json 阻断项,仍只验阻断项)。
|
|
407
|
+
- **Grill 并行化(fire-and-forget)**:tier=independent 时派发审查子代理用 **run_in_background: true**,然后**立即进入 Step 8 生成规范文件**——不阻塞等待审查结果(Grill 平均可省 15 分钟墙钟)。Step 8 产出完成、即将 \`--done\` 本步骤前,回收子代理结果:verdict=pass → 直接完成;verdict=fail → 走上条增量复审流程;子代理仍在跑 → 轮询等待(它有 12 请求预算不会发散太久)。Stage Review Gate 在 \`--done\` 时仍硬校验 review.json 在场——并行化省的是墙钟不是门。
|
|
408
|
+
- **审查只读纪律(审查员是法医不是外科医生——并行化的前置安全条件)**:审查子代理的工具面**只读**(Read/Grep/只读 Bash 如 cat/ls/git log/git diff);**禁止 Edit/Write 任何被审产物**(design.md/四件套/源码),唯一可写文件是 review.json。发现问题 → 写进 review.json 的 checklist/blockers 由主代理修复(2026-09-20 实证:Grill 审查员在审查窗口直接 Edit design.md 15 处——主会话零请求窗口铁证——三重危害:独立性破坏〔自己审自己改,pass 有效性打折〕、责任链断裂〔修改未经主代理复核即生效〕、回合数爆炸〔边审边改 48 回合 vs 正常 12~15〕)。与上条并行化叠加时本纪律是硬前提:审查员后台改文件的同时主链在前台写后续产物 = 同文件双写竞态。
|
|
409
|
+
|
|
404
410
|
### 仪式档位菜单(ceremony_tier 按 risk 客观定价——注入的 tier 行已标注当前档)
|
|
405
411
|
- S0:CLI 清单核验——按下方交叉审查模型/交叉点清单机械逐项核对(零 token 发散),结论逐条附证据锚点,无需独立子代理
|
|
406
412
|
- S1:CLI 清单核验 + 定向探针抽查——机械项全查之外,对最高风险的 1-2 个交叉点做定向源码探针(读相关实现验证设计假设),无需独立子代理
|
package/src/stages/execute.js
CHANGED
|
@@ -439,6 +439,7 @@ ${REVIEW_CHECKLISTS.execute.map((item, index) => ' ' + (index + 1) + '. ' + ite
|
|
|
439
439
|
**产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
|
|
440
440
|
**执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证)**:brainstorm/plan 阶段已实证的结论(既往 stage-review checklist pass 项、file:line 锚点)直接引用勿重验;必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。QA 子代理写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
441
441
|
**gate 重试修复**:review.json 落盘后若 design.md 又有改版,gate 会**自动机械重算 docHash 放行**(verdict/checklist 保留,结论是否仍适用于新文档需人工确认)——**不要重做审查**(重做=同一材料第三遍);主文档路径错/缺失不会被自动修复,按 gate 报错修正 reviewedFiles[0],或跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\`。
|
|
442
|
+
**审查经济学三律(2026-09-20 对撞实验驱动,同 brainstorm/plan 审查步)**:①请求预算硬钳——QA 子代理请求上限 **15 次**(材料包+抽查 task 的 diff 文件+全量重审 task 的核心文件),到达即出 verdict;②FAIL 后增量复审——resume 原 QA 子代理只发修复说明+diff 验证阻断项(不全文重审,5 倍差实证),checklist 追加增量复核行;③并行化——tier=independent 时 run_in_background: true 派发后主代理继续下一知识库审阅步骤,回收在即将 --done 代码审查步骤前;④**审查只读纪律**——QA 子代理工具面只读(Read/Grep/只读 Bash),禁止 Edit/Write 被审产物(源码/四件套/review.json 之外的生成物),唯一可写 review.json——发现问题写 checklist/blockers 由主代理/对应 task 修复(2026-09-20 实证 Grill 越权改 design 15 处:独立性破坏+责任链断裂+回合爆炸;③并行化下同文件双写竞态的前置防线)。
|
|
442
443
|
|
|
443
444
|
### 操作(材料包口径——2026-09-19-review-material-cli-wiring)
|
|
444
445
|
1. **评审材料包(基准面)**:CLI 已机械组装注入下方(src/review-material-pack.js assembleStageReviewMaterials('execute-qa'):diff 摘要+design 热区+验收清单)——checklist 逐条只对包作答;包外文件可定向查证但须列明(禁全量扫读);包不足以作答→cannot_verify+requiredEvidence 列缺件。task review 双 pass 抽查、未双 pass 全量重审的分级照旧(上方「审查范围分级」);tier=independent 时把整包贴进 QA 子代理派发 prompt:
|
|
@@ -1562,6 +1562,51 @@ function validateWaveProposal(changeDir, waves, repoRegistry = null) {
|
|
|
1562
1562
|
/** plan_level 客观复核阈值单点(2026-09-09-plan-derived FR-03,D-003@v1) */
|
|
1563
1563
|
const PLAN_LEVEL_SIGNALS = { files: 8, modules: 2 }
|
|
1564
1564
|
|
|
1565
|
+
/**
|
|
1566
|
+
* Wave 结构分诊(ql-20260920-010 修复二,对撞三轮 execute 108 分钟串行的收口):
|
|
1567
|
+
* 显式 Wave 排布 vs depends_on 拓扑最小波数的差值判定。判据=**可合并相邻波对**:
|
|
1568
|
+
* 相邻 (W_i, W_{i+1}) 之间无依赖边(W_{i+1} 任务不 depends_on W_i 任务)且两波文件面
|
|
1569
|
+
* (allowed_paths 并集)无交集 → 合并不破坏任何约束,是纯手工串行化。刻意不用
|
|
1570
|
+
* 「波数 ≥5 且平均每波 <2.5」的粗暴启发式——它会误伤真依赖链(task-N 依赖 task-(N-1)
|
|
1571
|
+
* 全链波数=N≥5、平均=1,但串行合法必须放行);可合并对计数对真链恒为 0(拓扑最小波
|
|
1572
|
+
* 的每对相邻波间必有依赖边),天然免疫。
|
|
1573
|
+
* - 可合并对 ≥2 → error(伪并行串行链:三轮形态=13 任务 6 波全串行、平均 2.2/波、
|
|
1574
|
+
* Wave 5 单波 30 分钟,并行度归零);
|
|
1575
|
+
* - 显式波数=拓扑最小(diff=0)且波数 ≥5 → advisory 深链提示(真串行合法放行,
|
|
1576
|
+
* 但 depends_on 可能过声明——task-07 只需 task-01 产物却声明依赖 task-06 那种;
|
|
1577
|
+
* 阻断归阻断、提示归提示,不混淆)。
|
|
1578
|
+
* 文件面交集的相邻对不算可合并(同 Wave 共享文件=双写竞态,分离合法)——与
|
|
1579
|
+
* validateWaveProposal 的「卡片读取失败按无重叠证据」同口径,卡片缺/未声明 → 空面。
|
|
1580
|
+
* 纯函数(文件面由调用方注入,测试不依赖卡片读盘)。
|
|
1581
|
+
* @param {{ existingWaves: string[][], topoWaves: string[][], depMap: Map<string,string[]>, wavePathSets?: (Set<string>|null)[] }} args
|
|
1582
|
+
* @returns {{ mergeablePairs: Array<[number,number]>, error: string|null, advisory: string|null }}
|
|
1583
|
+
*/
|
|
1584
|
+
export function assessWaveStructure({ existingWaves, topoWaves, depMap, wavePathSets = [] }) {
|
|
1585
|
+
const mergeablePairs = []
|
|
1586
|
+
for (let i = 0; i + 1 < existingWaves.length; i++) {
|
|
1587
|
+
const cur = existingWaves[i]
|
|
1588
|
+
const next = existingWaves[i + 1]
|
|
1589
|
+
const curSet = new Set(cur)
|
|
1590
|
+
const hasDepEdge = next.some(t => (depMap.get(t) || []).some(d => curSet.has(d)))
|
|
1591
|
+
if (hasDepEdge) continue
|
|
1592
|
+
const a = wavePathSets[i] || new Set()
|
|
1593
|
+
const b = wavePathSets[i + 1] || new Set()
|
|
1594
|
+
let overlap = false
|
|
1595
|
+
for (const p of a) { if (b.has(p)) { overlap = true; break } }
|
|
1596
|
+
if (overlap) continue
|
|
1597
|
+
mergeablePairs.push([i, i + 1])
|
|
1598
|
+
}
|
|
1599
|
+
let error = null
|
|
1600
|
+
let advisory = null
|
|
1601
|
+
if (mergeablePairs.length >= 2) {
|
|
1602
|
+
const detail = mergeablePairs.map(([a, b]) => `Wave ${b + 1} 可并入 Wave ${a + 1}`).join('、')
|
|
1603
|
+
error = `planPostcheck: Wave 伪并行串行链——${detail}(相邻波间无 depends_on 依赖边且文件面无交集,合并不破坏任何约束),手工排布比依赖拓扑多出 ${mergeablePairs.length} 段纯串行化:同层无依赖的波必须合并(execute 按 Wave 串行推进,碎片化波=并行度归零);或回 tasks 卡核对 depends_on 是否过声明。sillyspec plan-adopt-waves --change <变更名> 可按 depends_on 拓扑一键重排(重排后同波文件面有交集会被拒绝,需手工拆分)`
|
|
1604
|
+
} else if (existingWaves.length >= 5 && existingWaves.length === topoWaves.length) {
|
|
1605
|
+
advisory = `Wave 链深 ${existingWaves.length}(与 depends_on 拓扑最小一致,串行合法放行)——若任务间实际没有这么强的依赖,回 tasks 卡核对 depends_on 是否过声明(如 task-07 只需 task-01 的产物却声明依赖 task-06);砍掉过声明依赖可提升并行度`
|
|
1606
|
+
}
|
|
1607
|
+
return { mergeablePairs, error, advisory }
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1565
1610
|
/**
|
|
1566
1611
|
* plan_level 第二把尺子(不接管,仅 warning):design 文件清单数 / 模块跨度 / task 数
|
|
1567
1612
|
* 与声明档位比对。fail-open(design/map 读取失败 → null 零输出)。
|
|
@@ -1919,6 +1964,27 @@ export async function executePlanPostcheck(context) {
|
|
|
1919
1964
|
}
|
|
1920
1965
|
// 结构不一致但方向合法(手工保守串行)→ 静默放行(2026-09-09-plan-derived:
|
|
1921
1966
|
// 合法安全模式消每轮提示噪音;手动对齐出口保留 sillyspec plan-adopt-waves)
|
|
1967
|
+
|
|
1968
|
+
// ── 伪并行串行链守卫(ql-20260920-010 修复二)──
|
|
1969
|
+
// 「静默放行」被三轮对撞实证穿透:plan 把 13 任务手排 6 波全串行(方向合法)→
|
|
1970
|
+
// execute 108 分钟并行度归零。静默放行只对「保守但收敛」的排布安全,对碎片化
|
|
1971
|
+
// 串行链是漏门——此处按可合并相邻波对分诊(见 assessWaveStructure):≥2 可合并
|
|
1972
|
+
// → 硬拦;深链(diff=0 且波数≥5)→ advisory 提示核 depends_on,不阻断。
|
|
1973
|
+
const wavePathSets = existingWaves.map(ws => {
|
|
1974
|
+
const s = new Set()
|
|
1975
|
+
for (const t of ws) {
|
|
1976
|
+
try {
|
|
1977
|
+
for (const p of parseAllowedPaths(readFileSync(pJoin(tasksDir, `${t}.md`), 'utf8'))) s.add(p)
|
|
1978
|
+
} catch { /* 卡片缺失/读失败按空面(同 validateWaveProposal 口径) */ }
|
|
1979
|
+
}
|
|
1980
|
+
return s
|
|
1981
|
+
})
|
|
1982
|
+
const structure = assessWaveStructure({ existingWaves, topoWaves: waves, depMap, wavePathSets })
|
|
1983
|
+
if (structure.advisory) console.warn(` ⚠️ ${structure.advisory}`)
|
|
1984
|
+
if (structure.error) {
|
|
1985
|
+
console.error(`\n❌ ${structure.error}`)
|
|
1986
|
+
throw new Error(structure.error)
|
|
1987
|
+
}
|
|
1922
1988
|
}
|
|
1923
1989
|
}
|
|
1924
1990
|
}
|
package/src/stages/plan.js
CHANGED
|
@@ -368,6 +368,7 @@ prompt 要点:
|
|
|
368
368
|
4. verdict=fail 时在 reviewerNotes 写明阻断项
|
|
369
369
|
5. 执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:plan 审查重核三仓源码 94 分钟不收敛——brainstorm 阶段已两轮实证的结论被全量重验):brainstorm 阶段 design review 已实证的结论(checklist pass 项、file:line 锚点)**直接引用勿重验**,本阶段只审 plan 特有面(任务拆分/依赖/契约/规模映射);必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。
|
|
370
370
|
6. 写通道降级:写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)作为最终文本回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
371
|
+
7. **审查经济学三律(2026-09-20 对撞实验驱动,同 brainstorm Grill 步)**:①请求预算硬钳——子代理请求上限 **10 次**(材料包基准面 + plan.md + ≤3 次定向补证),到达即出 verdict 禁「再确认一下」续读,派发 prompt 明示此预算;②FAIL 后增量复审——修复阻断项后 **resume 同一子代理**只发修复说明+diff 验证「阻断项解决否」,不重开全文审(64s vs 322s = 5 倍差实证),review.json checklist 追加增量复核行;原 agent 不可 resume 才新开(prompt 附首轮阻断项仍只验阻断项);③**并行化**——派发用 run_in_background: true,主代理立即继续 Step 4(生成 TaskCard),回收在即将 --done 本步骤前(pass → 完成 / fail → 增量复审 / 在跑 → 等待),Stage Review Gate 照常硬校验;④**审查只读纪律**——子代理工具面只读(Read/Grep/只读 Bash),禁止 Edit/Write 被审产物(design/plan/四件套/源码),唯一可写 review.json,发现问题写 checklist/blockers 由主代理修(2026-09-20 实证 Grill 越权改 design 15 处;③并行化下同文件双写竞态的前置防线,详见 brainstorm Grill 步全文)。
|
|
371
372
|
|
|
372
373
|
### module-impact.md 首版(scale≠small 时:CLI 自动生成,审查步勿手写)
|
|
373
374
|
module-impact.md 首版**由 CLI 在本阶段 --done 时自动生成**——文件×模块归属按 _module-map.yaml 前缀匹配机械预填,章节含「## 模块影响矩阵」「## 未匹配文件」「## 更新结果」表骨架(每受影响模块一行 pending),影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。已存在不覆盖。手写整份首版是历史返工根源(章节标题变体会被 archive contains_sections 硬拦),**本步不要手写**。
|
package/src/verify-postcheck.js
CHANGED
|
@@ -39,6 +39,7 @@ import { resolveChangeRisk } from './change-risk-profile.js'
|
|
|
39
39
|
import { loadBlastDeclarationsAllProjects } from './blast-surface.js'
|
|
40
40
|
import { loadSpanRiskPatternsAllProjects } from './span-risk-surface.js'
|
|
41
41
|
import { reconcileDualRun, computeCeremonyTier, CEREMONY_TIERS } from './ceremony-tier.js'
|
|
42
|
+
import { readCeremonyPricingConfig } from './ceremony-config.js'
|
|
42
43
|
import { mergeFrictionEntry } from './friction-ledger.js'
|
|
43
44
|
import { discoverModuleIndex } from './decision-distill.js'
|
|
44
45
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
@@ -952,6 +953,71 @@ export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailur
|
|
|
952
953
|
}
|
|
953
954
|
}
|
|
954
955
|
|
|
956
|
+
// ═══ 沙箱伪影分诊(ql-20260920-010 修复一 a,对撞三轮 68 分钟单步收口)═══
|
|
957
|
+
// 三轮实测:verify 质量扫描三连假红重试(9.7+8.2+7.8 分钟)根因全是工具/环境自身缺陷
|
|
958
|
+
// 而非被测代码——四类已知形态:沙箱环境缺件(venv dev 依赖/工具二进制不在)、overlay
|
|
959
|
+
// 半成品缺模块、CRLF 字节差(autocrlf 检出 vs 快照期望)、判账正则把通过/汇总行误判
|
|
960
|
+
// 失败行(PER_TEST_FAIL_RE 残留形态)。本函数只做**签名分类**(纯函数零 IO),处置归
|
|
961
|
+
// 消费侧(run/verify-quality-scan.js executeVerifyQualityScan):快照口径失败且命中 →
|
|
962
|
+
// 主树口径单点复跑对照,复跑过 = 伪影坐实按主树结果放行;复跑仍挂 = 真失败照常拦截。
|
|
963
|
+
// 分诊永不单独放行——只改「要不要换口径再测一次」与「给 agent 的归因提示」。
|
|
964
|
+
const ARTIFACT_ENV_MISSING_RES = [
|
|
965
|
+
// 条目 H 签名族:pytest-xdist 缺失 → usage error(unrecognized -n/--dist)
|
|
966
|
+
/ERROR:\s*usage:\s*py(?:test|thon)[^\n]*(?:-n\b|--dist|xdist|unrecognized argument)/i,
|
|
967
|
+
/ModuleNotFoundError:\s*No module named\s+'?(?:pytest|pytest_xdist|xdist|_pytest)'?/i,
|
|
968
|
+
/(?:command not found|not recognized as an internal or external command)[^\n]*(?:pytest|vitest|jest|mocha|ruff|tsc|eslint|python)/i,
|
|
969
|
+
]
|
|
970
|
+
const ARTIFACT_MODULE_IMPORT_RES = [
|
|
971
|
+
// overlay 部分态:改动引用方没改被引用方 → 本地顶层包 import 不到(①已先排除 pytest 家族)
|
|
972
|
+
/ModuleNotFoundError:\s*No module named\s+'?[A-Za-z_][\w.]*'?/i,
|
|
973
|
+
/ImportError:\s*cannot import name\s+\S+\s+from\s+/i,
|
|
974
|
+
]
|
|
975
|
+
const ARTIFACT_CRLF_RES = [
|
|
976
|
+
/\\r\\n/i, // 断言差异里字面 \r\n 转义可见(jest/vitest 快照与精确串断言)
|
|
977
|
+
/\bCRLF\b/i,
|
|
978
|
+
/line endings?\s*(?:mismatch|differ)/i,
|
|
979
|
+
]
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* 失败结果伪影分诊。优先消费 failureRemaining(判账行集,不受 tail 截断),缺失时回退
|
|
983
|
+
* outputTail。返回 { matched, kind, evidence, advice };matched=false 时其余字段为
|
|
984
|
+
* null/[]。分类顺序刻意:env-missing 先于 module-import(pytest 家族两处命中算环境缺件)。
|
|
985
|
+
* @param {{ status:string, failureRemaining?:string[], outputTail?:string|null }} result
|
|
986
|
+
* @returns {{ matched:boolean, kind:string|null, evidence:string[], advice:string|null }}
|
|
987
|
+
*/
|
|
988
|
+
export function classifyTestFailureArtifact(result) {
|
|
989
|
+
const empty = { matched: false, kind: null, evidence: [], advice: null }
|
|
990
|
+
if (!result || result.status !== 'failed') return empty
|
|
991
|
+
const lines = (Array.isArray(result.failureRemaining) && result.failureRemaining.length > 0)
|
|
992
|
+
? result.failureRemaining.map(String)
|
|
993
|
+
: String(result.outputTail || '').split(/\r?\n/).filter(Boolean)
|
|
994
|
+
if (lines.length === 0) return empty
|
|
995
|
+
const hit = (res) => lines.filter(l => res.some(re => re.test(l.replace(ANSI_RE, ''))))
|
|
996
|
+
const pick = (ev) => ev.slice(0, 3).map(l => { const t = l.trim(); return t.length > 120 ? t.slice(0, 120) + '…' : t })
|
|
997
|
+
|
|
998
|
+
const envEv = hit(ARTIFACT_ENV_MISSING_RES)
|
|
999
|
+
if (envEv.length > 0) {
|
|
1000
|
+
return { matched: true, kind: 'sandbox-env-missing', evidence: pick(envEv),
|
|
1001
|
+
advice: '疑似沙箱环境缺件(venv dev 依赖 / 工具二进制不在 PATH)——先主树口径对照复跑再定论;若主仓同样缺件,补环境(uv sync --group dev / 安装工具)而非改代码' }
|
|
1002
|
+
}
|
|
1003
|
+
const impEv = hit(ARTIFACT_MODULE_IMPORT_RES)
|
|
1004
|
+
if (impEv.length > 0) {
|
|
1005
|
+
return { matched: true, kind: 'overlay-module-missing', evidence: pick(impEv),
|
|
1006
|
+
advice: '疑似快照 overlay 部分态缺模块(改了引用方没改被引用方)——主树口径对照复跑可分辨;主树也挂才是真失败' }
|
|
1007
|
+
}
|
|
1008
|
+
const crlfEv = hit(ARTIFACT_CRLF_RES)
|
|
1009
|
+
if (crlfEv.length > 0) {
|
|
1010
|
+
return { matched: true, kind: 'crlf-diff', evidence: pick(crlfEv),
|
|
1011
|
+
advice: '疑似 CRLF/LF 字节差(autocrlf 检出口径 vs 断言期望)——主树口径对照复跑可分辨;确认后统一行尾口径(.gitattributes / 编辑器设置)' }
|
|
1012
|
+
}
|
|
1013
|
+
// 判账假阳性:所有「失败行」剥 ANSI 后全是通过/汇总/捕获块形态——失败标记本身误判
|
|
1014
|
+
if (lines.every(l => { const b = l.replace(ANSI_RE, ''); return PASS_LINE_RE.test(b) || SUMMARY_LINE_RE.test(b) || CONSOLE_CAPTURE_RE.test(b) })) {
|
|
1015
|
+
return { matched: true, kind: 'parser-false-positive', evidence: pick(lines),
|
|
1016
|
+
advice: '判账疑似把通过/汇总行误判为失败行(PER_TEST_FAIL_RE 残留形态)——请把失败输出原文上报工具仓修判定正则;exitCode 非零的根因另在输出里' }
|
|
1017
|
+
}
|
|
1018
|
+
return empty
|
|
1019
|
+
}
|
|
1020
|
+
|
|
955
1021
|
/**
|
|
956
1022
|
* 决定 verify 实测的执行动作(纯函数,便于测试;坑 verify-worktree-... 修复方向 3)。
|
|
957
1023
|
* - skip → 'skip'(真跳过,D-005@v2——不落 full 兜底;evidence-auto 经
|
|
@@ -1392,11 +1458,20 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1392
1458
|
console.warn(`📋 known_failures 豁免清单已加载:${knownFailures.length} 条模式(local.yaml)`)
|
|
1393
1459
|
}
|
|
1394
1460
|
|
|
1461
|
+
// —— 缺省收窄(ql-20260920-010 修复一 c,对撞三轮 68 分钟单步构成之一=缺省全量)——
|
|
1462
|
+
// modules: 已配置且未显式 test_strategy → 缺省按 module 子集实测。未配置 modules: 的仓
|
|
1463
|
+
// 保持缺省 full 零打扰;显式 test_strategy: full 是用户有意跑全量,不受影响。
|
|
1464
|
+
let defaultedToModule = false
|
|
1465
|
+
if (rawStrategy === null && extractModules(yamlText)) {
|
|
1466
|
+
defaultedToModule = true
|
|
1467
|
+
console.warn('ℹ️ test_strategy 未配置但 modules: 已配置——缺省按 module 子集实测(v3.29.3 起收窄;显式写 test_strategy: full 恢复全量)')
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1395
1470
|
// —— evidence-auto 生效策略解析(D-005@v2 / task-11)——
|
|
1396
1471
|
// 按变更目录 module-impact.md 影响面取生效策略(行为→module、纯文档/门禁→skip、
|
|
1397
1472
|
// 缺失/不可解析→降级 module 并注记)再进既有链路;full/module/skip/缺省四路径
|
|
1398
1473
|
// 不经此分支(消费语义逐字不变)。
|
|
1399
|
-
let strategy = rawStrategy
|
|
1474
|
+
let strategy = defaultedToModule ? 'module' : rawStrategy
|
|
1400
1475
|
let evidenceAuto = null
|
|
1401
1476
|
if (rawStrategy === 'evidence-auto') {
|
|
1402
1477
|
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
@@ -1503,7 +1578,8 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1503
1578
|
durationMs: null,
|
|
1504
1579
|
outputTail: null,
|
|
1505
1580
|
reason: 'test_strategy: module 但本次变更未命中任何已配置 modules(0 命中)。为避免回退到注定超时/含预存失败的全量 commands.test,CLI 未自动跑全量——据 verify-result.md 自报告判定测试。若需全量覆盖,显式设 test_strategy: full。' +
|
|
1506
|
-
` 诊断:${diagLines.join(';')}` + eaNote
|
|
1581
|
+
` 诊断:${diagLines.join(';')}` + eaNote
|
|
1582
|
+
+ (defaultedToModule ? '(注:module 为缺省收窄——local.yaml 未显式配置 test_strategy;常跑全量请显式写 test_strategy: full)' : ''),
|
|
1507
1583
|
resultPath: null,
|
|
1508
1584
|
mode: 'module-zero-hit',
|
|
1509
1585
|
fallbackReason: null,
|
|
@@ -1515,6 +1591,7 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1515
1591
|
// evidence-auto 解析为 module 后落全量兜底时追加推荐来源注记(eaNote;其余路径空串零变化)。
|
|
1516
1592
|
let fallbackReason = computeFullFallbackReason({ strategy, modulesPresent, hitCount })
|
|
1517
1593
|
if (fallbackReason && eaNote) fallbackReason = fallbackReason + eaNote
|
|
1594
|
+
if (fallbackReason && defaultedToModule) fallbackReason = fallbackReason + '(注:module 为缺省收窄——local.yaml 未显式配置 test_strategy)'
|
|
1518
1595
|
mainResult = runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason, knownFailures })
|
|
1519
1596
|
}
|
|
1520
1597
|
|
|
@@ -3075,9 +3152,12 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
|
|
|
3075
3152
|
try { factModuleIndex = discoverModuleIndex(join(sb, 'knowledge')) } catch { /* 缺 map → span 跨模块维跳过 */ }
|
|
3076
3153
|
|
|
3077
3154
|
// —— 引擎对账(mismatch 判定单点)+ 命中明细(同入参跑 computeCeremonyTier 取 reasons,供披露)——
|
|
3155
|
+
// pricing 配置注入(2026-09-20 项目化定价):双跑事实面与 classifyReviewTier 声明面同源读
|
|
3156
|
+
// local.yaml ceremony: 段——两侧配置不一致会制造假 mismatch,此处与 review-tier 同 specBase 口径。
|
|
3157
|
+
const factPricingConfig = readCeremonyPricingConfig(sb)
|
|
3078
3158
|
const verdict = reconcileDualRun({ declaredTier, factBlastTier: factRisk.tier, factFiles: actual.files, factModuleIndex, factSpanRiskPatterns })
|
|
3079
3159
|
const factDetail = computeCeremonyTier({
|
|
3080
|
-
blastTier: factRisk.tier, declaredFiles: actual.files, moduleIndex: factModuleIndex, spanRiskPatterns: factSpanRiskPatterns, frictionCounts: {},
|
|
3160
|
+
blastTier: factRisk.tier, declaredFiles: actual.files, moduleIndex: factModuleIndex, spanRiskPatterns: factSpanRiskPatterns, frictionCounts: {}, config: factPricingConfig,
|
|
3081
3161
|
})
|
|
3082
3162
|
if (verdict.severity === 'warn') {
|
|
3083
3163
|
notes.push(`[advisory] 高报记账(D-005):声明档 ${String(declaredTier)} > 事实档 ${verdict.factTier}——只记账不阻断不记摩擦(高报代价是更重仪式,自罚机制在场)`)
|