sillyspec 3.32.9 → 3.32.10

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 (45) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  2. package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
  5. package/AGENTS.md +1 -1
  6. package/assets/command-cards/flow.md +1 -1
  7. package/package.json +1 -1
  8. package/src/commit-suggest.js +45 -4
  9. package/src/config-schema.js +14 -0
  10. package/src/decision-distill.js +77 -21
  11. package/src/decision-renumber.js +157 -0
  12. package/src/docs-check.js +30 -15
  13. package/src/docs-debt.js +3 -2
  14. package/src/doctor-diagnostics.js +3 -1
  15. package/src/flow-parity.js +18 -2
  16. package/src/flow.js +75 -12
  17. package/src/fr-index.js +15 -1
  18. package/src/hooks/worktree-guard.js +3 -3
  19. package/src/index.js +7 -5
  20. package/src/knowledge-digest.js +23 -6
  21. package/src/knowledge-graph.js +92 -14
  22. package/src/knowledge-match.js +34 -9
  23. package/src/knowledge-stats.js +12 -13
  24. package/src/knowledge-vector.js +98 -7
  25. package/src/machine-interface.js +26 -0
  26. package/src/quicklog.js +6 -1
  27. package/src/run/archive-distill.js +3 -0
  28. package/src/run/command.js +3 -3
  29. package/src/run/complete-handlers.js +8 -3
  30. package/src/run/complete.js +113 -44
  31. package/src/run/gates.js +93 -89
  32. package/src/run/prompt.js +3 -3
  33. package/src/run/stage.js +1 -1
  34. package/src/sentinel-assertions.js +43 -1
  35. package/src/stage-contract.js +26 -3
  36. package/src/stage-review.js +44 -11
  37. package/src/stage-templates.js +2 -0
  38. package/src/stages/brainstorm.js +5 -1
  39. package/src/stages/knowledge.js +56 -5
  40. package/src/sync.js +28 -2
  41. package/src/task-tick.js +12 -0
  42. package/src/timeline.js +60 -19
  43. package/src/verify-postcheck.js +49 -1
  44. package/src/verify-probes.js +61 -18
  45. package/templates/agents-instruction.md +1 -1
@@ -493,25 +493,26 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
493
493
  .filter((h) => !_hits.some((x) => x.file === h.file && x.id === h.id && (x.change || '') === (h.change || '')))
494
494
  _hits.push(..._viaImpact)
495
495
  } catch { /* 图构建/键解析 fail-soft——词面命中面不受影响 */ }
496
- // 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
497
- // 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
496
+ // 已回应不重弹:decisions.md 某行同时含「命中 id+域文件名(或命中条目变更名)」(小白鼠
497
+ // 回应形态「unmapped.md D-001@v1」)→ 视为已甄别过,静默。行级共现(2026-10-14-decision-id-p2-hardening
498
+ // 收紧:文件级共现时本变更自身同号 id + 正文常见词 backend/unmapped 撞车会静默吞掉他变更命中)。
498
499
  const _answered = _dm
499
500
  ? (h) => {
500
501
  const _dom = String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, '')
501
- return _dm.includes(h.id) && _dm.includes(_dom)
502
+ return _dm.split(/\r?\n/).some((line) => line.includes(h.id) && (line.includes(_dom) || (h.change && line.includes(h.change))))
502
503
  }
503
504
  : () => false
504
505
  const _echoHits = _hits.filter((h) => !_answered(h))
505
506
  const _entries = km.matched ? (km.entries || []) : []
