sillyspec 3.29.2 → 3.29.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.
Files changed (42) hide show
  1. package/analyze-transcript.cjs +38 -0
  2. package/asset-audit-2026-09-20/README.md +42 -0
  3. package/asset-audit-2026-09-20/asset-audit.cjs +253 -0
  4. package/asset-audit-2026-09-20/extract-candidates.cjs +37 -0
  5. package/asset-audit-2026-09-20/platform-archive.csv +294 -0
  6. package/asset-audit-2026-09-20/platform-fr.csv +12 -0
  7. package/asset-audit-2026-09-20/platform-gotchas.csv +6 -0
  8. package/asset-audit-2026-09-20/platform-knownissues.csv +23 -0
  9. package/asset-audit-2026-09-20/platform-modules.csv +222 -0
  10. package/asset-audit-2026-09-20/platform-quicklog.csv +1076 -0
  11. package/asset-audit-2026-09-20/platform-troubleshooting-candidates.csv +88 -0
  12. package/asset-audit-2026-09-20/platform-troubleshooting.csv +1 -0
  13. package/asset-audit-2026-09-20/sillyspec-archive.csv +108 -0
  14. package/asset-audit-2026-09-20/sillyspec-fr.csv +51 -0
  15. package/asset-audit-2026-09-20/sillyspec-gotchas.csv +4 -0
  16. package/asset-audit-2026-09-20/sillyspec-knownissues.csv +34 -0
  17. package/asset-audit-2026-09-20/sillyspec-modules.csv +30 -0
  18. package/asset-audit-2026-09-20/sillyspec-quicklog.csv +460 -0
  19. package/asset-audit-2026-09-20/sillyspec-troubleshooting.csv +69 -0
  20. package/asset-audit-2026-09-20/summary.json +241 -0
  21. package/package.json +1 -1
  22. package/src/ceremony-config.js +64 -0
  23. package/src/ceremony-tier.js +66 -13
  24. package/src/config-schema.js +16 -3
  25. package/src/cross-repo-reconcile.js +232 -46
  26. package/src/fr-index.js +80 -2
  27. package/src/index.js +2 -2
  28. package/src/review-tier.js +5 -2
  29. package/src/run/bg-sync.js +231 -0
  30. package/src/run/complete-handlers.js +208 -5
  31. package/src/run/gates.js +10 -5
  32. package/src/run/prompt.js +1 -1
  33. package/src/run/shared.js +32 -0
  34. package/src/run/verify-quality-scan.js +131 -3
  35. package/src/scope-audit.js +303 -20
  36. package/src/stages/brainstorm.js +6 -0
  37. package/src/stages/execute.js +1 -0
  38. package/src/stages/plan-postcheck.js +66 -0
  39. package/src/stages/plan.js +1 -0
  40. package/src/stages/quick.js +2 -0
  41. package/src/sync.js +12 -0
  42. package/src/verify-postcheck.js +92 -8
@@ -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 个交叉点做定向源码探针(读相关实现验证设计假设),无需独立子代理
@@ -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
  }
@@ -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 硬拦),**本步不要手写**。
@@ -70,6 +70,8 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
70
70
  ### 📌 末步预告:结果摘要四字段(step3 --done 硬校验,缺一被拒)
