sillyspec 3.28.10 → 3.28.12

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 (52) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/.husky/pre-commit +4 -0
  6. package/.husky/pre-push +3 -0
  7. package/AGENTS.md +1 -1
  8. package/package.json +1 -1
  9. package/src/change-risk-profile.js +76 -6
  10. package/src/commit-guard.js +119 -0
  11. package/src/config-schema.js +23 -0
  12. package/src/design-facts.js +46 -9
  13. package/src/diagnostic-codes.js +95 -0
  14. package/src/docs-check.js +14 -10
  15. package/src/docs-debt.js +7 -2
  16. package/src/docs-gate.js +42 -15
  17. package/src/doctor-diagnostics.js +141 -1
  18. package/src/endpoint-extractor.js +50 -2
  19. package/src/foreign-declared.js +25 -16
  20. package/src/hooks/worktree-guard.js +75 -2
  21. package/src/index.js +129 -1
  22. package/src/machine-interface.js +63 -0
  23. package/src/probe7-anchor-check.js +20 -2
  24. package/src/progress/stage-machine.js +34 -5
  25. package/src/progress.js +1 -1
  26. package/src/quicklog.js +231 -1
  27. package/src/run/complete-handlers.js +172 -1
  28. package/src/run/complete.js +73 -3
  29. package/src/run/gate-snapshot.js +262 -0
  30. package/src/run/gates.js +41 -7
  31. package/src/run/prompt.js +79 -1
  32. package/src/run/shared.js +10 -0
  33. package/src/run/verify-quality-scan.js +151 -5
  34. package/src/stage-contract-spec.js +14 -0
  35. package/src/stage-contract.js +738 -8
  36. package/src/stage-review-checklist.js +16 -2
  37. package/src/stage-review.js +137 -2
  38. package/src/stages/archive.js +9 -5
  39. package/src/stages/brainstorm.js +7 -1
  40. package/src/stages/execute.js +62 -8
  41. package/src/stages/plan-postcheck.js +42 -5
  42. package/src/stages/plan.js +8 -1
  43. package/src/stages/quick.js +5 -5
  44. package/src/stages/verify.js +8 -3
  45. package/src/sync.js +69 -0
  46. package/src/task-review.js +9 -3
  47. package/src/verify-facts-schema.js +95 -10
  48. package/src/verify-postcheck.js +346 -28
  49. package/src/verify-probes.js +1221 -46
  50. package/src/worktree-apply.js +175 -3
  51. package/src/write-audit.js +55 -0
  52. package/.claude/full.json +0 -1
@@ -37,8 +37,10 @@ import { filterDeliverableFiles, classifyToolScaffold } from './worktree-apply.j
37
37
  import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
38
38
  // 探针重跑(P3b task-02 一致性抽查):依赖方向已核实无环——verify-probes 只依赖
39
39
  // fs/path/git-helper/change-list/plan-postcheck/contract-matrix/foreign-declared/run-shared,
40
- // 不反向依赖本模块,此处直连不引入循环
41
- import { runVerifyProbes } from './verify-probes.js'
40
+ // 不反向依赖本模块,此处直连不引入循环。smoke 机器段一致性(2026-09-17-api-coverage-smoke
41
+ // task-02)复用同向 import:记录读取/态推导/标记扫描解析单点在 verify-probes
42
+ // (readSmokeResultRecord/deriveSmokeSectionState/scanSmokeReceiptSection,与 ensure 注入同源)。
43
+ import { runVerifyProbes, readSmokeResultRecord, deriveSmokeSectionState, scanSmokeReceiptSection } from './verify-probes.js'
42
44
  import { parseEvidenceSlots, classifyVerifiedFile } from './verify-facts-schema.js'
43
45
 
44
46
  // 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
@@ -368,7 +370,7 @@ function collectPathTokens(text) {
368
370
 
369
371
  /**
370
372
  * 解析 module-impact.md 的「模块影响矩阵」+「未匹配文件」两节,按检查面归类影响证据。
371
- * 纯机械启发式(确定性:同输入同输出);矩阵行 <!--TODO--> 未回填 → 保守计行为类
373
+ * 纯机械启发式(确定性:同输入同输出);矩阵行 <!--TODO--> 未回填 → 保守计行为类 probe1-noqa
372
374
  * (影响面未知时多测不漏测)。矩阵节缺失 / 无数据行 → parseable=false(调用方降级)。
373
375
  *
374
376
  * @param {string|null} mdText
@@ -393,8 +395,8 @@ function classifyModuleImpactEvidence(mdText) {
393
395
  const tok = IMPACT_BEHAVIORAL_TOKENS.find((w) => rowText.includes(w))
394
396
  if (tok) { out.behavioral.push(`(影响类型:${tok})`); classified++ }
395
397
  }
396
- // 矩阵行 <!--TODO--> 未回填 → 影响面未知,保守计行为类
397
- if (String(rowText).includes('<!--TODO')) { out.behavioral.push('<!--TODO--> 未回填行'); classified++ }
398
+ // 矩阵行 <!--TODO--> 未回填 → 影响面未知,保守计行为类 /* probe1-noqa */
399
+ if (String(rowText).includes('<!--TODO')) { out.behavioral.push('<!--TODO--> 未回填行'); classified++ } /* probe1-noqa */
398
400
  return classified
399
401
  }
