sillyspec 3.28.13 → 3.29.0

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/blast-surface.js +168 -0
  3. package/src/ceremony-tier.js +359 -0
  4. package/src/change-risk-profile.js +72 -199
  5. package/src/config-schema.js +21 -0
  6. package/src/decisions-io.js +32 -0
  7. package/src/design-facts.js +1 -0
  8. package/src/doctor-diagnostics.js +119 -1
  9. package/src/index.js +92 -5
  10. package/src/knowledge-stats.js +25 -0
  11. package/src/modules.js +28 -0
  12. package/src/prefill.js +491 -0
  13. package/src/probe7-anchor-check.js +8 -6
  14. package/src/quick-gate-profile.js +8 -4
  15. package/src/quick-session-owner.js +63 -0
  16. package/src/review-dispatch.js +473 -1
  17. package/src/review-material-pack.js +265 -0
  18. package/src/review-tier.js +185 -41
  19. package/src/run/archive-distill.js +6 -0
  20. package/src/run/command.js +158 -87
  21. package/src/run/complete-handlers.js +82 -1
  22. package/src/run/complete.js +37 -1
  23. package/src/run/gates.js +301 -19
  24. package/src/run/prompt.js +423 -21
  25. package/src/run/shared.js +36 -0
  26. package/src/run/verify-quality-scan.js +14 -6
  27. package/src/scope-audit.js +8 -1
  28. package/src/span-risk-surface.js +127 -0
  29. package/src/stage-contract-spec.js +2 -2
  30. package/src/stage-contract.js +102 -81
  31. package/src/stage-review.js +26 -2
  32. package/src/stages/brainstorm.js +28 -15
  33. package/src/stages/execute.js +10 -2
  34. package/src/stages/plan.js +29 -12
  35. package/src/stages/verify.js +7 -7
  36. package/src/taskcard.js +3 -0
  37. package/src/verify-postcheck.js +375 -20
  38. package/src/verify-probes.js +117 -10
  39. package/src/worktree.js +1 -1
  40. package/templates/prompts/taskcard-rules.md +1 -0
  41. package/templates/prompts/verify-probes.md +1 -1
package/src/run/prompt.js CHANGED
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { basename, join } from 'node:path'
22
22
  import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
23
+ import { createHash } from 'node:crypto'
24
+ import jsYaml from 'js-yaml'
23
25
  import { writeAtomicSync } from '../fs-atomic.js'
24
26
  import { stageRegistry } from '../stages/index.js'
25
27
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
@@ -34,6 +36,97 @@ import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
34
36
  // buildKnowledgeInjection docstring)。
35
37
  import { matchKnowledge } from '../knowledge-match.js'
36
38
  import { appendKnowledgeHit } from '../knowledge-hits.js'