506
507
  if (_echoHits.length > 0 || _entries.length > 0) {
507
- const { deathPathNote } = await import('../knowledge-match.js')
508
+ const { deathPathNote, decisionChangeQualifier } = await import('../knowledge-match.js')
508
509
  console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
509
510
  for (const h of _echoHits.slice(0, 5)) {
510
511
  const _note = h.status === 'rejected'
511
512
  ? ` 否决理由:${h.reason || '(未记录)'}`
512
513
  : h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
513
514
  const _via = h.viaImpact ? ` 🧭impact 可达(键:${h.impactKey})` : ''
514
- console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}${_via}`)
515
+ console.warn(` - ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)})status=${h.status || '?'}${_note}${_via}`)
515
516
  }
516
517
  for (const e of _entries.slice(0, 3)) {
517
518
  const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
@@ -673,40 +674,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
673
674
  // 勾选回归 agent 手动(完成=实现+测试绿+wt-commit 即勾,同 thin 工作单元语义),假勾防线由
674
675
  // detectExecuteBatchFinish 内 checkExecuteCodeEvidence 代码证据核验 + verify 测试对账承担。
675
676
  if (stageName === 'execute' && changeName) {
676
- // 自动勾选(full-autopilot-parity,R21 thin 实证迁移):execute --done 时解析 run 内提交的
677
- // task-NN token,自动勾选 tasks.md 未勾条目——agent 手动勾选是 R19/R20 的行为惯性(tick-loop-nudge
678
- // 提示词无效),机器代勾与 thin 的 flow done 同逻辑。fail-soft。
679
- try {
680
- const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
681
- if (existsSync(_tasksMdPath)) {
682
- const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
683
- let _tickCommits = ''
684
- try {
685
- if (existsSync(_runIdFile)) {
686
- _tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', 'HEAD~20..HEAD']) || ''
687
- }
688
- } catch {}
689
- if (!_tickCommits) _tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', '-20']) || ''
690
- if (_tickCommits) {
691
- const _texts = String(_tickCommits).split('\x1e').map(x => x.trim()).filter(Boolean).join('\n')
692
- const _evidenced = new Set()
693
- for (const m of _texts.matchAll(/task-(\d{1,2})/gi)) _evidenced.add(String(Number(m[1])).padStart(2, '0'))
694
- if (_evidenced.size > 0) {
695
- let _md = readFileSync(_tasksMdPath, 'utf8')
696
- let _ticked = 0
697
- for (const nn of _evidenced) {
698
- const re = new RegExp(`^- \\[ \\](.*task-${nn}[:\\s])`, 'm')
699
- if (re.test(_md)) { _md = _md.replace(re, `- [x]$1`); _ticked++ }
700
- }
701
- if (_ticked > 0) {
702
- const { writeAtomicSync } = await import('../fs-atomic.js')
703
- writeAtomicSync(_tasksMdPath, _md)
704
- console.log(`✅ [自动勾选] ${_ticked} 个任务有提交证据但未勾——已机器代勾(full-autopilot-parity)`)
705
- }
706
- }
707
- }
708
- }
709
- } catch { /* auto-tick fail-soft */ }
677
+ // 自动勾选(full-autopilot-parity,R21 thin 实证迁移):代勾判据/实现见
678
+ // autoTickTasksFromCommitSubjects(subject token × allowed_paths 实现事实锚)。
679
+ await autoTickTasksFromCommitSubjects({ changeName, cwd, specBase, platformOpts })
710
680
  const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps })
711
681
  if (_bf.batched && _bf.aligned > 0) {
712
682
  console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
@@ -1532,8 +1502,10 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1532
1502
  if (isExplicitReviewWrite(r.review)) return true
1533
1503
 
1534
1504
  // W2 task-04 草稿零 diff 守卫(FR-03)
1535
- // ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
1536
- if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
1505
+ // review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空。ctx 缺席(直写/主仓形态
1506
+ // buildDraftContext 返 null)同样保守不勾——原「ctx=null 直接放行」使 cannot_verify 草稿
1507
+ // 无任何实现核验即被代勾(坑 wt-commit-auto-tick-unimplemented-tasks 连带,2026-10-15)。
1508
+ if (r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
1537
1509
  const taskId = r.review.task || 'task-NN'
1538
1510
  const unlockCmd = `sillyspec review write${changeName ? ` --change ${changeName}` : ' --change <变更名>'} --task ${taskId} --spec pass --quality pass --force --changed-files <文件>`
1539
1511
  const changedFiles = r.review.changedFiles || []
@@ -1541,8 +1513,8 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1541
1513
  console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选(task=${taskId})——解锁:${unlockCmd}(纯验证任务用 --changed-files "" + --evidence "<验证说明>")`)
1542
1514
  return false
1543
1515
  }