400
402
  for (const line of lines) {
@@ -414,7 +416,7 @@ function classifyModuleImpactEvidence(mdText) {
414
416
  matrixRows++
415
417
  consumeRow(trimmed)
416
418
  } else if (section === 'unmatched') {
417
- // 骨架产出 `- \`path\` <!--TODO-->` 列表行;手写形态可能是 `| 文件 | 处置说明 |` 表行
419
+ // 骨架产出 `- \`path\` <!--TODO-->` 列表行;手写形态可能是 `| 文件 | 处置说明 |` 表行 /* probe1-noqa */
418
420
  const bullet = trimmed.match(/^-\s+(.+)$/)
419
421
  if (bullet) { consumeRow(bullet[1]); continue }
420
422
  if (trimmed.startsWith('|')) {
@@ -803,7 +805,21 @@ const PY_TEST_ID_RE = /^\S+::\S+$/
803
805
  // 通过行用例名恰含这些字样(如「✓ … 超时后 syncStatus=failed」)会被误判失败行——
804
806
  // 2710 用例套件 382 个"失败行"里 378 假阳性,known_failures 无法逐条枚举而实质失效。
805
807
  // 框架输出里通过行恒以通过标记开头、失败行恒以失败标记开头,行首判定即足以分离两类。
806
- const PASS_LINE_RE = /^\s*(?:[✓√✔]|PASS\b)/
808
+ // ✅ 补入标记集(2026-09-16-friction5-hardening verify 移交项②实证):本仓自定义 harness 用
809
+ // 「✅ PASS: <断言名>」打印通过行,断言名高频含 fail-closed/failed/×3(乘号)——✅ 不在集内时
810
+ // 整行落 \bFAIL\b/×/failed 子串命中成假失败行(一轮 verify 实测 20/27 未豁免行为此噪声)。
811
+ const PASS_LINE_RE = /^\s*(?:[✓√✔✅]|PASS\b)/
812
+ // CLI advisory 行(行首 emoji 前缀,剥 ANSI 后判定):⚠️ warn / ℹ️ info / 🔄 进度是工具自身的
813
+ // 提示通道(如 gate-snapshot 的「⚠️ 快照 overlay 冒烟:…import 失败」——warn 正文含 ModuleNotFoundError,
814
+ // 被子串命中成失败行)。测试框架的失败行恒以 ✕✗✘×/FAIL/--- FAIL 开头,不用这些 emoji 前缀——
815
+ // 行首判定零误伤;漏检风险由 judgeWithKnownFailures 的 fail-safe(检测不到失败行不判 pass)兜底。
816
+ const ADVISORY_LINE_RE = /^[ \t]*(?:⚠️?|ℹ️?|🔄)/
817
+ // 工具词汇 fail-<x> 复合词中和(判账前从行内剥除,2026-09-16 verify 移交项②):fail-closed/
818
+ // fail-open/fail-soft/fail-safe/fail-loud/fail-fast 是本工具的行为契约词汇,PER_TEST_FAIL_RE 的
819
+ // \bFAIL\b(i 标志)会把「(fail-closed)」「--- 1.3 fail-open 三态 ---」整行误收;剥除后再测,
820
+ // 行内其余真失败词(FAILED/assertionerror/error: 等)不受影响(如 AssertionError 断言消息里
821
+ // 出现 fail-closed 仍照常命中)。
822
+ const FAIL_COMPOUND_NEUTRALIZE_RE = /fail-(?:closed|open|soft|safe|loud|fast)/gi
807
823
  // ANSI 色码剥离(分类用):TTY 捕获的输出里 ✓/× 前缀可能被色码包裹,行首锚定会失配。
808
824
  const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g
809
825
 
@@ -848,8 +864,10 @@ export function partitionFailures(output, knownFailures) {
848
864
  else pyWarnCtx = false
849
865
  }
850
866
  if (PASS_LINE_RE.test(bare)) continue
867
+ if (ADVISORY_LINE_RE.test(bare)) continue
851
868
  if (ENV_NOISE_RE.test(bare)) continue
852
- if (PER_TEST_FAIL_RE.test(bare) && !SUMMARY_LINE_RE.test(bare)) failureLines.push(l)
869
+ // fail-<x> 工具词汇中和后再测(见 FAIL_COMPOUND_NEUTRALIZE_RE 注释):真失败词不被剥除
870
+ if (PER_TEST_FAIL_RE.test(bare.replace(FAIL_COMPOUND_NEUTRALIZE_RE, '')) && !SUMMARY_LINE_RE.test(bare)) failureLines.push(l)
853
871
  }
854
872
  // 豁免匹配同样在剥 ANSI 后的行上做(坑 verify-known-failures-ansi-exemption-split,2026-09-07
855
873
  // 工具复盘):TTY 捕获的失败行里色码把可见词拦腰拆开(`× \x1b[31mtests/foo.test.ts\x1b[0m > case`
@@ -1407,6 +1425,9 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
1407
1425
  // 跨仓仓不参与 module 子集策略,只跑 full npm test(design §6 + §5.4);
1408
1426
  // 跨仓仓无 package.json → 跳过 + warn 不阻断(design §9 兼容策略)。
1409
1427
  // 任一跨仓仓 fail → 整体 fail(合并语义)。
1428
+ // 主仓 skipped(test_strategy: skip / evidence-auto 推荐跳过 / module 0 命中)→ 进短路档:
1429
+ // 未自配 commands.test 的跨仓不再 fallback npm test 假败(task-05 / FR-07,见
1430
+ // runCrossRepoTestUnderMainSkip 逐仓三态)。
1410
1431
  return mergeCrossRepoResults(mainResult, ctx)
1411
1432
  }
1412
1433
 
@@ -1419,6 +1440,9 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
1419
1440
  * - 跨仓仓无 package.json → 跳过 + console.warn(不阻断 verify)
1420
1441
  * - 跨仓仓只跑 full npm test,不参与 module 子集策略(module 映射主仓强相关)
1421
1442
  * - 任一仓 fail → 整体 fail;主仓 skipped + 跨仓仓 passed → 整体 passed(跨仓仓有测试即有效)
1443
+ * - 主仓 skipped → 短路档(task-05 / FR-07):未自配 commands.test 的跨仓随主仓 skip 短路
1444
+ * (不跑 fallback npm test 假败),自配了 commands.test 的仍跑,own test_strategy: skip
1445
+ * 的单独 skip(runCrossRepoTestUnderMainSkip 逐仓三态)
1422
1446
  *
1423
1447
  * 单仓 ctx(无跨仓 entry)→ 直接返主仓结果,零行为变化(GOAL-2)。
1424
1448
  *
@@ -1434,9 +1458,15 @@ function mergeCrossRepoResults(mainResult, ctx) {
1434
1458
  }
1435
1459
  if (crossEntries.length === 0) return mainResult
1436
1460
 
1461
+ // 主仓 skip 短路档(task-05 / FR-07 / design §5):主仓 skipped(test_strategy: skip /
1462
+ // evidence-auto 推荐跳过 / module 0 命中)时,未自配 commands.test 的跨仓不再 fallback
1463
+ // `npm test`(无 test script 的仓必 exit 1 → npm missing-script 假败打回整批)——随主仓
1464
+ // skip 短路;自配了 commands.test 的跨仓仍跑(用户显式意图,主仓 skip 不连带豁免);own
1465
+ // local.yaml 配 test_strategy: skip 的跨仓单独 skip(逐仓生效)。主仓 passed/failed 零变化。
1466
+ const mainSkipped = !!(mainResult && mainResult.status === 'skipped')
1437
1467
  const crossResults = []
1438
1468
  for (const entry of crossEntries) {
1439
- const crossResult = runCrossRepoFullTest(entry)
1469
+ const crossResult = mainSkipped ? runCrossRepoTestUnderMainSkip(entry) : runCrossRepoFullTest(entry)
1440
1470
  crossResults.push({ repoKey: entry.repoKey, projectRoot: entry.projectRoot, result: crossResult })
1441
1471
  }
1442
1472
 
@@ -1454,10 +1484,66 @@ function mergeCrossRepoResults(mainResult, ctx) {
1454
1484
  return mergeResultInfo(mainResult, crossResults)
1455
1485
  }
1456
1486
 
1487
+ /**
1488
+ * 主仓 skip 短路档下跑单个跨仓仓测试(task-05 / FR-07,逐仓三态判定):
1489
+ * - own local.yaml(<projectRoot>/.sillyspec/local.yaml)配 test_strategy: skip → 该仓
1490
+ * 单独 skip(逐仓生效,独立于主仓策略)
1491
+ * - own local.yaml 配 commands.test → 仍走 runCrossRepoFullTest 执行(用户显式意图,
1492
+ * 主仓 skip 不连带豁免实测)
1493
+ * - 未自配 commands.test(无 local.yaml / 读取失败 / 无 commands.test)→ 随主仓 skip
1494
+ * 短路,不跑 fallback `npm test`(无 test script 的仓必 exit 1 → npm missing-script
1495
+ * 假败,与坑 cross-repo-no-test-script 同族的环境性假红——但那是主仓非 skip 时的豁免
1496
+ * 通道,语义不动)。出路注记:配 own local.yaml 的 commands.test 即可纳入实测。
1497
+ *
1498
+ * 合并语义(上游 mergeCrossRepoResults):跨仓全 skipped → 整体保持主仓 skipped(reason
1499
+ * 带短路与出路注记);自配 commands.test 的跨仓 failed → 整体 fail(真败非假败,合理打回)。
1500
+ *
1501
+ * @param {object} entry - RepoEntry(isMain=false)
1502
+ * @returns {object} 结果 shape 对齐 runCrossRepoFullTest 返回
1503
+ */
1504
+ function runCrossRepoTestUnderMainSkip(entry) {
1505
+ const crossLocalYaml = join(entry.projectRoot, '.sillyspec', 'local.yaml')
1506
+ let yamlText = null
1507
+ if (existsSync(crossLocalYaml)) {
1508
+ try { yamlText = readFileSync(crossLocalYaml, 'utf8') } catch { yamlText = null }
1509
+ }
1510
+ if (yamlText) {
1511
+ // own test_strategy: skip → 该仓单独 skip(显式声明优先于 commands.test)
1512
+ if (extractTestStrategy(yamlText) === 'skip') {
1513
+ return {
1514
+ status: 'skipped',
1515
+ command: null,
1516
+ exitCode: null,
1517
+ durationMs: null,
1518
+ outputTail: null,
1519
+ reason: `跨仓 repo "${entry.repoKey}" own local.yaml test_strategy: skip,该仓单独跳过测试(逐仓生效)`,
1520
+ resultPath: null,
1521
+ mode: 'cross-repo-skip-strategy',
1522
+ repoKey: entry.repoKey,
1523
+ }
1524
+ }
1525
+ // 自配 commands.test → 仍执行(原路径原语义)
1526
+ if (extractTestCommand(yamlText)) return runCrossRepoFullTest(entry)
1527
+ }
1528
+ // 未自配 → 随主仓 skip 短路(不再 fallback npm test 假败)
1529
+ console.warn(`⚠️ 主仓 test_strategy skip 短路档:跨仓 repo "${entry.repoKey}"(${entry.projectRoot})未自配 commands.test,不跑 fallback npm test(假败消除)。如需纳入实测:在 ${crossLocalYaml} 配 commands.test。`)
1530
+ return {
1531
+ status: 'skipped',
1532
+ command: null,
1533
+ exitCode: null,
1534
+ durationMs: null,
1535
+ outputTail: null,
1536
+ reason: `主仓 skip 短路:跨仓 repo "${entry.repoKey}" 未自配 commands.test,随主仓跳过(可配 own local.yaml commands.test 纳入实测)`,
1537
+ resultPath: null,
1538
+ mode: 'cross-repo-main-skip',
1539
+ repoKey: entry.repoKey,
1540
+ }
1541
+ }
1542
+
1457
1543
  /**
1458
1544
  * 在单个跨仓仓根跑 full npm test(跨仓仓不参与 module 子集,只跑 full)。
1459
1545
  * 跨仓仓 own local.yaml 若存在且配 commands.test → 用之;否则 fallback `npm test`。
1460
- * 跨仓仓无 package.json → 跳过 + warn。
1546
+ * 跨仓仓无 package.json,或有 package.json 但无 test 脚本(未被 own local.yaml 覆盖)→ 跳过 + warn。
1461
1547
  *
1462
1548
  * @param {object} entry - RepoEntry(isMain=false)
1463
1549
  * @returns {object} 结果 shape 对齐 runFullCommand 返回
@@ -1494,6 +1580,35 @@ function runCrossRepoFullTest(entry) {
1494
1580
  } catch { /* 读取失败 fallback npm test */ }