39
+ import { readActiveFrDigest } from '../fr-index.js'
40
+ // 前置失败清单 validator 面(2026-09-18-preflight-slimming task-01):design-facts /
41
+ // stage-contract-engine 均为 src/ 叶子方向(不反向 import 本模块),静态 import 不引入环。
42
+ import { validateDesignFileList } from '../design-facts.js'
43
+ import { evaluateRules } from '../stage-contract-engine.js'
44
+ // 注入账本锁(2026-09-18-preflight-slimming task-02,R-06):withFileLock(quicklog.js:40 先例,
45
+ // 防多会话 read-modify-write 丢更新)——quicklog.js 仅 import path/fs/crypto/git-helper/
46
+ // stage-contract-spec,不反向引用本模块,静态 import 不引入环。
47
+ import { withFileLock } from '../quicklog.js'
48
+
49
+ // ═══════════════════════════════════════════════════════════════
50
+ // ceremony 档位仪式菜单(2026-09-18-ceremony-risk-pricing task-05,D-002/D-005)
51
+ // ═══════════════════════════════════════════════════════════════
52
+
53
+ /** ceremony 档位序(与 ceremony-tier.js CEREMONY_TIERS 同序;此处仅作消费侧排序/枚举校验,不 reimport) */
54
+ const CEREMONY_TIER_ORDER = ['S0', 'S1', 'S2', 'S3']
55
+
56
+ /** 档位→仪式菜单(消费侧映射,design「档位→仪式菜单」契约:引擎只定价不映射) */
57
+ const CEREMONY_TIER_MENUS = [
58
+ ['S0', 'CLI 清单核验(stage-review-checklist 机械项逐条核对,零 token 发散)'],
59
+ ['S1', 'CLI 清单核验 + 定向探针抽查(机械项全查 + 高风险交叉点定向源码探针,无需独立子代理)'],
60
+ ['S2', '独立评审×1(独立审查子代理单轮 + review.json)'],
61
+ ['S3', '两轮独立评审 + Grill 深查(两轮独立子代理交叉 + review.json)'],
62
+ ]
63
+
64
+ /**
65
+ * 读 local.yaml `ceremony:` 段(best-effort 绝不抛,对齐 readReviewChannelPriority 范式)。
66
+ * force_tier:S0|S1|S2|S3 枚举逃生阀(非法值 warn 后忽略);shadow:boolean 缺省 true。
67
+ * config-schema.js ceremony 节的 prompt 面 reader(影子派发侧消费随 task-06 接线)。
68
+ */
69
+ export function readCeremonyLocalConfig(cwd) {
70
+ let raw = null
71
+ try {
72
+ const p = join(cwd || process.cwd(), '.sillyspec', 'local.yaml')
73
+ if (existsSync(p)) raw = jsYaml.load(readFileSync(p, 'utf8'))
74
+ } catch { raw = null }
75
+ const c = raw && typeof raw === 'object' && raw.ceremony && typeof raw.ceremony === 'object'
76
+ ? raw.ceremony
77
+ : null
78
+ const forceRaw = c && typeof c.force_tier === 'string' ? c.force_tier.trim() : null
79
+ const forceTier = CEREMONY_TIER_ORDER.includes(forceRaw) ? forceRaw : null
80
+ if (forceRaw && !forceTier) {
81
+ console.warn(`[sillyspec] ceremony.force_tier 未知档位「${forceRaw}」已忽略(可选:${CEREMONY_TIER_ORDER.join(' | ')})`)
82
+ }
83
+ const shadow = c && typeof c.shadow === 'boolean' ? c.shadow : true
84
+ return { forceTier, shadow }
85
+ }
86
+
87
+ /**
88
+ * ceremony 档位菜单渲染({REVIEW_TIER} 注入值的追加块——追加不替换:tier: self/independent
89
+ * 语义与 {REVIEW_TIER_REASON} 注入机制原样保留,菜单块按 ceremonyTier 标注当前档)。
90
+ *
91
+ * 逃生阀(ceremony.force_tier)只升不降:强制档高于客观定价档 → 菜单当前档随调、注入
92
+ * tier 值升为 independent(S2/S3);低于客观档 → 只留注记不降档——防 prompt 面与 gate 侧
93
+ * classifyReviewTier(未读逃生阀)判定分裂(prompt 说 self 而 gate 等 review.json 的错位阻断)。
94
+ *
95
+ * 强制轻仪审计痕(FR-02):agent 报 plan_level=full 而档位 S0/S1 → 菜单块追加固定审计行
96
+ * (照「分级异常降级 self」既有降级戳先例:注入文案即留痕),明示勿因「计划写得完整」自行升仪。
97
+ *
98
+ * @param {{ ceremonyTier?: string, tierValue?: string, planLevel?: string|null, cwd?: string }} args
99
+ * @returns {{ tierValue: string, menuMd: string }} menuMd 以 \n 开头(拼在 tier 值后);无有效档位时为 ''
100
+ */
101
+ export function renderCeremonyTierInjection({ ceremonyTier, tierValue, planLevel, cwd } = {}) {
102
+ const baseTierValue = tierValue || 'self'
103
+ if (!CEREMONY_TIER_ORDER.includes(ceremonyTier)) {
104
+ return { tierValue: baseTierValue, menuMd: '' }
105
+ }
106
+ const { forceTier, shadow } = readCeremonyLocalConfig(cwd)
107
+ let active = ceremonyTier
108
+ let forceNote = ''
109
+ let effectiveTierValue = baseTierValue
110
+ if (forceTier && forceTier !== ceremonyTier) {
111
+ if (CEREMONY_TIER_ORDER.indexOf(forceTier) > CEREMONY_TIER_ORDER.indexOf(ceremonyTier)) {
112
+ active = forceTier
113
+ forceNote = `——local.yaml ceremony.force_tier=${forceTier} 强制升档(逃生阀,绕过客观定价)`
114
+ if (active === 'S2' || active === 'S3') effectiveTierValue = 'independent'
115
+ } else {
116
+ forceNote = `——local.yaml ceremony.force_tier=${forceTier} 低于客观定价档,不降档(只升不降)`
117
+ }
118
+ }
119
+ const menuLines = CEREMONY_TIER_MENUS.map(([t, menu]) => `- ${t}:${menu}${t === active ? ' ◀ 当前档' : ''}`)
120
+ const lightTier = active === 'S0' || active === 'S1'
121
+ const shadowNote = lightTier && shadow
122
+ ? '\n(影子期 on:轻档明面轻仪,后台静默派发重仪式对照只记账不阻断——local.yaml ceremony.shadow 开关,对照报告见 doctor)'
123
+ : ''
124
+ const auditTrail = planLevel === 'full' && lightTier
125
+ ? '\n⚠️ 强制轻仪审计痕:agent 报 plan_level=full 但 ceremony 档为轻档——仪式按 risk 计价、plan_level 仅编排,按当前轻档菜单执行,勿因「计划写得完整」自行升回 independent×2'
126
+ : ''
127
+ const menuMd = `\nceremony_tier: ${active}${forceNote}(仪式按 risk 计价,plan_level 仅编排——四档菜单:)\n${menuLines.join('\n')}${shadowNote}${auditTrail}`
128
+ return { tierValue: effectiveTierValue, menuMd }
129
+ }
37
130
 
38
131
  /**
39
132
  * 从 _module-map.yaml 读取模块上下文索引
@@ -70,10 +163,9 @@ export function loadModuleContextIndex(specBase, projectName) {
70
163
  * @param {object} moduleIndex - loadModuleContextIndex 返回值
71
164
  * @param {string} specBase - 规范目录
72
165
  * @param {string} projectName - 项目名
73
- * @returns {string} 上下文注入文本,空字符串表示无匹配模块
166
+ * @returns {{text: string, frModules: Array<{id: string, count: number}>}} 上下文注入文本(空=无匹配模块)+ 有活需求的模块清单(供遥测)
74
167
  */
75
- function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) {
76
- if (!moduleIndex || !taskDescription) return ''
168
+ export function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) { if (!moduleIndex || !taskDescription) return { text: '', frModules: [] }
77
169
 
78
170
  const taskLower = taskDescription.toLowerCase()
79
171
  const matched = []
@@ -93,15 +185,22 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
93
185
  if (score > 0) matched.push({ moduleId, data, score, matchReasons })
94
186
  }
95
187
 
96
- if (matched.length === 0) return ''
188
+ if (matched.length === 0) return { text: '', frModules: [] }
97
189
 
98
190
  matched.sort((a, b) => b.score - a.score)
99
191
 
100
192
  let injection = '\n### 📦 模块上下文(按相关性排序,来自 Module Context Index)\n\n'
101
193
  injection += `> 以下模块上下文由 scan 阶段生成的 _module-map.yaml 自动匹配。\n`
194
+ injection += `> 活需求来自 knowledge/fr 索引(active 以索引为准,现场 join)——承接/取代指引见 step8 digest\n`
102
195
  injection += `> Matched modules: ${matched.map(m => m.moduleId).join(', ')}\n`