1544
- if (!ctx.gitDir || !ctx.base || !ctx.head) {
1545
- console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
1516
+ if (!ctx || !ctx.gitDir || !ctx.base || !ctx.head) {
1517
+ console.warn(`⚠️ ctx 缺席或不完整(gitDir=${ctx && ctx.gitDir}, base=${ctx && ctx.base}, head=${ctx && ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
1546
1518
  return false
1547
1519
  }
1548
1520
  // 预取路径:一次全量 diff 文件集内存归属(autoCheckPlanFromReviews/批量路径已 prefetch)
@@ -1572,6 +1544,86 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1572
1544
 
1573
1545
  return true
1574
1546
  }
1547
+ /**
1548
+ * execute --done 的 full-autopilot 代勾(独立函数供测试直驱;completeStep execute 分支调用)。
1549
+ *
1550
+ * 实现事实锚(坑 wt-commit-auto-tick-unimplemented-tasks,2026-10-15 消费仓两次实证:原按
1551
+ * %B 提交全文提 token——task-01 提交正文提及 task-02/03/04 即全被误勾,批量完成判定与续跑
1552
+ * 跳过双失真):证据源收敛为提交 subject 行(%s)的 task-NN token,且任务卡 allowed_paths
1553
+ * 在场时要求「提及该 task 的提交触及面 × allowed_paths」交叠(parseAllowedPaths +
1554
+ * pathMatches,与草稿归属同口径);无卡面/无 allowed_paths 退化为仅 subject token。无锚不勾。
1555
+ * fail-soft(异常零行为)。
1556
+ * @param {{ changeName: string, cwd: string, specBase: string, platformOpts?: object }} opts
1557
+ */
1558
+ export async function autoTickTasksFromCommitSubjects({ changeName, cwd, specBase, platformOpts = {} }) {
1559
+ try {
1560
+ const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
1561
+ if (!existsSync(_tasksMdPath)) return { ticked: 0, skippedNoAnchor: 0 }
1562
+ const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
1563
+ let _tickLog = ''
1564
+ try {
1565
+ if (existsSync(_runIdFile)) {
1566
+ _tickLog = gitQuiet(cwd, ['log', '--name-only', '--format=%x1e%s', 'HEAD~20..HEAD'], { trim: false }) || ''
1567
+ }
1568
+ } catch {}
1569
+ if (!_tickLog) _tickLog = gitQuiet(cwd, ['log', '--name-only', '--format=%x1e%s', '-20'], { trim: false }) || ''
1570
+ if (_tickLog) {
1571
+ // 逐提交解析:首行 = subject(token 证据面——正文散文提及不是实现证据),其余行 = 该提交触及文件
1572
+ const _evidenced = new Map()
1573
+ for (const _chunk of String(_tickLog).split('\x1e')) {
1574
+ const _lines = String(_chunk || '').split('\n').map(x => x.trim()).filter(Boolean)
1575
+ if (_lines.length === 0) continue
1576
+ for (const m of _lines[0].matchAll(/task-(\d{1,2})/gi)) {
1577
+ const nn = String(Number(m[1])).padStart(2, '0')
1578
+ if (!_evidenced.has(nn)) _evidenced.set(nn, new Set())
1579
+ for (const f of _lines.slice(1)) _evidenced.get(nn).add(f)
1580
+ }
1581
+ }
1582
+ if (_evidenced.size > 0) {
1583
+ // 任务卡 allowed_paths 索引(卡面读取失败 → 无卡面口径:仅 subject token)
1584
+ const _allowedByTask = new Map()
1585
+ let _pathMatches = null
1586
+ try {
1587
+ const { parseAllowedPaths } = await import('../stages/plan-postcheck.js')
1588
+ ;({ pathMatches: _pathMatches } = await import('../change-list.js'))
1589
+ const _tasksDir = join(specBase, 'changes', changeName, 'tasks')
1590
+ for (const nn of _evidenced.keys()) {
1591
+ const _cardPath = join(_tasksDir, `task-${nn}.md`)
1592
+ if (existsSync(_cardPath)) {
1593
+ const _paths = parseAllowedPaths(readFileSync(_cardPath, 'utf8'))
1594
+ .map(p => String(p).replace(/^NEW:\s*/, '').trim()).filter(Boolean)
1595
+ if (_paths.length > 0) _allowedByTask.set(nn, _paths)
1596
+ }
1597
+ }
1598
+ } catch { /* 卡面读取失败 → 无锚退化为仅 subject token(与无卡面同口径) */ }
1599
+ let _md = readFileSync(_tasksMdPath, 'utf8')
1600
+ let _ticked = 0
1601
+ let _skippedNoAnchor = 0
1602
+ for (const [nn, _files] of _evidenced) {
1603
+ const _allowed = _allowedByTask.get(nn)
1604
+ if (_allowed && _pathMatches && ![..._files].some(f => _allowed.some(p => _pathMatches(f, p)))) {
1605
+ _skippedNoAnchor++
1606
+ continue // subject 提及但触及面与声明面零交叠——无实现事实锚不勾
1607
+ }
1608
+ const re = new RegExp(`^- \\[ \\](.*task-${nn}[:\\s])`, 'm')
1609
+ if (re.test(_md)) { _md = _md.replace(re, `- [x]$1`); _ticked++ }
1610
+ }
1611
+ if (_ticked > 0) {
1612
+ const { writeAtomicSync } = await import('../fs-atomic.js')
1613
+ writeAtomicSync(_tasksMdPath, _md)
1614
+ console.log(`✅ [自动勾选] ${_ticked} 个任务有实现证据(提交 subject token${_allowedByTask.size > 0 ? ' × allowed_paths 交叠' : ''})但未勾——已机器代勾(full-autopilot-parity)`)
1615
+ }
1616
+ if (_skippedNoAnchor > 0) {
1617
+ console.log(`ℹ️ [自动勾选] ${_skippedNoAnchor} 个 task 号有 subject token 但触及面与 allowed_paths 零交叠——不代勾(无实现事实锚),完成时请人工核对勾选`)
1618
+ }
1619
+ return { ticked: _ticked, skippedNoAnchor: _skippedNoAnchor }
1620
+ }
1621
+ }
1622
+ return { ticked: 0, skippedNoAnchor: 0 }
1623
+ } catch { /* auto-tick fail-soft */ }
1624
+ return { ticked: 0, skippedNoAnchor: 0 }
1625
+ }
1626
+
1575
1627
  export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
1576
1628
  if (stageName !== 'execute' || !changeName) {
1577
1629
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
@@ -1713,7 +1765,7 @@ export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
1713
1765
  }
1714
1766
  }
1715
1767
 
1716
- async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1768
+ export async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1717
1769
  try {
1718
1770
  const changeDir = join(specBase, 'changes', changeName)
1719
1771
  const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
@@ -1747,6 +1799,23 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
1747
1799
  // → 每个 task 进 blockedTasks → 批量完成静默失效
1748
1800
  const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
1749
1801
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1802
+ // ── 缺席草稿自动补齐(坑 task-review-retired-archive-mismatch,2026-10-15 归档门口实证)──
1803
+ // Task Review 层退役(2026-09-26)后 execute 批量完成不再产 per-task review.json,但本函数
1804
+ // 下方复核循环与 archive step2 完成度判定(真相源=review.json verdict)仍以它在场为前提——
1805
+ // 正常走完五阶段的变更到归档门口必然全量「review.json 缺失」。退役时 generateTaskReviewDrafts
1806
+ // 只摘了调用点(函数本体与 backfill-reviews 手动通道都在):此处对缺失面重新接线——机械
1807
+ // 幂等(已存在不覆盖人工/子代理 verdict),失败 fail-open 不阻断批量(最坏退回修复前形态,
1808
+ // 下方复核循环照旧拦)。挂在 runId marker 读取之前:run 缺失时生成器建 run + 写 marker,
1809
+ // 下方循环一次读盘即见。
1810
+ try {
1811
+ const { generateTaskReviewDrafts } = await import('../task-review.js')
1812
+ const draftOut = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
1813
+ if (draftOut && draftOut.generated > 0) {
1814
+ console.log(`📝 [批量完成前置] 自动补 ${draftOut.generated} 份 per-task review 草稿(机械幂等;archive 完成度真相源在场——草稿 verdict=cannot_verify 仍由 verify 兑证/人工复核升级,summarizeTaskCompletion 单列可见)`)
1815
+ }
1816
+ } catch (e) {
1817
+ console.warn(`⚠️ per-task 草稿自动补齐异常(fail-open,批量完成继续走既有复核循环): ${(e && e.message) || e}`)
1818
+ }
1750
1819
  const blockedTasks = []
1751
1820
  const { readReview, isValidExecuteRunId } = await import('../task-review.js')
1752
1821
  // runId marker 对全部 task 恒定:循环外一次读盘(原实现逐 task 重复 readFileSync 同一文件)
package/src/run/gates.js CHANGED
@@ -958,6 +958,99 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
958
958
  writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
959
959
  docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'target_files 对账阻断(②类「声明未做」——计划落空,明细见上)' })
960
960
  }
961
+ // ── 探针一致性 / draft 篡改门禁 / ceremony 双跑三门(坑 verify-close-gate-staggered-exposure,
962
+ // 2026-10-15 消费仓四轮 --done 实证:三门原在实测门之后逐个 fail-fast——每轮先付 195s
963
+ // 实测再见一个新文档问题,四轮纯往返约 20 分钟。三门数据面(facts 已于 runValidators 前
964
+ // backfill、verify-result.md 正文/sidecar、ceremony tier 文件、git diff)与实测结果零数据
965
+ // 依赖——照 required-evidence/reconcile 先例前移并入 docGateFailures 收集(R16 聚合一次
966
+ // 全列),失败统一在检查点 A 回滚(进度不丢,修复后重跑 --done)。──
967
+ // 探针一致性(task-03 / ir-stage-p3b D-002@v1 方案A):「#### 探针 N」机械预填段长在 agent
968
+ // 可编辑的正文里,其防篡改基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。
969
+ // checkProbeConsistency 重跑只读探针与正文锚点对账,分级(D-003):probe1/probe6 不符 =
970
+ // ERROR → 收集阻断(正文预填段疑似被篡改);probe3/probe5 环境敏感维度 + R-06 HEAD 前进
971
+ // 降级 = WARNING → 放行告警(drift);skipped/degraded → 放行提示。
972
+ // 接线形态照 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
973
+ // supportedFixes)+ verify-runs 落盘 + docGateFailures。
974
+ const { checkProbeConsistency } = await import('../verify-postcheck.js') // isStrictChange 复用本块 reconcile 段既有 import
975
+ const probeCheck = checkProbeConsistency({
976
+ // 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
977
+ // reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
978
+ // 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
979
+ cwd, specBase, changeName,
980
+ runtimeRoot: reconcileRuntimeRoot,
981
+ // IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
982
+ // 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
983
+ strictMode: isStrictChange({ pm, cwd, changeName }),
984
+ })
985
+ const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
986
+ const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
987
+ // 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
988
+ // covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
989
+ // 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
990
+ try {
991
+ const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
992
+ const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
993
+ if (existsSync(p7ReportPath)) {
994
+ const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
995
+ if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
996
+ console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
997
+ for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
998
+ console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
999
+ }
1000
+ if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
1001
+ }
1002
+ }
1003
+ } catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
1004
+ // 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
1005
+ // 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
1006
+ // reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
1007
+ // 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
1008
+ writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
1009
+ if (probeBlocked) {
1010
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
1011
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: '探针一致性抽查 error 阻断(明细见上)' })
1012
+ }
1013
+ // ── verify 填槽制篡改门禁(r5l 方案3 / 评审护栏#2 承重件,v2 件二)──
1014
+ // verify-probes --init --draft 生成的机器预填段(任务完成度/风险等级/测试结果/决策追踪)带
1015
+ // sha256 指纹标记 + .runtime sidecar 台账;--done 对账:标记被删 / 内容哈希失配 → 收集阻断
1016
+ // (heredoc 整份重写的典型形态——Write 可用与否均免疫,机制修法不做生成侧劝说)。修改机器
1017
+ // 段唯一通道 verify-probes --amend-draft(重锚 + amendment 审计)。无 sidecar(未用 draft /
1018
+ // 存量变更)→ not-applicable 零红;门禁自身异常 fail-soft(对齐探针抽查 degraded 语义)。
1019
+ try {
1020
+ const { checkDraftIntegrity } = await import('../verify-draft.js')
1021
+ const draftCheck = checkDraftIntegrity({
1022
+ mdPath: join(specBase, 'changes', changeName, 'verify-result.md'),
1023
+ changeName, runtimeRoot: reconcileRuntimeRoot,
1024
+ })
1025
+ if (draftCheck.applicable && !draftCheck.ok) {
1026
+ console.error(`\n❌ verify-result.md 机器预填段篡改门禁:${draftCheck.violations.length} 处失配——draft 预填段被整份重写或标记被删/被手工重锚。`)
1027
+ for (const v of draftCheck.violations) console.error(` - ${v}`)
1028
+ console.error(' 修复:还原机器段原文(AGENT 槽——结论枚举/移交项/审查叙述——不受本门限制);确要修改机器段,跑 sillyspec verify-probes --change ' + changeName + ' --amend-draft 留痕重锚后重新 --done。')
1029
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'draft 篡改门禁阻断' })
1030
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'verify-result.md 机器预填段篡改门禁(明细见上)' })
1031
+ }
1032
+ } catch { /* 门禁异常 fail-soft(不是篡改,对齐探针 degraded 放行) */ }
1033
+ // ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
1034
+ // execute 评审 FAIL 项补接线):声明档(.runtime/ceremony-tier-<change>.json 开跑定价)vs
1035
+ // 实际 diff 重跑 blast+span 的事实档,mismatch(severity=error,懒 agent 低报)汇入收集
1036
+ // 硬 flag 面(照 reconcile/probe 先例检查点 A 统一回滚,修复后重跑 --done 进度不丢)。
1037
+ // 检查/记账/种子收敛在 runCeremonyDualRunCheck 单点(archive 出口 complete-handlers.js
1038
+ // runArchiveCeremonyDualRunExit 同款消费);skipped(档位文件缺失=存量变更零红门禁)/
1039
+ // degraded(git 不可用降级)零噪音放行;整体 fail-soft——检查自身异常降级放行(异常
1040
+ // 不是低报,对齐 archive 出口同款处置)。specBase/runtimeRoot 与四邻检查同源。──
1041
+ try {
1042
+ const { runCeremonyDualRunCheck, printCeremonyDualRunCheck } = await import('../verify-postcheck.js')
1043
+ const ceremonyCheck = await runCeremonyDualRunCheck({
1044
+ cwd, specBase, changeName,
1045
+ runtimeRoot: reconcileRuntimeRoot,
1046
+ })
1047
+ printCeremonyDualRunCheck(ceremonyCheck)
1048
+ if (ceremonyCheck.status === 'mismatch' && ceremonyCheck.severity === 'error') {
1049
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'ceremony 双跑 mismatch(声明档低报,明细见上)' })
1050
+ }
1051
+ } catch (e) {
1052
+ console.warn(`⚠️ Ceremony 双跑对账异常降级放行(不阻断 verify,fail-soft): ${(e && e.message) || e}`)
1053
+ }
961
1054
  }
962
1055
  // ── 收集模式检查点 A(verify):纯文档门全清后才进入实测门(test/lint/parity/probe 保持
963
1056
  // fail-fast 不变)。brainstorm/plan/execute 不走本检查点(其实测面为空,检查点 B 收口)。──
@@ -1351,95 +1444,6 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1351
1444
  const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
1352
1445
  const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
1353
1446
  printVerifyDeletionCheck(deletionCheck)
1354
- // ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
1355
- // 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
1356
- // 基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。checkProbeConsistency 重跑只读
1357
- // 探针与正文锚点对账,分级(D-003):probe1/probe6 不符 = ERROR → 阻断回滚(正文预填段疑似
1358
- // 被篡改);probe3/probe5 环境敏感维度 + R-06 HEAD 前进降级 = WARNING → 放行告警(drift);
1359
- // skipped(存量旧格式报告/无 changeName)/degraded(重跑异常 fail-soft)→ 放行提示。
1360
- // 接线形态照上方 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
1361
- // supportedFixes)+ verify-runs 落盘 + rollbackCompletionAndReturn。
1362
- const { checkProbeConsistency, isStrictChange } = await import('../verify-postcheck.js')
1363
- const probeCheck = checkProbeConsistency({
1364
- // 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
1365
- // reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
1366
- // 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
1367
- cwd, specBase, changeName,
1368
- runtimeRoot: reconcileRuntimeRoot,
1369
- // IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
1370
- // 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
1371
- strictMode: isStrictChange({ pm, cwd, changeName }),
1372
- })
1373
- const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
1374
- const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
1375
- // 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
1376
- // covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
1377
- // 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
1378
- try {
1379
- const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
1380
- const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
1381
- if (existsSync(p7ReportPath)) {
1382
- const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
1383
- if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
1384
- console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
1385
- for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
1386
- console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
1387
- }
1388
- if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
1389
- }
1390
- }
1391
- } catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
1392
- // 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
1393
- // 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
1394
- // reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
1395
- // 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
1396
- writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
1397
- if (probeBlocked) {
1398
- writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
1399
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1400
- }
1401
- // ── verify 填槽制篡改门禁(r5l 方案3 / 评审护栏#2 承重件,v2 件二)──
1402
- // verify-probes --init --draft 生成的机器预填段(任务完成度/风险等级/测试结果/决策追踪)带
1403
- // sha256 指纹标记 + .runtime sidecar 台账;--done 对账:标记被删 / 内容哈希失配 → 阻断回滚
1404
- // (heredoc 整份重写的典型形态——Write 可用与否均免疫,机制修法不做生成侧劝说)。修改机器
1405
- // 段唯一通道 verify-probes --amend-draft(重锚 + amendment 审计)。无 sidecar(未用 draft /
1406
- // 存量变更)→ not-applicable 零红;门禁自身异常 fail-soft(对齐探针抽查 degraded 语义)。
1407
- try {
1408
- const { checkDraftIntegrity } = await import('../verify-draft.js')
1409
- const draftCheck = checkDraftIntegrity({
1410
- mdPath: join(specBase, 'changes', changeName, 'verify-result.md'),
1411
- changeName, runtimeRoot: reconcileRuntimeRoot,
1412
- })
1413
- if (draftCheck.applicable && !draftCheck.ok) {
1414
- console.error(`\n❌ verify-result.md 机器预填段篡改门禁:${draftCheck.violations.length} 处失配——draft 预填段被整份重写或标记被删/被手工重锚。`)
1415
- for (const v of draftCheck.violations) console.error(` - ${v}`)
1416
- console.error(' 修复:还原机器段原文(AGENT 槽——结论枚举/移交项/审查叙述——不受本门限制);确要修改机器段,跑 sillyspec verify-probes --change ' + changeName + ' --amend-draft 留痕重锚后重新 --done。')
1417
- writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'draft 篡改门禁阻断' })
1418
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1419
- }
1420
- } catch { /* 门禁异常 fail-soft(不是篡改,对齐探针 degraded 放行) */ }
1421
- // ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
1422
- // execute 评审 FAIL 项补接线):声明档(.runtime/ceremony-tier-<change>.json 开跑定价)vs
1423
- // 实际 diff 重跑 blast+span 的事实档,mismatch(severity=error,懒 agent 低报)汇入 verify
1424
- // errors 硬 flag 面——阻断回滚(照 reconcile/probe 先例 rollbackCompletionAndReturn,修复后
1425
- // 重跑 --done 进度不丢)。检查/记账/种子收敛在 runCeremonyDualRunCheck 单点(archive 出口
1426
- // complete-handlers.js runArchiveCeremonyDualRunExit 同款消费);skipped(档位文件缺失=存量
1427
- // 变更零红门禁)/degraded(git 不可用降级)零噪音放行;整体 fail-soft——检查自身异常降级
1428
- // 放行(异常不是低报,对齐 archive 出口同款处置)。specBase/runtimeRoot 与四邻检查同源
1429
- // (reconcileRuntimeRoot=resolveRuntimeRoot(platformOpts, specBase),口径一处定义不二算)。──
1430
- try {
1431
- const { runCeremonyDualRunCheck, printCeremonyDualRunCheck } = await import('../verify-postcheck.js')
1432
- const ceremonyCheck = await runCeremonyDualRunCheck({
1433
- cwd, specBase, changeName,
1434
- runtimeRoot: reconcileRuntimeRoot,
1435
- })
1436
- printCeremonyDualRunCheck(ceremonyCheck)
1437
- if (ceremonyCheck.status === 'mismatch' && ceremonyCheck.severity === 'error') {
1438
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1439
- }
1440
- } catch (e) {
1441
- console.warn(`⚠️ Ceremony 双跑对账异常降级放行(不阻断 verify,fail-soft): ${(e && e.message) || e}`)
1442
- }
1443
1447
  // ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
1444
1448
  // 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
1445
1449
  // 同口径回执),消灭「src 改动平移行号 → 下次 docs gate 拦」的人工往返。挂 verify 收尾即
package/src/run/prompt.js CHANGED
@@ -1390,7 +1390,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1390
1390
  if (stageName === 'brainstorm' && promptText.includes('{DECISION_HITS}')) {
1391
1391
  try {
1392
1392
  const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
1393
- const { deathPathNote } = await import('../knowledge-match.js')
1393
+ const { deathPathNote, decisionChangeQualifier } = await import('../knowledge-match.js')
1394
1394
  const decSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1395
1395
  const decKnowledgeDir = join(decSpecBase, 'knowledge')
1396
1396
  // taskContext:changeName(brainstorm Step2 时 tasks.md 尚未生成,变更名是唯一稳定任务
@@ -1402,10 +1402,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1402
1402
  let decInjected = ''
1403
1403
  if (rejectedHits.length > 0) {
1404
1404
  const lines = [
1405
- '⚠️ 否决决策/死路注记提示(历史已否决或已记死路,防复潮)——下列决策此前已被否决或已记死路。看到理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据:'
1405
+ '⚠️ 否决决策/死路注记提示(历史已否决或已记死路,防复潮)——下列决策此前已被否决或已记死路。看到理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据;跨变更引用他条目须带变更名限定(变更名#D-xxx@vN——同号不同变更互不相干):'
1406
1406
  ]
1407
1407
  for (const h of rejectedHits.slice(0, 5)) {
1408
- lines.push(`- ${h.id} ${h.title}(${h.file})${h.status === 'rejected' ? '' : ' ⚰️死路注记'}`)
1408
+ lines.push(`- ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)})${h.status === 'rejected' ? '' : ' ⚰️死路注记'}`)
1409
1409
  lines.push(` - ${h.status === 'rejected' ? '否决理由' : '死路'}:${h.status === 'rejected' ? (h.reason || '(未记录)') : deathPathNote(h.reason)}`)
1410
1410
  lines.push(` - 复潮条件:${h.revisitWhen || '(未记录)'}`)
1411
1411
  }
package/src/run/stage.js CHANGED
@@ -417,7 +417,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
417
417
  // 进程内直调 runCommand 的入口(测试/编程调用)与 --change 指向不存在会话的形态。
418
418
  console.error(`❌ quick 通道已退役,无法启动 quick 会话(若带 --change 则该会话不存在或已清理)。`)
419
419
  console.error(` 新工作请走轻量变更(--input 引号内换行合法,可照抄):`)
420
- console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
420
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
421
421
  console.error('')
422
422
  console.error(' 成功标准:')
423
423
  console.error(' - <可验证标准>"')
@@ -165,6 +165,48 @@ export function detectBatchCheckCadence(events) {
165
165
  return worst;
166
166
  }
167
167
 
168
+ /**
169
+ * 侧车勾选突发检测(2026-10-14-tick-burst-sidecar-gate,纯函数):task-ticks.jsonl 的毫秒级
170
+ * 时戳按相邻间隔窗口分簇——CLI 逐格快连点(收口一把点的官方路径形态)在事件流里是单格精确跳,
171
+ * 10-07 采样去重后对 detectBatchCheckCadence 不可见(近期实证:6 格 2 秒内/4 格 1 秒内零检出);
172
+ * 侧车是 CLI tick 的事实源(随归档进 git、不受观测盲窗影响),收口节奏门回退消费本函数——
173
+ * CLI 与直接 Edit 两条路径同判。
174
+ * 语义:tickLog 同任务取最早(幂等重勾防御)+ 坏行跳过(task 形态/时戳有限性)+ 排序后按
175
+ * gap<=windowMs(缺省 60s,opts.windowMs 覆写)分簇;最大簇 size>=2 → 返回与事件流同形的
176
+ * batchTick 记录。from/to 是 CLI 勾序计数(Edit 翻格不记侧车,与事件流 checked N→M 计数空间
177
+ * 不同源)——resolveBatchTickAction 只消费跳幅(to-from)与 ts,语义等价;detail 带「侧车突发」
178
+ * 标注防口径混淆。fail-open:不足两格/空输入/全坏行 → null。
179
+ */
180
+ export function detectTickBurst(tickLog, opts = {}) {
181
+ const windowMs = Number.isFinite(opts.windowMs) ? opts.windowMs : 60_000;
182
+ const byTask = new Map();
183
+ for (const r of Array.isArray(tickLog) ? tickLog : []) {
184
+ const task = String((r && r.task) || '').trim();
185
+ const ts = Number(r && r.ts);
186
+ if (!/^task-\d+$/.test(task) || !Number.isFinite(ts)) continue;
187
+ const prev = byTask.get(task);
188
+ if (prev === undefined || ts < prev) byTask.set(task, ts);
189
+ }
190
+ const ticks = [...byTask.values()].sort((a, b) => a - b);
191
+ if (ticks.length < 2) return null;
192
+ let worst = null;
193
+ let start = 0;
194
+ for (let i = 1; i <= ticks.length; i++) {
195
+ if (i < ticks.length && ticks[i] - ticks[i - 1] <= windowMs) continue;
196
+ const size = i - start;
197
+ if (size >= 2 && (!worst || size > worst.size)) {
198
+ worst = { size, from: start, to: i, ts: ticks[start], spanMs: ticks[i - 1] - ticks[start] };
199
+ }
200
+ start = i;
201
+ }
202
+ if (!worst) return null;
203
+ return {
204
+ from: worst.from, to: worst.to, size: worst.size, spanMs: worst.spanMs, ts: worst.ts,
205
+ source: 'tick-burst',
206
+ detail: `checked ${worst.from}→${worst.to}(task tick 侧车突发 ${worst.size} 格)`,
207
+ };
208
+ }
209
+
168
210
  /**
169
211
  * 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
170
212
  * 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
@@ -193,4 +235,4 @@ export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allow
193
235
  return { action: 'reject', reason: 'agent-batch-tick' };
194
236
  }
195
237
 
196
- export default { detectFakeCheckCompletion, detectBatchCheckCadence };
238
+ export default { detectFakeCheckCompletion, detectBatchCheckCadence, detectTickBurst };
@@ -1059,8 +1059,12 @@ const API_COVERAGE_HEADING_RE = /^## 接口验证覆盖矩阵/m
1059
1059
  const API_ENDPOINT_CELL_RE = /(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|]+)/
1060
1060
  /** 声明占位行(D-005 零解析降级侧,骨架「本变更接口面:N 端点(agent 声明)」字面同源) */
1061
1061
  const API_DECLARED_ROW_RE = /本变更接口面[::]\s*(\d+)\s*端点/
1062
- /** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态) */
1063
- const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;))]+)/g
1062
+ /** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态)。
1063
+ * 字符类左右括号对称排除(坑 verify-close-gate-staggered-exposure,2026-10-15 消费仓 node 直调
1064
+ * 实证:原只排右括号)不排左括号(——`GET /api/x(注记)` 的路径吞成 `/api/x(注记`,与解析面
1065
+ * 永不匹配 → 恒空指,报错文案却指向「design.md 未命中」误导排查方向)。中文(英文、全/半角
1066
+ * 四形与既有分隔符一并终止捕获。 */
1067
+ const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;()()]+)/g
1064
1068
  /** 探索行标记(骨架文法注释字面同源:证据列含 [探索]) */
1065
1069
  const API_EXPLORATION_MARK_RE = /\[探索\]/
1066
1070
 
@@ -1261,9 +1265,16 @@ export function judgeApiCoverageMatrix(args) {
1261
1265
  }
1262
1266
  }
1263
1267
  if (anchorViolations.length > 0) {
1268
+ // 空指诊断(坑 verify-close-gate-staggered-exposure 建议③,2026-10-15 实证:报错只说「未命中
1269
+ // 解析面」不展示解析面实际内容——「锚点吞字」与「design 缺表」分不清,agent 被引去改 design
1270
+ // 文档白做功。附上 parseSet 实际端点集一眼对齐)。
1271
+ const parseFaceDesc = parseSet.size > 0
1272
+ ? [...parseSet].join(';')
1273
+ : '(解析面为空——design.md 接口段未解析到端点,先表格化接口段再跑 verify-probes --init)'
1264
1274
  errors.push(
1265
1275
  `接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
1266
1276
  `(covered/partial 证据须含锚点五形态之一,可复制样例:design接口表#POST /api/xx(# 后必须 METHOD /path 且命中 design 接口段解析面,仅表名/行号不计)/ 权限矩阵[admin×读] / 契约表@任务卡字段清单 / DDL@users.id / 载荷@e2e_body.json;covered-service 证据须含测试锚点三形态之一:\`.test.\` 文件名 / file:line / 反引号包裹的测试名):${anchorViolations.join(';')}。` +
1277
+ `design 接口段实际解析面端点集:${parseFaceDesc}——design接口表# 锚点路径须与之逐字一致。` +
1267
1278
  `修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
1268
1279
  )
1269
1280
  }
@@ -1419,6 +1430,9 @@ function extractPermissionMatrixText(designMd) {
1419
1430
  * - tasks/ 不存在(quick 会话/旧变更)或 verify-result.md 未落盘(中间步骤)→ no-op
1420
1431
  * - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
1421
1432
  * - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
1433
+ * - context.apiFaceLive(可选,2026-10-14-gate-verify-apiface-preflight):gate/derive 预检侧
1434
+ * 以同一解析器实况解析 design.md 注入(与 --done backfill 产物同形同源,零落盘)——在场时
1435
+ * 覆写 facts.apiFace 作解析产出集(过期 facts 不得赢),未注入行为与现状完全一致
1422
1436
  * - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
1423
1437
  * - resolveChangeRisk 声明面判级(design 文件清单 × blast 声明 + explicit,task-03)
1424
1438
  * - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
@@ -1441,9 +1455,18 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
1441
1455
  // 2026-09-19-ceremony-pricing-five-cuts task-03:声明面判级(design 文件清单 × blast 声明 +
1442
1456
  // explicit;level 为五级词兼容字段——evidenceRequired → integration-critical)
1443
1457
  const riskProfile = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
1458
+ // gate/derive 预检注入(2026-10-14-gate-verify-apiface-preflight FR-01/FR-02):facts.apiFace
1459
+ // 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
1460
+ // 刻意不落键 → 预检 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。调用方
1461
+ // (machine-interface runGate/derive)以同一解析器实况解析 design.md 经 context.apiFaceLive
1462
+ // 传入(本模块零 import verify-probes 纪律:注入不做函数调用)——与 --done backfill 产物同形
1463
+ // 同源,在场时必须覆写 facts(过期 facts 不得赢),未注入时行为与现状完全一致。
1464
+ const apiFace = context.apiFaceLive && typeof context.apiFaceLive === 'object'
1465
+ ? context.apiFaceLive
1466
+ : (facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null)
1444
1467
  return judgeApiCoverageMatrix({
1445
1468
  matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
1446
- apiFace: facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null,
1469
+ apiFace,
1447
1470
  consumerHints: facts && facts.consumerHints && typeof facts.consumerHints === 'object' ? facts.consumerHints : null,
1448
1471
  facts,
1449
1472
  factsExpected: true,