1495
1581
  }
1496
1582
 
1583
+ // 跨仓仓有 package.json 但无 test 脚本,且未被 own local.yaml 覆盖 → 降级跳过 + warn
1584
+ // (坑 cross-repo-no-test-script,2026-09-15 wp EHS 会话实证:spdemo 无 test script,
1585
+ // fallback `npm test` 必 exit 1(npm missing-script)→ verify-test 环境性假红,agent 被迫
1586
+ // 手工给该仓造 .sillyspec/local.yaml + 冒烟脚本。测试基建缺失是仓常态,非代码回归——
1587
+ // 与「无 package.json」同款 skip 处理,hint 指向 own local.yaml 配置通道)。
1588
+ // own local.yaml 显式配了命令 → 无条件执行(用户显式意图,即使无 test script 也跑)。
1589
+ // package.json 解析失败 → 按原行为跑 `npm test`(fail-visible,不静默吞)。
1590
+ if (command === 'npm test') {
1591
+ let hasTestScript = true
1592
+ try {
1593
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'))
1594
+ hasTestScript = !!(pkg && pkg.scripts && Object.prototype.hasOwnProperty.call(pkg.scripts, 'test'))
1595
+ } catch { /* 解析失败维持 npm test 原行为 */ }
1596
+ if (!hasTestScript) {
1597
+ console.warn(`⚠️ 跨仓 repo "${entry.repoKey}"(${projectRoot})package.json 无 test 脚本,跳过该仓测试(非代码回归,不阻断 verify)。如需纳入实测:在 ${join(projectRoot, '.sillyspec', 'local.yaml')} 配 commands.test(如轻量冒烟检查)。`)
1598
+ return {
1599
+ status: 'skipped',
1600
+ command: null,
1601
+ exitCode: null,
1602
+ durationMs: null,
1603
+ outputTail: null,
1604
+ reason: `跨仓 repo "${entry.repoKey}" package.json 无 test 脚本,跳过测试(可配 own local.yaml commands.test 纳入)`,
1605
+ resultPath: null,
1606
+ mode: 'cross-repo-no-test-script',
1607
+ repoKey: entry.repoKey,
1608
+ }
1609
+ }
1610
+ }
1611
+
1497
1612
  const startedAt = Date.now()