103
196
  injection += `> Reasons: ${matched.map(m => m.matchReasons.join(', ')).join('; ')}\n\n`
104
197
 
198
+ // FR 活需求注入期 join(ql-20260919-002,L2 替代形态):join 键 = 域≡模块 id
199
+ // (fr-index.js resolveTouchedDomains 同键),现场查 active 条目——id+标题紧凑行(场景名
200
+ // 留给 brainstorm step8 digest:此处设计期感知,digest 写作期指引——**有意冗余勿当重复
201
+ // 优化掉**)。截前 5 条+溢出指针行;空段消隐(与 digest 空态同款)。fail-open:读失败该
202
+ // 模块无活需求行,绝不阻塞注入。
203
+ const frModules = []
105
204
  for (const { moduleId, data } of matched) {
106
205
  injection += `#### ${moduleId}\n`
107
206
  if (data.role) injection += `- **职责**: ${String(data.role).slice(0, 100)}\n`
@@ -124,10 +223,21 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
124
223
  if (recentChanges.length > 0) {
125
224
  injection += `- **最近变更**: ${recentChanges.slice(0, 3).map(e => e.name).join('、')}\n`
126
225
  }
226
+ // 活需求行(注入期 join 本体)
227
+ try {
228
+ const frs = readActiveFrDigest(join(specBase, 'knowledge'), [moduleId])
229
+ if (frs.length > 0) {
230
+ const top = frs.slice(0, 5).map(f => `${f.id} ${f.title}`.trim()).join(';')
231
+ injection += `- **活需求**: ${top}`
232
+ if (frs.length > 5) injection += `;+${frs.length - 5} 条见 knowledge/fr/${moduleId}.md`
233
+ injection += '\n'
234
+ frModules.push({ id: moduleId, count: frs.length })
235
+ }
236
+ } catch { /* fail-open:FR 读失败该模块无活需求行 */ }
127
237
  injection += '\n'
128
238
  }
129
239
 
130
- return injection
240
+ return { text: injection, frModules }
131
241
  }
132
242
 