71
71
  下一步(step3)--done 必须给四字段结果摘要(推荐 \`--req/--cause/--solution/--result\` 四参数,CLI 自动合成;兼容 \`--output "需求:… 根因:… 方案:… 结果:…"\`)。其中「结果:验证情况(测试数 / lint / typecheck / 部署状态)」的素材在**本步**产生——实现摘要里请顺手记下具体验证数据(如「3 个单测通过、lint 0 告警」),step3 直接引用,避免到收尾时回忆不起来或被硬校验打回重跑。
72
72
 
73
+ ⚠️ 文件括注 \`--file-notes "path::注 || path2"\` 也只随 step3 --done 同一命令传才生效——**本步与 step1 的命令带了会被直接拒绝执行**(白跑一轮)。括注内容留到 step3 收尾时连同四字段一起传(2026-09-20 用户反馈:提示此前只在 step3 prompt 可见,第一次传白跑一轮)。
74
+
73
75
  ### 铁律
74
76
  - 不要修改无关文件
75
77
  - 不要编造不存在的 CLI 子命令
package/src/sync.js CHANGED
@@ -2281,6 +2281,18 @@ export async function syncSpecTreeOnly(changeName, cwd, opts = {}) {
2281
2281
  return syncSpecTree(safePlatformSpecDir(cwd) || join(cwd, '.sillyspec'), platform, changeName, { signal: opts.signal });
2282
2282
  }
2283
2283
 
2284
+ /**
2285
+ * 平台连接预判(run/bg-sync.js 父侧 spawn 前的免开销短路):与 _getPlatform 同源判据
2286
+ * 的布尔化——env 两键齐全,或 cwd/.sillyspec/local.yaml platform 段带 url+token。
2287
+ * 未连接时不为一次注定 no-op 的同步 spawn 后台子进程(本地独立用户零开销零行为变化)。
2288
+ * 只做连接性预判不携带凭据:子进程自行经 _getPlatform 取全量配置(判据同源必一致)。
2289
+ */
2290
+ export function peekPlatformConnected(cwd) {
2291
+ if (process.env.SILLYHUB_PLATFORM_URL && process.env.SILLYHUB_PLATFORM_TOKEN) return true;
2292
+ const p = readLocalYaml(cwd).platform;
2293
+ return Boolean(p && p.url && p.token);
2294
+ }
2295
+
2284
2296
  // TBD-hub-api: approve/reject 端点路径与请求体以 SillyHub 仓库实际 API 为准;
2285
2297
  // 对齐时只改本函数(_submitApproval),无需动 approve/reject 入口。
2286
2298
  /**
@@ -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
 
@@ -2832,21 +2909,25 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
2832
2909
  const decl = collectDeclaredTargetFiles(sb, changeName)
2833
2910
  // —— 跨仓 per-repo 对账(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004 分期:
2834
2911
  // 此前 22 个跨仓文件在归档表全标「计划未动」、跨仓全靠人工到对应仓解释)——advisory 不阻断:
2835
- // 锚点是各仓 HEAD~1..HEAD 最近提交窗口(多 task 同仓只反映最近一笔,锚点脆弱期会产②类假
2836
- // 信号),只报告不翻转主仓状态。结果经 crossRepo 字段(gates 渲染明细)与 notes(摘要随
2837
- // 各状态出口流动)双通道出口。
2912
+ // 锚点档经内核分级(A reviews 锡点区间 > B HEAD~1..HEAD 窗口 > C 未提交窗口,runtimeRoot/
2913
+ // changeName 传参贯通喂 A 档 reviews 解析——评审 G2;锚点脆弱期会产②类假信号),只报告不
2914
+ // 翻转主仓状态。结果经 crossRepo 字段(gates 渲染明细)与 notes(摘要随各状态出口流动)
2915
+ // 双通道出口。
2838
2916
  let crossRepo = []
2839
2917
  if (decl.crossRepoCards.length > 0) {
2840
2918
  try {
2841
- crossRepo = reconcileCrossRepoDeclarations({ specBase: sb, cwd, declarationsByRepo: decl.crossRepoDeclarations })
2919
+ crossRepo = reconcileCrossRepoDeclarations({ specBase: sb, cwd, declarationsByRepo: decl.crossRepoDeclarations, runtimeRoot: rt, changeName })
2842
2920
  } catch { crossRepo = [] /* 跨仓对账异常不拖垮主仓链(advisory 定位) */ }
2843
2921
  for (const label of decl.crossRepoCards) {
2844
2922
  notes.push(`跨仓 task 卡 ${label} 不进主仓对账(主仓 actual 对不上跨仓声明,D-004)——其声明已转 per-repo 对账(见下方跨仓对账段)`)
2845
2923
  }
2846
2924
  for (const r of crossRepo) {
2925
+ // 锚点档动态化(task-03):优先内核透传的 anchor.label(A/B/C/degraded 四档人类可读描述);
2926
+ // 旧形态无 anchor 字段 → 防御性回退原文案(该仓最近提交窗口)
2927
+ const anchorTxt = r.anchor && typeof r.anchor.label === 'string' && r.anchor.label ? r.anchor.label : '该仓最近提交窗口'
2847
2928
  notes.push(r.degradedReason
2848
2929
  ? `跨仓 ${r.repo}:${r.degradedReason}`
2849
- : `跨仓 ${r.repo} 对账:声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? `(另有 ${r.scaffoldCount} 脚手架已聚合)` : ''}——advisory(锚点=该仓最近提交窗口)`)
2930
+ : `跨仓 ${r.repo} 对账:声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? `(另有 ${r.scaffoldCount} 脚手架已聚合)` : ''}——advisory(锚点=${anchorTxt})`)
2850
2931
  }
2851
2932
  }
2852
2933
  if (decl.cardCount === 0) {
@@ -3075,9 +3156,12 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
3075
3156
  try { factModuleIndex = discoverModuleIndex(join(sb, 'knowledge')) } catch { /* 缺 map → span 跨模块维跳过 */ }
3076
3157
 
3077
3158
  // —— 引擎对账(mismatch 判定单点)+ 命中明细(同入参跑 computeCeremonyTier 取 reasons,供披露)——
3159
+ // pricing 配置注入(2026-09-20 项目化定价):双跑事实面与 classifyReviewTier 声明面同源读
3160
+ // local.yaml ceremony: 段——两侧配置不一致会制造假 mismatch,此处与 review-tier 同 specBase 口径。
3161
+ const factPricingConfig = readCeremonyPricingConfig(sb)
3078
3162
  const verdict = reconcileDualRun({ declaredTier, factBlastTier: factRisk.tier, factFiles: actual.files, factModuleIndex, factSpanRiskPatterns })
3079
3163
  const factDetail = computeCeremonyTier({
3080
- blastTier: factRisk.tier, declaredFiles: actual.files, moduleIndex: factModuleIndex, spanRiskPatterns: factSpanRiskPatterns, frictionCounts: {},
3164
+ blastTier: factRisk.tier, declaredFiles: actual.files, moduleIndex: factModuleIndex, spanRiskPatterns: factSpanRiskPatterns, frictionCounts: {}, config: factPricingConfig,
3081
3165
  })
3082
3166
  if (verdict.severity === 'warn') {
3083
3167
  notes.push(`[advisory] 高报记账(D-005):声明档 ${String(declaredTier)} > 事实档 ${verdict.factTier}——只记账不阻断不记摩擦(高报代价是更重仪式,自罚机制在场)`)