1498
1613
  let exitCode = 0
1499
1614
  let output = ''
@@ -1910,7 +2025,7 @@ export function printVerifyParityCheck(result) {
1910
2025
  console.warn(' ⚠️ 本次 scope 为 full-repo(diff 与 apply-pathspec 均不可得时的兜底)——missing 大概率是口径错配噪音而非真实 contract gap。')
1911
2026
  console.warn(' 先收窄复跑:sillyspec verify-probes --change <变更名>(change-diff/apply-pathspec 口径)再判定。')
1912
2027
  }
1913
- console.warn(' 提示:检查是否后端漏实现,或前端调用了尚未实现的端点。确认无误可在 design.md 标注豁免。')
2028
+ console.warn(' 提示:检查是否后端漏实现,或前端调用了尚未实现的端点。确认无误可在 design.md 标注豁免。') /* probe1-noqa */
1914
2029
  }
1915
2030
 
1916
2031
  /**
@@ -2088,6 +2203,34 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
2088
2203
  * @param {{cwd:string, specBase:string, changeName?:string}} args
2089
2204
  * @returns {{status:'skipped'|'passed'|'warning', items:Array, unacknowledged:Array, summary:string, reason:string|null}}
2090
2205
  */
2206
+ /**
2207
+ * evidence 核验双根解析(坑⑤ / D-005@v1 消费侧双根 + 坑 platform-dual-root-fourth-consumer
2208
+ * 2026-09-16 hunt):apply 前新文件只在 worktree,逐文件核验单查主仓必误报「文件不存在」。
2209
+ * meta 双候选定位(平台模式 specBase=specRoot 时主仓 worktree meta 在项目侧 <cwd>/.sillyspec,
2210
+ * 单查 specBase 必落空——对齐 contract-matrix._readWorktreeMeta 双候选样板);meta 在场 &&
2211
+ * 非 in-place-fallback && worktreePath 目录存在 → [cwd, worktreePath],否则 [cwd]。
2212
+ * 解析异常 fail-open 退单根 + warning(不新增阻断路径)。导出供统一双根夹具测试。
2213
+ */
2214
+ export function resolveEvidenceDualRoots({ cwd, specBase, changeName, warnings = [] }) {
2215
+ let roots = [cwd]
2216
+ try {
2217
+ const metaCandidates = [...new Set([specBase, join(cwd, '.sillyspec')].filter(Boolean))]
2218
+ const metaPath = metaCandidates
2219
+ .map(b => join(b, '.runtime', 'worktrees', changeName, 'meta.json'))
2220
+ .find(p => existsSync(p))
2221
+ if (metaPath) {
2222
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
2223
+ if (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
2224
+ roots = [cwd, meta.worktreePath]
2225
+ }
2226
+ }
2227
+ } catch (e) {
2228
+ roots = [cwd]
2229
+ warnings.push(`worktree meta 解析失败(${e.message}),evidence 双根核验退主仓单根`)
2230
+ }
2231
+ return roots
2232
+ }
2233
+
2091
2234
  export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null, verifyStartAt = null }) {
2092
2235
  if (!changeName) {
2093
2236
  return { status: 'skipped', items: [], unacknowledged: [], summary: '', reason: '无 changeName(quick 等无关联变更场景),evidence 对账跳过' }
@@ -2179,19 +2322,7 @@ function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, v
2179
2322
  // && worktreePath 目录存在 → roots = [cwd, worktreePath];任一不满足退 [cwd] 单根
2180
2323
  // (零回归)。解析异常 fail-open 退单根 + warning(不新增阻断路径)。静默双根——对齐
2181
2324
  // buildAcceptanceHints(verify-probes.js:223)双根先例,核验明细 root 字段已可辨。──
2182
- let roots = [cwd]
2183
- try {
2184
- const metaPath = join(specBase || join(cwd, '.sillyspec'), '.runtime', 'worktrees', changeName, 'meta.json')
2185
- if (existsSync(metaPath)) {
2186
- const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
2187
- if (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
2188
- roots = [cwd, meta.worktreePath]
2189
- }
2190
- }
2191
- } catch (e) {
2192
- roots = [cwd]
2193
- warnings.push(`worktree meta 解析失败(${e.message}),evidence 双根核验退主仓单根`)
2194
- }
2325
+ let roots = resolveEvidenceDualRoots({ cwd, specBase, changeName, warnings })
2195
2326
 
2196
2327
  const detailed = []
2197
2328
  let blockedCount = 0
@@ -2750,6 +2881,21 @@ export const PROBE5_MISSING_ROW_RE = /^\| ❌ missing \|/
2750
2881
  * unavailable 行 / 无删除行 / ℹ️ note 行天然区分——后三者均无「反引号路径+状态尾缀」形态) */
2751
2882
  export const PROBE6_DELETION_LINE_RE = /^- (?:✅ 合规|❌ 高风险|⚠️ 未声明删除)[^`]* `[^`]+`(git 状态/
2752
2883
 
2884
+ /** probe8 契约外载荷键汇总行:`- ⚠️ 契约外载荷键 N 条(design 契约面未见,…):…`
2885
+ * (renderProbe8Lines,verify-probes.js;N 条计数段与段内其它 ⚠️ 行(疑似字段错位配对/
2886
+ * NOT NULL 列前端未见)区分——契约行是汇总行非逐条行,锚点取行内 N 而非行计数) */
2887
+ export const PROBE8_CONTRACT_ORPHANS_LINE_RE = /^- ⚠️ 契约外载荷键 (\d+) 条/
2888
+
2889
+ /** probe8 契约必填漏发汇总行:`- ⚠️ 契约必填漏发 N 条(契约 required 前端载荷未见,…):…`
2890
+ * (同上汇总行口径,行内 N 为锚) */
2891
+ export const PROBE8_MISSING_REQUIRED_LINE_RE = /^- ⚠️ 契约必填漏发 (\d+) 条/
2892
+
2893
+ /** probe9 守卫不一致汇总行:`- ⚠️ 守卫不一致实体组 N 个(同实体有守卫/无守卫并存——…)`
2894
+ * (renderProbe9Lines,verify-probes.js;同 probe8 契约行汇总行口径——锚点取行内 N 而非
2895
+ * 逐条实体组行计数;`实体组 N 个` 计数段与段内逐条行 `- ⚠️ 实体 X:有守卫 […] / 无守卫 […]`
2896
+ * 及其它 ⚠️ 行(NOT NULL 列等)形态区分) */
2897
+ export const PROBE9_INCONSISTENT_LINE_RE = /^- ⚠️ 守卫不一致实体组 (\d+) 个/
2898
+
2753
2899
  /**
2754
2900
  * `#### 探针 N` 子节定界(G8/R-05):子节 = 标题行起、下一任意 markdown 标题(下一探针
2755
2901
  * 子节 / `##` 章 / `#` 题)前止。探针 2/4 的 agent 补写内容天然落在本子节定界之外,其
@@ -2780,27 +2926,40 @@ function extractProbeSubsections(text) {
2780
2926
  * 导出供渲染→解析 round-trip 测试直接消费(R-01)。
2781
2927
  * @param {string} text verify-result.md 全文
2782
2928
  * @returns {{
2783
- * subsections: { probe1: boolean, probe3: boolean, probe5: boolean, probe6: boolean, any: boolean },
2929
+ * subsections: { probe1: boolean, probe3: boolean, probe5: boolean, probe6: boolean, probe8: boolean, probe9: boolean, any: boolean },
2784
2930
  * probe1Hits: number, // probe1 命中行计数(对比 probe1.matches.length)
2785
2931
  * probe3HasTest: number, // probe3 hasTest 行计数(对比 tasks.filter(hasTest).length)
2786
2932
  * probe5SummaryPresent: boolean, // probe5 summary 锚行存在性(pass 形态锚)
2787
2933
  * probe5Missing: number, // `| ❌ missing |` 行计数(fail 形态锚,对比 missingBackend.length)
2788
2934
  * probe6Deletions: number, // probe6 删除条目行计数(对比 deletions.length)
2935
+ * probe8ContractOrphans: number, // probe8 契约外载荷键汇总行内 N 求和(无行=0;对比 contractOrphans.length)
2936
+ * probe8MissingRequired: number, // probe8 契约必填漏发汇总行内 N 求和(无行=0;对比 missingRequired.length)
2937
+ * probe9InconsistentGroups: number, // probe9 守卫不一致汇总行内 N 求和(无行=0;对比 inconsistentGroups.length)
2789
2938
  * }}
2790
2939
  */
2791
2940
  export function parseProbePrefillAnchors(text) {
2792
2941
  const sections = extractProbeSubsections(text)
2793
2942
  const count = (lines, re) => lines.reduce((n, l) => n + (re.test(l) ? 1 : 0), 0)
2943
+ // 汇总行锚点(probe8 契约行/probe9 守卫行是「一行含总数」非逐条行):提取行内 N 求和,无行 → 0
2944
+ // (渲染面恒只输出一行,重复撞形行累加——重复本身就会撞出 mismatch,同子节合并口径)
2945
+ const sumGroupCount = (lines, re) => lines.reduce((n, l) => {
2946
+ const m = l.match(re)
2947
+ return n + (m ? Number(m[1]) : 0)
2948
+ }, 0)
2794
2949
  const s1 = sections['1'] || []
2795
2950
  const s3 = sections['3'] || []
2796
2951
  const s5 = sections['5'] || []
2797
2952
  const s6 = sections['6'] || []
2953
+ const s8 = sections['8'] || []
2954
+ const s9 = sections['9'] || []
2798
2955
  return {
2799
2956
  subsections: {
2800
2957
  probe1: '1' in sections,
2801
2958
  probe3: '3' in sections,
2802
2959
  probe5: '5' in sections,
2803
2960
  probe6: '6' in sections,
2961
+ probe8: '8' in sections,
2962
+ probe9: '9' in sections,
2804
2963
  any: Object.keys(sections).length > 0,
2805
2964
  },
2806
2965
  probe1Hits: count(s1, PROBE1_HIT_LINE_RE),
@@ -2808,6 +2967,9 @@ export function parseProbePrefillAnchors(text) {
2808
2967
  probe5SummaryPresent: count(s5, PROBE5_SUMMARY_LINE_RE) > 0,
2809
2968
  probe5Missing: count(s5, PROBE5_MISSING_ROW_RE),
2810
2969
  probe6Deletions: count(s6, PROBE6_DELETION_LINE_RE),
2970
+ probe8ContractOrphans: sumGroupCount(s8, PROBE8_CONTRACT_ORPHANS_LINE_RE),
2971
+ probe8MissingRequired: sumGroupCount(s8, PROBE8_MISSING_REQUIRED_LINE_RE),
2972
+ probe9InconsistentGroups: sumGroupCount(s9, PROBE9_INCONSISTENT_LINE_RE),
2811
2973
  }
2812
2974
  }
2813
2975
 
@@ -2856,7 +3018,13 @@ function detectHeadAdvanceSinceFacts(cwd, facts) {
2856
3018
  * - HEAD 前进子案(R-06):facts.generatedAt 之后有新 commit 时 probe6 的 `git diff HEAD`
2857
3019
  * 口径整体漂移 → 该 mismatch 降 WARNING 提示重跑 --init;
2858
3020
  * - probe3 hasTest 数 / probe5 锚(summary 行存在性 + missing 行数)不符 = WARNING
2859
- * (测试文件布局与 parity 扫描根环境敏感)。
3021
+ * (测试文件布局与 parity 扫描根环境敏感);
3022
+ * - probe8 契约锚(契约外载荷键/契约必填漏发汇总行内 N)不符 = WARNING(2026-09-16
3023
+ * task-02:契约面解析随 design 形态与清单文件读取环境敏感;重跑降级(fail-soft catch
3024
+ * 兜底无契约键)时跳过该维度不误报);
3025
+ * - probe9 守卫锚(守卫不一致实体组汇总行内 N)不符 = WARNING(2026-09-16
3026
+ * guard-consistency-probe task-02:聚类/信号比对随 design 清单与 Java 文件读取环境
3027
+ * 敏感;重跑降级时跳过该维度不误报)。
2860
3028
  *
2861
3029
  * 接线约定(task-03,gates.js):status='mismatch' 且 severity='error' → 阻断回滚;
2862
3030
  * 'mismatch'+'warning' → 放行告警(envelope probe_consistency_drift);'skipped'/'degraded' →
@@ -2877,7 +3045,7 @@ function detectHeadAdvanceSinceFacts(cwd, facts) {
2877
3045
  * mismatches: Array<{probe: string, expected: *, actual: *, severity: 'error'|'warning', note: string}>,
2878
3046
  * // expected = 当前重跑指标(应然),actual = 正文锚点解析值(agent 可篡改侧)
2879
3047
  * skipReason: string|null,
2880
- * subsections: {probe1: boolean, probe3: boolean, probe5: boolean, probe6: boolean, any: boolean}|null,
3048
+ * subsections: {probe1: boolean, probe3: boolean, probe5: boolean, probe6: boolean, probe8: boolean, probe9: boolean, any: boolean}|null,
2881
3049
  * // 诊断(additive):探针子节在场性;报告未读到的早期 skip 为 null
2882
3050
  * }}
2883
3051
  */
@@ -2894,6 +3062,69 @@ export function isStrictChange({ pm, cwd, changeName } = {}) {
2894
3062
  return createdAt >= IR_STRICT_SINCE
2895
3063
  }
2896
3064
 
3065
+ /**
3066
+ * smoke 回执槽机器段一致性对比(2026-09-17-api-coverage-smoke task-02 / FR-03 / Grill #6 /
3067
+ * R-06):机器段唯一事实源 = .runtime/verify-quality-scan-<change>.json 的 smokeResult 段
3068
+ * (verify-probes ensure 注入的同源记录),md 侧 agent 只可追加段、不可改写/删除机器段。
3069
+ * 时序保障:backfillFactsFromMdAndTests 的 ensure 注入(gates verify 收尾前置 + 二次回填)
3070
+ * 先于本对比(checkProbeConsistency 在 gates 后段),此刻仍不符 = 机器段被改写/删除/冒充。
3071
+ * 对比口径(全 ERROR——数值面防篡改,非环境敏感维度):
3072
+ * - 记录 passed:md 须有标记回执条目(无 = 被删除);每条 command/exit/log 与记录逐字段比对
3073
+ * (trim 后严格相等——ensure 注入原样、解析侧 trim,正常链路恒一致),任一不符 = 被改写;
3074
+ * - 记录 not-configured / not-ran:md 出现标记回执条目 = 冒充(未配置/未绿跑却有绿回执行);
3075
+ * 缺态注释行在场但态与记录不符 = 被改写;
3076
+ * - 无记录:md 出现任何 CLI 标记行(回执条目或缺态注释)= 冒充——标记行唯一合法来源是
3077
+ * ensure,而 ensure 只在有记录时注入。
3078
+ * export 说明(2026-09-17-api-coverage-smoke task-07):供 test/smoke-gate.test.mjs
3079
+ * 直接断言(task-02 偏离①挂账的接线正主——checkProbeConsistency 内部消费不变)。
3080
+ * @param {string} reportText verify-result.md 全文
3081
+ * @param {string} specBase .sillyspec 根
3082
+ * @param {string} changeName 变更名
3083
+ * @returns {{ state: string|null, absentState: string|null, machineCount: number, mismatches: Array<{probe:string,expected:*,actual:*,severity:'error',note:string}> }}
3084
+ */
3085
+ export function auditSmokeReceiptConsistency(reportText, specBase, changeName) {
3086
+ const smoke = readSmokeResultRecord(specBase, changeName)
3087
+ const state = deriveSmokeSectionState(smoke)
3088
+ const scan = scanSmokeReceiptSection(reportText)
3089
+ const mismatches = []
3090
+ const push = (expected, actual, note) =>
3091
+ mismatches.push({ probe: 'smoke-receipt', expected, actual, severity: 'error', note })
3092
+ const fixHint = '机器段唯一事实源是 .runtime/verify-quality-scan 记录 json 的 smokeResult(R-06),禁止手改——记录已过期则重跑 verify 质量扫描步(sillyspec run verify),CLI 会重新 ensure 机器段'
3093
+ if (!state) {
3094
+ if (scan.machineEntries.length > 0) {
3095
+ push('无 smokeResult 记录(无 CLI 标记行)', `md 回执槽 ${scan.machineEntries.length} 条 source=cli-noai-smoke 标记条目`, `无 quality-scan smokeResult 记录而回执槽出现 CLI 机器段标记行——标记行唯一合法来源是 CLI ensure 注入(ensure 只在有记录时注入),疑似 agent 手写冒充。${fixHint}`)
3096
+ } else if (scan.absentState) {
3097
+ push('无 smokeResult 记录(无 CLI 标记行)', `缺态注释行 ${scan.absentState}`, `无 quality-scan smokeResult 记录而回执槽出现 CLI 缺态标注行——疑似 agent 手写冒充。${fixHint}`)
3098
+ }
3099
+ return { state, absentState: scan.absentState, machineCount: scan.machineEntries.length, mismatches }
3100
+ }
3101
+ if (state === 'passed') {
3102
+ if (scan.machineEntries.length === 0) {
3103
+ push('passed 机器段在场(command/exit/log 实录)', '回执槽无 source=cli-noai-smoke 标记条目', `quality-scan 记录 smokeResult.status='passed' 而回执槽机器段缺失/被删除(ensure 已在 backfill 时点先行注入,此刻仍缺 = 正文被改)。${fixHint}`)
3104
+ }
3105
+ for (const [i, e] of scan.machineEntries.entries()) {
3106
+ const cmdOk = String((smoke && smoke.command) || '').trim() === String(e.command || '').trim()
3107
+ const exitOk = (typeof (smoke && smoke.exitCode) === 'number' ? smoke.exitCode : null)
3108
+ === (typeof e.exitCode === 'number' ? e.exitCode : null)
3109
+ const logOk = String((smoke && smoke.logPath) || '').trim() === String(e.logPath || '').trim()
3110
+ if (!cmdOk || !exitOk || !logOk) {
3111
+ push(
3112
+ `command=${smoke.command} / exit=${smoke.exitCode} / log=${smoke.logPath}`,
3113
+ `第 ${i + 1} 条标记条目 command=${e.command} / exit=${e.exitCode} / log=${e.logPath}`,
3114
+ `机器段数值与 quality-scan 记录 smokeResult 不符(command/exit/log 任一字段被改写即打回;多出的重复标记条目按同口径逐条比对)。${fixHint}`)
3115
+ }
3116
+ }
3117
+ } else {
3118
+ if (scan.machineEntries.length > 0) {
3119
+ push(`缺态标注 ${state}(无绿跑机器段)`, `${scan.machineEntries.length} 条 source=cli-noai-smoke 标记回执条目`, `quality-scan 记录为 ${state}(未配置/未绿跑)而回执槽出现 CLI 机器段标记回执行——疑似 agent 手写冒充绿回执(ensure 已按记录态对齐,此刻仍在 = 正文被改)。${fixHint}`)
3120
+ }
3121
+ if (scan.absentState && scan.absentState !== state) {
3122
+ push(`缺态标注 ${state}`, `缺态注释行 ${scan.absentState}`, `缺态注释行态与 quality-scan 记录不符——疑似被改写。${fixHint}`)
3123
+ }
3124
+ }
3125
+ return { state, absentState: scan.absentState, machineCount: scan.machineEntries.length, mismatches }
3126
+ }
3127
+
2897
3128
  export function checkProbeConsistency({ cwd, specBase = null, changeName = null, runtimeRoot = null, strictMode = false }) {
2898
3129
  if (!changeName) {
2899
3130
  return { status: 'skipped', severity: null, mismatches: [],
@@ -2993,6 +3224,46 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
2993
3224
  note: 'contract gap(missing backend)行数与重跑不符(探针5 扫描根/缓存口径环境敏感,WARNING 不阻断)' })
2994
3225
  }
2995
3226
 
3227
+ // probe8 契约锚(WARNING——2026-09-16 task-02:契约外载荷键/契约必填漏发汇总行内 N 对比
3228
+ // 重跑数组长度;契约面解析随 design 形态与清单文件读取环境敏感,不阻断。fail-soft:重跑
3229
+ // 降级(runVerifyProbes 的 catch 兜底对象无契约键)时 curP8*=null 跳过对比不误报)
3230
+ const curProbe8 = current.probe8 || {}
3231
+ const curP8Orphans = Array.isArray(curProbe8.contractOrphans) ? curProbe8.contractOrphans.length : null
3232
+ const curP8MissingRequired = Array.isArray(curProbe8.missingRequired) ? curProbe8.missingRequired.length : null
3233
+ if (curP8Orphans !== null && anchors.probe8ContractOrphans !== curP8Orphans) {
3234
+ mismatches.push({ probe: 'probe8', expected: curP8Orphans, actual: anchors.probe8ContractOrphans, severity: 'warning',
3235
+ note: '契约外载荷键条数与重跑不符(探针8 契约面解析对 design/清单文件形态环境敏感,WARNING 不阻断)——建议重跑 sillyspec verify-probes --change <变更名> --init 刷新预填段' })
3236
+ }
3237
+ if (curP8MissingRequired !== null && anchors.probe8MissingRequired !== curP8MissingRequired) {
3238
+ mismatches.push({ probe: 'probe8', expected: curP8MissingRequired, actual: anchors.probe8MissingRequired, severity: 'warning',
3239
+ note: '契约必填漏发条数与重跑不符(探针8 契约面解析对 design/清单文件形态环境敏感,WARNING 不阻断)——建议重跑 sillyspec verify-probes --change <变更名> --init 刷新预填段' })
3240
+ }
3241
+
3242
+ // probe9 守卫锚(WARNING——2026-09-16 guard-consistency-probe task-02:守卫不一致实体组
3243
+ // 汇总行内 N 对比重跑数组长度;聚类启发式与信号比对随 design 清单及 Java 文件读取环境
3244
+ // 敏感,不阻断。fail-soft:重跑降级(runVerifyProbes 的 catch 兜底)时 probe9 兜底对象与
3245
+ // probe8 不同——inconsistentGroups 恒为数组([]),Array.isArray 判别失效,改以「探针 9
3246
+ // 执行失败」注记判降级跳过对比,不误报)
3247
+ const curProbe9 = current.probe9 || {}
3248
+ const curP9Degraded = (curProbe9.notes || []).some(n => typeof n === 'string' && n.includes('探针 9 执行失败'))
3249
+ const curP9Inconsistent = Array.isArray(curProbe9.inconsistentGroups) && !curP9Degraded
3250
+ ? curProbe9.inconsistentGroups.length : null
3251
+ const curP9GroupCount = typeof curProbe9.groupCount === 'number' && !curP9Degraded
3252
+ ? curProbe9.groupCount : null
3253
+ if (curP9Inconsistent !== null && anchors.probe9InconsistentGroups !== curP9Inconsistent) {
3254
+ mismatches.push({ probe: 'probe9', expected: curP9Inconsistent, actual: anchors.probe9InconsistentGroups, severity: 'warning',
3255
+ note: '守卫不一致实体组数与重跑不符(探针9 聚类/信号比对对 design 清单与 Java 文件形态环境敏感,WARNING 不阻断)——建议重跑 sillyspec verify-probes --change <变更名> --init 刷新预填段' })
3256
+ }
3257
+
3258
+ // —— smoke 回执槽机器段一致性(ERROR——2026-09-17-api-coverage-smoke task-02 / FR-03 /
3259
+ // Grill #6 / R-06):现 parseProbePrefillAnchors 只对比探针子节计数、不覆盖回执槽,
3260
+ // 本维度补回执槽机器段防篡改——md 标记条目(source=cli-noai-smoke)command/exit/log
3261
+ // 对 quality-scan 记录 json smokeResult 逐字段比对,agent 改写/删除/冒充 → ERROR 打回
3262
+ // (时序:backfill ensure 已先行注入/态对齐)。解析与记录读取单点在 verify-probes
3263
+ // (auditSmokeReceiptConsistency),与 ensure 注入同源不二算。——
3264
+ const smokeAudit = auditSmokeReceiptConsistency(reportText, sb, changeName)
3265
+ for (const sm of smokeAudit.mismatches) mismatches.push(sm)
3266
+
2996
3267
  // —— facts 基线对比(2026-09-08-ir-verify-facts FR-05 / task-05):重跑指标 vs
2997
3268
  // verify-facts.json probes 快照(init 时点)。md 锚点对账查「正文没被改」,本维度查
2998
3269
  // 「md 被手改对齐新代码后 facts 底稿过期」(P3d 数据源保鲜)。分级沿用现实现口径:
@@ -3025,8 +3296,45 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
3025
3296
  fm.push({ probe: 'probe5', snapshot: snap5, rerun: cur5, severity: 'warning',
3026
3297
  note: 'facts 快照 backend 端点数与重跑不符(扫描根口径环境敏感,WARNING)' })
3027
3298
  }
3299
+ // probe8 契约指标(2026-09-16 task-02,WARNING 同 md 锚点维度口径;metrics fail-soft
3300
+ // 少列键(旧 facts 无 probe8)或重跑降级时不对账不列 checked,不误报)
3301
+ const snap8Metrics = (((factsSnapshot.probes.probe8 || {}).metrics) || {})
3302
+ const p8MetricsAvailable = typeof snap8Metrics.contractOrphans === 'number' || typeof snap8Metrics.missingRequired === 'number'
3303
+ const p8RerunAvailable = curP8Orphans !== null || curP8MissingRequired !== null
3304
+ if (typeof snap8Metrics.contractOrphans === 'number' && curP8Orphans !== null
3305
+ && snap8Metrics.contractOrphans !== curP8Orphans) {
3306
+ fm.push({ probe: 'probe8', snapshot: snap8Metrics.contractOrphans, rerun: curP8Orphans, severity: 'warning',
3307
+ note: 'facts 快照契约外载荷键数与重跑不符(契约面解析环境敏感,WARNING)——建议 verify-probes --init 刷新 facts + 同步 md 探针段' })
3308
+ }
3309
+ if (typeof snap8Metrics.missingRequired === 'number' && curP8MissingRequired !== null
3310
+ && snap8Metrics.missingRequired !== curP8MissingRequired) {
3311
+ fm.push({ probe: 'probe8', snapshot: snap8Metrics.missingRequired, rerun: curP8MissingRequired, severity: 'warning',
3312
+ note: 'facts 快照契约必填漏发数与重跑不符(契约面解析环境敏感,WARNING)——建议 verify-probes --init 刷新 facts + 同步 md 探针段' })
3313
+ }
3314
+ // probe9 守卫指标(2026-09-16 guard-consistency-probe task-02,WARNING 同 md 锚点维度口径;
3315
+ // 双指标 = inconsistentGroups(命中面,同 md 锚点维度)+ groupCount(聚类组面,design 清单
3316
+ // Java 文件内容变更的漂移信号)。javaFileCount 不入对账——文件读取受 worktree 存活态等
3317
+ // 环境维度影响(R-02 同款排除口径,probe1 排除 worktreeHits 先例);metrics fail-soft 少列
3318
+ // 键(旧 facts 无 probe9)或重跑降级(curP9*=null)时不对账不列 checked,不误报)
3319
+ const snap9Metrics = (((factsSnapshot.probes.probe9 || {}).metrics) || {})
3320
+ const p9MetricsAvailable = typeof snap9Metrics.inconsistentGroups === 'number' || typeof snap9Metrics.groupCount === 'number'
3321
+ const p9RerunAvailable = curP9Inconsistent !== null || curP9GroupCount !== null
3322
+ if (typeof snap9Metrics.inconsistentGroups === 'number' && curP9Inconsistent !== null
3323
+ && snap9Metrics.inconsistentGroups !== curP9Inconsistent) {
3324
+ fm.push({ probe: 'probe9', snapshot: snap9Metrics.inconsistentGroups, rerun: curP9Inconsistent, severity: 'warning',
3325
+ note: 'facts 快照守卫不一致组数与重跑不符(聚类/信号比对环境敏感,WARNING)——建议 verify-probes --init 刷新 facts + 同步 md 探针段' })
3326
+ }
3327
+ if (typeof snap9Metrics.groupCount === 'number' && curP9GroupCount !== null
3328
+ && snap9Metrics.groupCount !== curP9GroupCount) {
3329
+ fm.push({ probe: 'probe9', snapshot: snap9Metrics.groupCount, rerun: curP9GroupCount, severity: 'warning',
3330
+ note: 'facts 快照同实体变更方法组数与重跑不符(聚类面随 design 清单 Java 文件内容漂移,WARNING)——建议 verify-probes --init 刷新 facts + 同步 md 探针段' })
3331
+ }
3028
3332
  factsConsistency = {
3029
- checked: fm.length === 0 ? ['probe1', 'probe3', 'probe5', 'probe6'] : [...new Set(fm.map(x => x.probe))],
3333
+ checked: fm.length === 0
3334
+ ? ['probe1', 'probe3', 'probe5', 'probe6',
3335
+ ...((p8MetricsAvailable && p8RerunAvailable) ? ['probe8'] : []),
3336
+ ...((p9MetricsAvailable && p9RerunAvailable) ? ['probe9'] : [])]
3337
+ : [...new Set(fm.map(x => x.probe))],
3030
3338
  verdict: fm.length === 0 ? 'match' : 'mismatch',
3031
3339
  detail: fm.length === 0 ? null : fm,
3032
3340
  }
@@ -3042,13 +3350,23 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
3042
3350
  } catch { /* 固化失败不阻断对账 */ }
3043
3351
  }
3044
3352
 
3353
+ // smoke 机器段诊断(additive,factsConsistency 同款先例:状态/条目计数随结果透传,供
3354
+ // gates 侧 envelope 落盘与排查;不参与判定——判定由上方 mismatches 收集完成)
3355
+ const smokeConsistency = {
3356
+ state: smokeAudit.state, absentState: smokeAudit.absentState,
3357
+ machineCount: smokeAudit.machineCount,
3358
+ verdict: smokeAudit.mismatches.length === 0 ? 'match' : 'mismatch',
3359
+ }
3360
+
3045
3361
  if (mismatches.length === 0) {
3046
3362
  const okResult = finish('ok', null, [], null)
3047
3363
  okResult.factsConsistency = factsConsistency
3364
+ okResult.smokeConsistency = smokeConsistency
3048
3365
  return okResult
3049
3366
  }
3050
3367
  const severity = mismatches.some(m => m.severity === 'error') ? 'error' : 'warning'
3051
3368
  const mismatchResult = finish('mismatch', severity, mismatches, null)
3052
3369
  mismatchResult.factsConsistency = factsConsistency
3370
+ mismatchResult.smokeConsistency = smokeConsistency
3053
3371
  return mismatchResult
3054
3372
  }