133
243
  /**
@@ -450,6 +560,198 @@ export function requiresUser(step, promptText) {
450
560
  return WAIT_MARKER_RE.test(String(promptText || ''))
451
561
  }
452
562
 
563
+ // ── 前置失败清单 + 注入账本分叉(2026-09-18-preflight-slimming task-01,D-001@v1 / D-002@v1)──
564
+ // 纯函数面三导出之二/之三(hasDecisionId 在 src/decisions-io.js)。本节只打底零接线:
565
+ // {PREFLIGHT_FAILURES} 占位符渲染与模块/scan 注入分叉归 task-02,stages 步骤定义上的
566
+ // preflightValidators 声明归 task-04——声明落地前 renderPreflightFailures 恒返 '',旧 prompt
567
+ // 逐字节不变(design 兼容策略第一条)。
568
+
569
+ /** 前置清单聚合条数帽(R-01 防打架三约束:条数帽 5——截断防清单全文顶回上下文) */
570
+ const PREFLIGHT_MAX_ITEMS = 5
571
+
572
+ /** 单 validator 超时帽 ms(R-02:只读快跑不拖慢 prompt 渲染;超时该 validator 计为跳过) */
573
+ const PREFLIGHT_VALIDATOR_TIMEOUT_MS = 3000
574
+
575
+ /** 超时哨兵(Promise.race 输出侧标记;Symbol 防与 validator 返回值撞车) */
576
+ const PREFLIGHT_TIMEOUT = Symbol('preflight-validator-timeout')
577
+
578
+ /**
579
+ * 已声明 validator 名 → 只读校验函数映射(分发骨架的映射表)。
580
+ *
581
+ * 步骤定义在 stages/* 上声明 `preflightValidators: ['<键名>', ...]`(task-04 落地),渲染时按
582
+ * 声明序查本表调用。本表收录键:
583
+ * - `design-file-list`:design-facts.validateDesignFileList 只读快查(design.md 文件清单
584
+ * 幻觉路径 gate 前置)——errors 项为 {path, message},取 message。
585
+ * - `four-piece-rules`:stage-contract 引擎轻查 brainstorm 四件套规则(evaluateRules 纯 kind
586
+ * dispatch,existsSync/readFileSync 量级)。scale 读取与 stage-contract.js
587
+ * validateBrainstormOutputs 同源(design.md frontmatter,fail-safe 无 scale → 四件套全
588
+ * 要求),保证「事前给的 == 事后查的」。v2 扩展点:若内容类规则实测拖慢渲染,可按
589
+ * rule.id 白名单只跑四件套 file-exists 子集。
590
+ * - `postcheck-lite`(plan postcheck 轻子集)/ `allowed-paths-scan`(execute allowed_paths
591
+ * 越界速查):v2 扩展点——task-04 先声明亦安全跳过(未收录键零贡献,不报错)。
592
+ *
593
+ * 每个 validator 返回 { errors: string[] }(error 级失败消息;warning 不入清单——帽 5 内
594
+ * 只放会拦 --done 的项,防噪声挤占)。
595
+ */
596
+ const PREFLIGHT_VALIDATORS = {
597
+ 'design-file-list': ({ changeDir, cwd }) => {
598
+ const r = validateDesignFileList({ changeDir, cwd })
599
+ const errors = (r && Array.isArray(r.errors)) ? r.errors : []
600
+ return { errors: errors.map((e) => (e && e.message) ? e.message : String(e)) }
601
+ },
602
+ 'four-piece-rules': ({ changeDir }) => {
603
+ let scale = null
604
+ const designPath = join(changeDir, 'design.md')
605
+ if (existsSync(designPath)) {
606
+ const fm = readFileSync(designPath, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
607
+ const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
608
+ if (sm) scale = sm[1]
609
+ }
610
+ const r = evaluateRules('brainstorm', { changeDir, scale })
611
+ return { errors: (r && Array.isArray(r.errors)) ? r.errors : [] }
612
+ },
613
+ }
614
+
615
+ /**
616
+ * 单 validator 只读快跑(带 3s 超时帽):Promise.race 模式——超时计为跳过(零贡献);
617
+ * validator 抛错原样上抛(由 renderPreflightFailures 外层 catch 整体 fail-open 返 ''——
618
+ * 验收口径「validator 抛错或无失败返回空串」)。finally clearTimeout 防 3s 定时器
619
+ * 拖住 CLI 短进程退出(prompt 渲染完即退,悬挂 handle 会多挂 3s)。
620
+ *
621
+ * @param {string} name PREFLIGHT_VALIDATORS 键名
622
+ * @param {{ changeDir: string|null, cwd: string, specBase: string, stageName: string, stepName: string, changeName: string|null }} ctx
623
+ * @returns {Promise<string[]>} 该 validator 的 error 级失败消息(超时/未收录键 → [])
624
+ */
625
+ async function runPreflightValidator(name, ctx) {
626
+ const fn = PREFLIGHT_VALIDATORS[name]
627
+ if (typeof fn !== 'function') return [] // 未收录键安全跳过(v2 扩展点见 PREFLIGHT_VALIDATORS 注释)
628
+ let timerId = null
629
+ try {
630
+ const timeout = new Promise((resolve) => { timerId = setTimeout(() => resolve(PREFLIGHT_TIMEOUT), PREFLIGHT_VALIDATOR_TIMEOUT_MS) })
631
+ const result = await Promise.race([Promise.resolve().then(() => fn(ctx)), timeout])
632
+ if (result === PREFLIGHT_TIMEOUT) return []
633
+ return (result && Array.isArray(result.errors)) ? result.errors : []
634
+ } finally {
635
+ if (timerId != null) clearTimeout(timerId)
636
+ }
637
+ }
638
+
639
+ /**
640
+ * renderPreflightFailures —— 产出型步骤 prompt 的前置失败清单渲染(D-001@v1 Phase 1 纯函数面)。
641
+ *
642
+ * 只读快跑本步骤 --done 将消费的 validator 子集,聚合 error 级失败为清单文本:
643
+ * - 分发骨架:从 stages 注册表步骤定义读 `preflightValidators` 键(task-04 落声明;无声明/
644
+ * 无该 stage/无该步骤名 → 返 ''——渐进兼容,声明落地前零注入)。
645
+ * - 聚合帽:条数帽 5 截断,截断时加「…另 N 项见 gate」行(完整判定归 gate,清单是预检
646
+ * 不是替身);单 validator 超时帽 3s 计跳过;任何异常整体返 ''(fail-open 注入面——
647
+ * 绝不阻塞 prompt 渲染与门判定,R-02)。
648
+ * - 防应试头行(R-07/FR-01):清单头固定「已知失败项(非全部要求)」明示,防清单被当
649
+ * 待办清单应试打磨致清单外质量失查;尾行指路 gate 完整清单。
650
+ *
651
+ * 零副作用:只读不写(validator 全部只读函数),不碰 stages 定义与 outputStep 渲染路径。
652
+ *
653
+ * @param {{ stageName?: string, stepName?: string, cwd?: string, specBase?: string, changeName?: string }} args
654
+ * @returns {Promise<string>} 清单文本;无失败 / 无声明 / 异常 → ''
655
+ */
656
+ export async function renderPreflightFailures({ stageName, stepName, cwd, specBase, changeName } = {}) {
657
+ try {
658
+ const stageDef = stageRegistry[stageName]
659
+ const steps = (stageDef && Array.isArray(stageDef.steps)) ? stageDef.steps : []
660
+ const step = steps.find((s) => s && s.name === stepName)
661
+ const declared = (step && Array.isArray(step.preflightValidators)) ? step.preflightValidators : []
662
+ if (declared.length === 0) return ''
663
+ const changeDir = (specBase && changeName) ? join(specBase, 'changes', changeName) : null
664
+ const ctx = { changeDir, cwd, specBase, stageName, stepName, changeName }
665
+ // 并行只读快跑(R-02):validator 抛错 → Promise.all 整体 reject → 外层 catch 返 ''
666
+ const outcomes = await Promise.all(declared.map(async (name) => ({ name, errors: await runPreflightValidator(name, ctx) })))
667
+ // 聚合(声明序)+ 折行压平(gate failMessage 可能含换行,清单一项一行)
668
+ const items = []
669
+ for (const o of outcomes) {
670
+ for (const msg of o.errors) items.push(`- ${o.name}: ${String(msg).replace(/\s*\n\s*/g, ' ')}`)
671
+ }
672
+ if (items.length === 0) return ''
673
+ const shown = items.slice(0, PREFLIGHT_MAX_ITEMS)
674
+ const hidden = items.length - shown.length
675
+ return [
676
+ '⚠️ 已知失败项(非全部要求),清单外仍需按步骤说明自检:',
677
+ ...shown,
678
+ ...(hidden > 0 ? [`…另 ${hidden} 项见 gate`] : []),
679
+ `完整清单:sillyspec gate ${stageName} --json`,
680
+ ].join('\n')
681
+ } catch {
682
+ return '' // fail-open:任何异常静默不注(不阻 prompt 渲染)
683
+ }
684
+ }
685
+
686
+ /**
687
+ * shouldInjectFullContext —— 阶段注入账本分叉判定(D-002@v1 Phase 2 纯函数面)。
688
+ *
689
+ * 读 `<runtimeRoot>/prompt-inject-<changeName>.json` 账本({stages:{[stage]:{firstStep,digest,at}}}):
690
+ * - 文件不存在 / JSON 坏 / 无该 stage 记录 → { full: true }(首步语义:本阶段尚未注过全量)
691
+ * - 有记录 → { full: false, digest, firstStep }(同阶段后续步骤注摘要行的判定输入——
692
+ * 「本阶段上下文已于步骤 N 注入(digest 前 8 位)」)
693
+ * - 账本读/解析异常 → { full: true }(design 兼容策略:分叉失败回退每步全量现状)
694
+ *
695
+ * 纯读不写:账本写入(首步全量注入后 withFileLock 幂等落盘)归 task-02;archive 回收
696
+ * (pruneArchivedChangeRuntime 枚举登记)归 task-03。
697
+ *
698
+ * @param {{ changeName?: string, stageName?: string, runtimeRoot?: string }} args
699
+ * @returns {{ full: boolean, digest?: string, firstStep?: number }}
700
+ */
701
+ export function shouldInjectFullContext({ changeName, stageName, runtimeRoot } = {}) {
702
+ try {
703
+ if (!runtimeRoot || !changeName || !stageName) return { full: true }
704
+ const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
705
+ if (!existsSync(ledgerPath)) return { full: true }
706
+ const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'))
707
+ const rec = (ledger && ledger.stages && typeof ledger.stages === 'object') ? ledger.stages[stageName] : null
708
+ if (!rec || typeof rec !== 'object') return { full: true }
709
+ return { full: false, digest: rec.digest, firstStep: rec.firstStep }
710
+ } catch {
711
+ return { full: true }
712
+ }
713
+ }
714
+
715
+ /**
716
+ * 阶段注入摘要行渲染(task-02 接线,D-002@v1 Phase 2):shouldInjectFullContext 判 full=false
717
+ * 时的一行替代注入——同阶段后续步骤不再重注全量模块/scan 上下文,改指路(首步步骤号 + digest
718
+ * 前 8 位 + 可 Read 路径,R-03 摘要丢上下文风险的对冲:该读可读回)。firstStep/digest 缺失
719
+ * (账本记录不完整)给占位符不抛(渲染层永不因账本数据炸)。
720
+ *
721
+ * @param {{ firstStep?: number, digest?: string, specBase: string, projectName: string }} args
722
+ * @returns {string} 单行摘要(「> 」引语形态)
723
+ */
724
+ function renderPromptInjectSummaryLine({ firstStep, digest, specBase, projectName }) {
725
+ const moduleMapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
726
+ const scanDocsDir = join(specBase, 'docs', projectName, 'scan')
727
+ return `> 本阶段模块/scan 上下文已于步骤 ${firstStep ?? '?'} 注入(digest ${digest || 'unknown'});需要时 Read ${moduleMapPath}(模块索引)或 ${scanDocsDir}/ 下 scan 文档(ARCHITECTURE.md / _facts.md)`
728
+ }
729
+
730
+ /**
731
+ * 阶段注入账本落账(task-02 接线,D-002@v1 Phase 2 / R-06):首步全量注入后写
732
+ * `<runtimeRoot>/prompt-inject-<changeName>.json`({stages:{[stage]:{firstStep,digest,at}}},
733
+ * design 数据模型节)。withFileLock 串行化 read-modify-write(quicklog.js:40 先例——多会话
734
+ * 并行写同账本防丢更新);幂等:已有该 stage 记录不覆盖(并发首步/同步重渲染不改写首步锚点)。
735
+ * digest=全量注入内容 sha256 前 8 位(摘要行可核对的指纹)。异常上抛由调用方 catch 静默
736
+ * (fail-open:账本写失败 → 后续步仍判 full 全量注入,绝不阻塞渲染);archive 回收登记
737
+ * (pruneArchivedChangeRuntime 枚举补本文件名)归 task-03。
738
+ *
739
+ * @param {{ runtimeRoot: string, changeName: string, stageName: string, firstStep: number, digest: string }} args
740
+ */
741
+ async function recordPromptInjectLedger({ runtimeRoot, changeName, stageName, firstStep, digest }) {
742
+ const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
743
+ await withFileLock(ledgerPath + '.lock', () => {
744
+ let ledger = null
745
+ try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
746
+ if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) ledger = {}
747
+ if (!ledger.stages || typeof ledger.stages !== 'object' || Array.isArray(ledger.stages)) ledger.stages = {}
748
+ if (ledger.stages[stageName] && typeof ledger.stages[stageName] === 'object') return // 幂等:已有该 stage 记录不覆盖
749
+ ledger.stages[stageName] = { firstStep, digest, at: new Date().toISOString() }
750
+ mkdirSync(runtimeRoot, { recursive: true })
751
+ writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
752
+ })
753
+ }
754
+
453
755
  export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
454
756
  const step = steps[stepIndex]
455
757
  const total = steps.length
@@ -857,7 +1159,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
857
1159
  try {
858
1160
  const { appendKnowledgeHit } = await import('../knowledge-hits.js')
859
1161
  appendKnowledgeHit(join(frSpecBase, '.runtime'), {
860
- type: 'fr-inject', change: changeName, domains, count: entries.length,
1162
+ type: 'fr-inject', change: changeName, domains, count: entries.length, source: 'digest',
861
1163
  })
862
1164
  } catch { /* 遥测 fail-soft */ }
863
1165
  } catch (e) {
@@ -1069,7 +1371,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1069
1371
 
1070
1372
  // Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
1071
1373
  // (scanProfile 只在 scan 生效、change-risk-profile 只管 apply/verify,都不约束这些阶段的审查方式,
1072
- // 故按 plan_level / 变更文件数分级:self 当前 agent 自审,independent 强制独立子代理 + review.json)
1374
+ // 故按 ceremony_tier 风险定价分级——2026-09-18-ceremony-risk-pricing task-05 起 plan_level 降级为
1375
+ // 编排标签不再驱动仪式档:S0/S1 → self 轻仪,S2 → independent×1,S3 → independent 多轮;
1376
+ // {REVIEW_TIER} 注入值 = tier 值 + 追加 ceremony 档位菜单块(renderCeremonyTierInjection),
1377
+ // {REVIEW_TIER_REASON} 注入机制不动(reason 已含 ceremony 档与 blast 来源,task-02 契约))
1073
1378
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
1074
1379
  try {
1075
1380
  const { classifyReviewTier } = await import('../review-tier.js')
@@ -1086,6 +1391,12 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1086
1391
  }
1087
1392
  }
1088
1393
  const tier = classifyReviewTier({ planLevel, designPath })
1394
+ // ceremony 档位菜单(第三消费面,task-05):classifyReviewTier 双字段的 ceremonyTier 驱动
1395
+ // 仪式档渲染;force_tier 逃生阀只升不降(升向强制 S2/S3 时注入 tier 值随调 independent,
1396
+ // 与下方契约渲染/前序事实注入同用 effective 值,防 prompt 与注入块自相矛盾)
1397
+ const ceremonyInjection = renderCeremonyTierInjection({
1398
+ ceremonyTier: tier.ceremonyTier, tierValue: tier.tier, planLevel, cwd,
1399
+ })
1089
1400
  // reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
1090
1401
  // 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
1091
1402
  // generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
@@ -1112,13 +1423,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1112
1423
  // process.cwd 兜底会读错 local.yaml,2026-09-10 通道配置批次);tier=independent 时契约
1113
1424
  // 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
1114
1425
  const reviewChannelPriority = readReviewChannelPriority(cwd)
1115
- const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
1426
+ const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: ceremonyInjection.tierValue, channelPriority: reviewChannelPriority })
1116
1427
  // C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
1117
1428
  // plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
1118
1429
  // prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
1119
1430
  // 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
1120
1431
  let priorFactsMd = ''
1121
- if (tier.tier === 'independent') {
1432
+ if (ceremonyInjection.tierValue === 'independent') {
1122
1433
  try {
1123
1434
  const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
1124
1435
  const factLines = []
@@ -1147,18 +1458,32 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1147
1458
  // 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
1148
1459
  // 上一轮上下文)。
1149
1460
  try {
1150
- const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
1461
+ const priorRound = await collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
1151
1462
  if (priorRound) {
1152
1463
  const blockMd = renderPriorRoundFindingsMd(priorRound)
1153
1464
  priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
1154
1465
  }
1155
1466
  } catch {}
1467
+ // {REVIEW_MATERIALS} 机械组装(2026-09-19-review-material-cli-wiring,Gap 1 收口):CLI 半边
1468
+ // 素材(designDigest/fileList/硬约束/diff 摘要/热区/checklist)经 assembleStageReviewMaterials
1469
+ // 组包注入(specBase 用上方 tierSpecBase——块内唯一解析源,平台模式不漂);主代理点名半边
1470
+ // (五交叉点/plan 差量)留位,模板补位指引接手。占位符在场才组装(省无槽步骤的 git IO);
1471
+ // best-effort:失败空串(与占位符缺失同态,模板散文兜底)。
1472
+ let reviewMaterialsMd = ''
1473
+ if (promptText.includes('{REVIEW_MATERIALS}')) {
1474
+ try {
1475
+ const { assembleStageReviewMaterials } = await import('../review-material-pack.js')
1476
+ const matStage = stageName === 'brainstorm' ? 'grill-first' : stageName === 'plan' ? 'plan-review' : 'execute-qa'
1477
+ reviewMaterialsMd = await assembleStageReviewMaterials({ stage: matStage, cwd, changeName, specBase: tierSpecBase })
1478
+ } catch { /* 组装 best-effort:失败零注入 */ }
1479
+ }
1156
1480
  promptText = promptText
1157
- .split('{REVIEW_TIER}').join(tier.tier)
1481
+ .split('{REVIEW_TIER}').join(ceremonyInjection.tierValue + ceremonyInjection.menuMd)
1158
1482
  .split('{REVIEW_TIER_REASON}').join(tier.reason)
1159
1483
  .split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
1160
1484
  .split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
1161
1485
  .split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
1486
+ .split('{REVIEW_MATERIALS}').join(reviewMaterialsMd) // 三阶段材料包槽(CLI 组装注入,Gap 1 收口——此前恒空串靠主代理手组,机械保证断一半;主代理点名半边由模板补位指引接手)
1162
1487
  } catch (e) {
1163
1488
  // 降级 self,避免 prompt 残留占位符
1164
1489
  promptText = promptText
@@ -1167,6 +1492,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1167
1492
  .split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
1168
1493
  .split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
1169
1494
  .split('{PRIOR_REVIEW_FACTS}').join('')
1495
+ .split('{REVIEW_MATERIALS}').join('') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
1170
1496
  }
1171
1497
  }
1172
1498
 
@@ -1379,22 +1705,76 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1379
1705
  promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
1380
1706
  }
1381
1707
 
1382
- // 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
1708
+ // 注入模块上下文(brainstorm/plan/execute 阶段 + quick 首步「理解任务」——刀①:quick
1383
1709
  // step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
1710
+ // ── 阶段感知注入分叉(2026-09-18-preflight-slimming task-02,D-002@v1 Phase 2)──
1711
+ // 三主阶段由「每步全注」改「每阶段首步全量、后续摘要行」:shouldInjectFullContext(task-01)
1712
+ // 读 .runtime/prompt-inject-<change>.json 账本判定——full=真注全量(现状)并在注入后
1713
+ // recordPromptInjectLedger 落账(withFileLock 幂等,R-06);full=假改注一行摘要(首步步骤号 +
1714
+ // digest 前 8 位 + 可 Read 路径,R-03)。quick 不分叉:仅首步注入,本无重注面(现状逐字保留)。
1715
+ // fail-open 红线(design 兼容策略):分叉判定/账本读写任何异常 → 回退全量注入(现状),
1716
+ // 绝不阻塞渲染。
1384
1717
  const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
1385
- if ((['brainstorm', 'plan', 'execute'].includes(stageName) || quickFirstStep) && projectName) {
1718
+ const mainStageModuleInject = ['brainstorm', 'plan', 'execute'].includes(stageName)
1719
+ if ((mainStageModuleInject || quickFirstStep) && projectName) {
1386
1720
  const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1387
1721
  const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
1388
1722
  if (moduleIndex && Object.keys(moduleIndex).length > 0) {
1389
- // 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
1390
- // changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
1391
- let taskDesc = step.prompt || changeName || ''
1392
- if (stageName === 'quick') {
1393
- taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
1723
+ // runtimeRoot 与 {EXECUTE_RUN_ID}/{REVIEW_TIER} 段同源解析(resolveRuntimeRoot(platformOpts, specBase))
1724
+ let injectFork = { full: true } // 缺省=现状全量(quick / 无 changeName / 分叉判定异常)
1725
+ if (mainStageModuleInject && changeName) {
1726
+ try {
1727
+ injectFork = shouldInjectFullContext({
1728
+ changeName, stageName,
1729
+ runtimeRoot: resolveRuntimeRoot(platformOpts, effectiveSpecBase),
1730
+ })
1731
+ } catch { injectFork = { full: true } } // fail-open:判定异常回退全量(现状)
1394
1732
  }
1395
- const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
1396
- if (injection) {
1397
- promptText = injection + '\n' + promptText
1733
+ if (!injectFork.full) {
1734
+ // 同阶段后续步骤:不重注全量,改摘要行(digest 前 8 位 + 可 Read 路径)
1735
+ promptText = renderPromptInjectSummaryLine({
1736
+ firstStep: injectFork.firstStep, digest: injectFork.digest,
1737
+ specBase: effectiveSpecBase, projectName,
1738
+ }) + '\n' + promptText
1739
+ } else {
1740
+ // 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
1741
+ // changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
1742
+ let taskDesc = step.prompt || changeName || ''
1743
+ if (stageName === 'quick') {
1744
+ taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
1745
+ }
1746
+ const injRes = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName) || { text: '', frModules: [] }
1747
+ const injection = injRes.text
1748
+ if (injection) {
1749
+ promptText = injection + '\n' + promptText
1750
+ // fr-inject 遥测(module-inject 源,ql-20260919-002):仅 count>0 模块发事件(防 jsonl
1751
+ // 堆 count-0 噪音);type 不变+source 归因(新 type 会静默漏过 knowledge-stats 的 if 链
1752
+ // 缩水 D-006 总数——source 字段保口径)。与 step8 digest 侧可能对同一变更各发一次,
1753
+ // frInjectChanges 按 change 去重不受双发影响。fail-soft。
1754
+ if (injRes.frModules && injRes.frModules.length > 0 && changeName) {
1755
+ try {
1756
+ for (const fm of injRes.frModules) {
1757
+ appendKnowledgeHit(join(effectiveSpecBase, '.runtime'), {
1758
+ type: 'fr-inject', change: changeName, domains: [fm.id], count: fm.count, source: 'module-inject',
1759
+ })
1760
+ }
1761
+ } catch { /* 遥测 fail-soft */ }
1762
+ }
1763
+ // 首步全量注入后落账:本阶段首个非空注入即「首步」锚点(记 firstStep/digest/at——后续
1764
+ // 步骤摘要行的判定输入)。只在真注入非空内容时落账——空注入不占首步位(防摘要行
1765
+ // 「已于步骤 N 注入」指向一个实际没注入任何模块内容的步骤,丢真上下文)。fail-open:
1766
+ // 落账异常静默(语义=后续步仍全量注入),绝不阻塞渲染。
1767
+ if (mainStageModuleInject && changeName) {
1768
+ try {
1769
+ await recordPromptInjectLedger({
1770
+ runtimeRoot: resolveRuntimeRoot(platformOpts, effectiveSpecBase),
1771
+ changeName, stageName,
1772
+ firstStep: stepIndex + 1,
1773
+ digest: createHash('sha256').update(injection, 'utf8').digest('hex').slice(0, 8),
1774
+ })
1775
+ } catch { /* fail-open:账本写失败 → 后续步仍全量注入(现状),不阻塞渲染 */ }
1776
+ }
1777
+ }
1398
1778
  }
1399
1779
  }
1400
1780
  }
@@ -1478,6 +1858,28 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1478
1858
  }
1479
1859
  }
1480
1860
 
1861
+ // ── {PREFLIGHT_FAILURES} 前置失败清单接线(2026-09-18-preflight-slimming task-02,D-001@v1 Phase 1)──
1862
+ // renderPreflightFailures(task-01 纯函数)从 stageRegistry 步骤定义读 preflightValidators
1863
+ // 声明(三 stages 声明统一归 task-04)只读快跑本步骤 --done 将消费的 validator 子集:
1864
+ // 未声明/无失败/超时/异常 → ''。注入位(计划锚点):步骤说明之后、门禁提示(铁律/完成后
1865
+ // 执行/gate 预检)之前——非空且模板无字面占位符时追加到 promptText 末尾;模板含字面
1866
+ // {PREFLIGHT_FAILURES} 时按既有占位符替换机制原位替换(空串替换零残留——「空串时占位符
1867
+ // 零出现」)。空串零注入:声明落地(task-04)前恒 ''——旧 prompt 逐字节不变(design 兼容
1868
+ // 策略第一条,knowledge-inject A/B 双渲染字节一致金丝雀守护)。接线层再包一层 try:
1869
+ // 任何异常静默不注不阻(fail-open 注入面,plan 全局硬约束 2)。
1870
+ let preflightFailuresMd = ''
1871
+ try {
1872
+ const pfSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1873
+ preflightFailuresMd = await renderPreflightFailures({
1874
+ stageName, stepName: step.name, cwd, specBase: pfSpecBase, changeName,
1875
+ })
1876
+ } catch { preflightFailuresMd = '' }
1877
+ if (promptText.includes('{PREFLIGHT_FAILURES}')) {
1878
+ promptText = promptText.split('{PREFLIGHT_FAILURES}').join(preflightFailuresMd)
1879
+ } else if (preflightFailuresMd) {
1880
+ promptText = `${promptText}\n\n${preflightFailuresMd}`
1881
+ }
1882
+
1481
1883
  console.log(promptText)
1482
1884
  // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
1483
1885
  // 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
package/src/run/shared.js CHANGED
@@ -23,6 +23,7 @@ import { createHash } from 'node:crypto'
23
23
  // 分级门禁画像信号层(2026-09-14-quick-exit-tiered-gates task-01 / FR-02):纯函数零 IO,
24
24
  // 仅依赖 change-risk-profile,无环——auditQuickCompletion 静态 import 安全(task-02 接线)。
25
25
  import { computeGateProfile, resolveGateThresholds } from '../quick-gate-profile.js'
26
+ import { loadSpanRiskPatterns } from '../span-risk-surface.js'
26
27
  export { safeGit, unquoteGitPath }
27
28
 
28
29
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -1771,10 +1772,15 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
1771
1772
  } catch { /* 坏 YAML → 纯默认阈值 */ }
1772
1773
  }
1773
1774
  const gateFiles = result.changedFiles.filter(f => !isQuicklogFileLineNoise(f, guard.linkedChanges))
1775
+ // span 声明面装载(2026-09-19-span-risk-pattern-migration task-03):与 loadQuickModuleIndex 同
1776
+ // specBase/projectName 上下文(_module-map.yaml 顶层 span_risk 段)→ gateOpts.riskTable;
1777
+ // 无声明项目 → 空表(riskTable 已默认 [],此处显式装载与其他声明面入参同风格,D-003)
1778
+ const riskTable = loadSpanRiskPatterns({ specBase, project: projectName })
1774
1779
  const gateOpts = {
1775
1780
  ...(thresholds ? { thresholds } : {}),
1776
1781
  ...(noDocs ? { noDocs: true } : {}),
1777
1782
  ...(Array.isArray(fileNotes) && fileNotes.length > 0 ? { fileNotes } : {}),
1783
+ riskTable,
1778
1784
  }
1779
1785
  const moduleIndex = await loadQuickModuleIndex(specBase, projectName)
1780
1786
  result.gateProfile = computeGateProfile(gateFiles, moduleIndex, gateOpts)
@@ -1783,6 +1789,36 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
1783
1789
  const attrGateFiles = gateFiles.filter(f => !undeclaredNorm.has(String(f).replace(/\\/g, '/')))
1784
1790
  result.gateProfile.checks.docClaim = computeGateProfile(attrGateFiles, moduleIndex, gateOpts).checks.docClaim
1785
1791
  }
1792
+ // ── FR 写面腐烂 suspect 遥测(钩子 #1,L3 裁决数据,advisory 零阻断)──
1793
+ // quick 永不归档 → FR 索引对 quick 改动无感知(L1 显式划出覆盖面),改掉 FR 覆盖行为后
1794
+ // 条目仍标 active——账本从 quick 路径腐烂且今天完全不可见。此钩子把它量出来:changedFiles
1795
+ // 经 moduleIndex 路径匹配得触达域(与 computeGateProfile 同 join),域内有 active FR 即记
1796
+ // fr-rot-suspect 事件+一行 warn(只提醒不阻断——看得见约束≠声明取代,L3 才有裁决权)。
1797
+ // 盲区预登(评审要求):①unmapped 文件静默漏 join(新路径未录 map 即无信号)②map 文件
1798
+ // 清单自身漂移——**无信号 ≠ 无腐烂**。fail-open:异常跳过不碰审计结论。
1799
+ try {
1800
+ const { readActiveFrDigest } = await import('../fr-index.js')
1801
+ const { appendKnowledgeHit } = await import('../knowledge-hits.js')
1802
+ const gateFilesNorm = gateFiles.map(f => String(f).replace(/\\/g, '/'))
1803
+ const touchedDomains = new Set()
1804
+ for (const [modId, mod] of Object.entries(moduleIndex || {})) {
1805
+ const prefixes = [...(mod && Array.isArray(mod.paths) ? mod.paths : []), ...(mod && Array.isArray(mod.core_files) ? mod.core_files : [])]
1806
+ for (const raw of prefixes) {
1807
+ const p = String(raw).replace(/\\/g, '/').replace(/\/+$/, '')
1808
+ if (p && gateFilesNorm.some(f => f === p || f.startsWith(p + '/'))) { touchedDomains.add(modId); break }
1809
+ }
1810
+ }
1811
+ if (touchedDomains.size > 0) {
1812
+ const frs = readActiveFrDigest(join(specBase, 'knowledge'), [...touchedDomains])
1813
+ if (frs.length > 0) {
1814
+ appendKnowledgeHit(join(specBase, '.runtime'), {
1815
+ type: 'fr-rot-suspect', change: guard.changeName || null,
1816
+ domains: [...touchedDomains], count: frs.length, source: 'quick-done',
1817
+ })
1818
+ console.warn(`⚠️ [FR 腐烂 suspect·advisory] 本次 quick 触达 ${[...touchedDomains].join('、')} 域的 ${frs.length} 条 active FR——quick 不走归档,若改动影响这些行为,索引不会自动 supersede(L3 声明义务;盲区:unmapped 文件/map 漂移不报警)`)
1819
+ }
1820
+ }
1821
+ } catch { /* fail-open:遥测异常不影响审计 */ }
1786
1822
  } catch { /* 画像 fail-open:异常只跳过(gateProfile 保持 null,D-003 不碰 status) */ }
1787
1823
 
1788
1824
  // --confirm 模式:展示 diff 并等待确认