sillyspec 3.28.13 → 3.29.1

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 (47) 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 +22 -1
  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/redlines.js +195 -0
  17. package/src/review-dispatch.js +473 -1
  18. package/src/review-material-pack.js +265 -0
  19. package/src/review-tier.js +185 -41
  20. package/src/run/archive-distill.js +6 -0
  21. package/src/run/command.js +158 -87
  22. package/src/run/complete-handlers.js +82 -1
  23. package/src/run/complete.js +90 -3
  24. package/src/run/gate-snapshot.js +30 -1
  25. package/src/run/gates.js +301 -19
  26. package/src/run/prompt.js +423 -21
  27. package/src/run/quick-audit.js +12 -2
  28. package/src/run/shared.js +36 -0
  29. package/src/run/verify-quality-scan.js +15 -7
  30. package/src/scope-audit.js +8 -1
  31. package/src/span-risk-surface.js +127 -0
  32. package/src/stage-contract-spec.js +2 -2
  33. package/src/stage-contract.js +102 -81
  34. package/src/stage-review.js +26 -2
  35. package/src/stages/brainstorm-auto.js +1 -1
  36. package/src/stages/brainstorm.js +35 -19
  37. package/src/stages/execute.js +11 -3
  38. package/src/stages/plan.js +45 -24
  39. package/src/stages/scan.js +1 -1
  40. package/src/stages/verify.js +7 -7
  41. package/src/taskcard.js +3 -0
  42. package/src/verify-postcheck.js +375 -20
  43. package/src/verify-probes.js +194 -10
  44. package/src/worktree-apply.js +1 -1
  45. package/src/worktree.js +1 -1
  46. package/templates/prompts/taskcard-rules.md +1 -0
  47. package/templates/prompts/verify-probes.md +1 -1
@@ -4,72 +4,31 @@
4
4
  * 根据变更涉及的文件类型、关键词、git diff、brainstorm 产物,
5
5
  * 判定 5 级风险(doc-only / unit-sufficient / contract-required /
6
6
  * integration-critical / deployment-critical),产出门控验证需求。
7
+ *
8
+ * 2026-09-19-ceremony-pricing-five-cuts:判级唯一入口 resolveChangeRisk(危险面=项目
9
+ * _module-map.yaml 顶层 blast 段路径声明 × 变更文件,D-008;explicit frontmatter 只压仪式档
10
+ * 不豁免证据,D-009)。散文词表判级(detectChangeRisk 及词表/否定抑制/枚举继承机器)已整体
11
+ * 退役删除。RISK_TO_TIER 五级词→档位映射表迁入本文件(ceremony-tier re-export 保持既有
12
+ * import 面——依赖方向 ceremony-tier→本文件已有先例)。证据门判据(VERIFICATION_NEEDS /
13
+ * checkIntegrationEvidence / auditRuntimeReceipt)原样保留;span 轴路径模式已迁项目声明面
14
+ * (_module-map.yaml 顶层 span_risk 段,装载 src/span-risk-surface.js——2026-09-19-
15
+ * span-risk-pattern-migration 硬退役,不留 legacy 别名)。
7
16
  */
8
17
  import { readFileSync, statSync } from 'fs'
9
18
  import { join, isAbsolute } from 'path'
19
+ import { resolveBlastSurfaces } from './blast-surface.js'
10
20
 
11
- // ============ 向后兼容:旧的 INTEGRATION_CRITICAL_PATTERNS ============
12
-
13
- const INTEGRATION_CRITICAL_PATTERNS = [
14
- /\bdaemon\b/i,
15
- /\bbackend\b/i,
16
- /\bclient.*api\b/i,
17
- /\bgrpc\b/i,
18
- /\bwebsocket\b/i,
19
- /\bhttp.*client\b/i,
20
- /\bsession\b/i,
21
- /\blease\b/i,
22
- /\bagent.?run\b/i,
23
- /\blifecycle\b/i,
24
- /\bstate.?transition\b/i,
25
- /\bclaim\b/i,
26
- /\bheartbeat\b/i,
27
- /\bcross.?process\b/i,
28
- /\bipc\b/i,
29
- /\bmessage.?queue\b/i,
30
- /\bpub.?sub\b/i,
31
- /\bcli\.ts\b/i,
32
- /\bmain\.ts\b/i,
33
- /\bentrypoint\b/i,
34
- /\bserver\.(js|ts)\b/i,
35
- /\bbootstrap\b/i,
36
- /\bdockerfile\b/i,
37
- /\bdocker.?compose\b/i,
38
- ]
39
-
40
- const INTEGRATION_FILE_PATTERNS = [
41
- /daemon/i,
42
- /session.?manager/i,
43
- /agent.?run/i,
44
- /lifecycle/i,
45
- /state.?machine/i,
46
- /lease/i,
47
- /cli\.(js|ts)$/,
48
- /main\.(js|ts)$/,
49
- /server\.(js|ts)$/,
50
- /bootstrap/i,
51
- /startup/i,
52
- ]
21
+ // ============(已退役)词表散文判级 ============
53
22
 
54
- // ============ quick 出口门禁:路径模式风险表(2026-09-14-quick-exit-tiered-gates task-01) ============
55
- //
56
- // quick 侧「风险特征命中」的单一数据源(design D-004@v2:v1 仅路径模式——不做 diff 关键词
57
- // 扫描、不加 git 子进程,R-04 零子进程承诺;diff 维度待真实需求另立变更)。与上方 verify 侧
58
- // INTEGRATION_FILE_PATTERNS 语义不同(那张表收跨进程/集成域、供 detectChangeRisk 判级),本表
59
- // 收 quick 实证踩坑域(R-03 从窄收录:auth/permission/billing/migration/锁/调度),供
60
- // quick-gate-profile.js 的 computeGateProfile 经 opts.riskTable 默认引用——命中即画像 L2 +
61
- // checks.runtimeEvidence='required'。detectChangeRisk 判级逻辑不消费本表(verify 侧行为零变化)。
62
- //
63
- // 匹配口径:POSIX 路径、大小写不敏感、段边界锚定——前界 (^|[/_-]) + 后界 (?=[/._-]|$),
64
- // 防 author/booking/lockfile 类子串假阳(R-03);pattern 为稳定域标识(riskHits 审计输出用)。
65
- export const QUICK_RISK_PATH_PATTERNS = [
66
- { pattern: 'auth', re: /(?:^|[/_-])(?:oauth2?|auth(?:orization|entication|enticator)?)(?=[/._-]|$)/i },
67
- { pattern: 'permission', re: /(?:^|[/_-])permissions?(?=[/._-]|$)/i },
68
- { pattern: 'billing', re: /(?:^|[/_-])billing(?=[/._-]|$)/i },
69
- { pattern: 'migration', re: /(?:^|[/_-])(?:migrations?|migrate)(?=[/._-]|$)/i },
70
- { pattern: 'lock', re: /(?:^|[/_-])(?:locks?|mutex(?:es)?)(?=[/._-]|$)/i },
71
- { pattern: 'scheduling', re: /(?:^|[/_-])(?:schedul(?:er|ing)|cron|jobs?)(?=[/._-]|$)/i },
72
- ]
23
+ // 2026-09-19-ceremony-pricing-five-cuts task-03 删除收口:INTEGRATION_CRITICAL_PATTERNS /
24
+ // INTEGRATION_FILE_PATTERNS 全宇宙词表与 detectChangeRisk 散文扫描整体退役(D-008:撞词≠危险、
25
+ // 自指陷阱——判级唯一入口是 resolveChangeRisk 声明面口径;知识库 conventions.md「判级/定价/门禁
26
+ // 输入必须项目声明」条目为口径真相源)。
27
+ // 2026-09-19-span-risk-pattern-migration task-03 同款删除收口:quick 出口门禁的六域硬编码
28
+ // 路径模式风险表(auth/permission/billing/migration/lock/scheduling,2026-09-14-quick-exit-
29
+ // tiered-gates task-01 引入)退役——span 模式维与 quick 画像 riskTable 唯一输入源改为项目声明
30
+ // _module-map.yaml 顶层 span_risk 段(装载 src/span-risk-surface.js;匹配口径段边界锚定逐字
31
+ // 继承)。无声明项目 → 空表(维度关闭,禁回退内置表——与 blast 未配置禁回退同款取舍)。
73
32
 
74
33
  /**
75
34
  * task 级「端到端/deployment-critical」文本判定(execute 批量完成 autoCheckPlanFromReviews 用)。
@@ -138,88 +97,27 @@ export const VERIFICATION_NEEDS = {
138
97
  },
139
98
  }
140
99
 
141
- /** 风险分级的判定来源(命中关键词来源 = design.md / plan.md 内容,非改动文件本身) */
100
+ /** 风险分级的判定来源(2026-09-19-ceremony-pricing-five-cuts task-03 改声明面口径) */
142
101
  export const RISK_LEVEL_CAUSES = {
143
102
  'deployment-critical':
144
- 'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
145
- '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design.md frontmatter 用 risk_level 显式声明真实等级(如 unit-sufficient)覆盖——不要靠删措辞绕门控,危险链路该有真实启动证据。',
103
+ '变更文件面命中项目声明危险面(_module-map.yaml 顶层 blast 段)且判级为顶档。' +
104
+ '这是按「文件路径 × 项目声明」判定的——若认为该路径不该定此价:改 map 声明(git 可见);危险链路该有真实启动证据。',
146
105
  'integration-critical':
147
- 'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。' +
148
- '关键词前方同句有否定提示(不/未/无/避免…)的命中已被抑制不参与判级——若本变更实际涉及跨进程/状态机改动,请在 design.md frontmatter 用 risk_level 声明真实等级。',
149
- 'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
150
- 'explicit': 'design.md frontmatter 的 risk_level 显式声明(覆盖关键词判级)。',
106
+ '变更文件面命中项目声明危险面的 evidence:true 路径(需要真实集成证据,D-009)。' +
107
+ 'frontmatter risk_level 只压仪式档、不豁免证据要求——证据豁免=改 map 的 evidence 声明(git 可见)。',
108
+ 'contract-required': '变更文件面命中项目声明的 S2 档危险面(跨模块契约域)。',
109
+ 'explicit': 'design.md frontmatter 的 risk_level 显式声明(压仪式档;不豁免 evidence 要求)。',
151
110
  }
152
111
 
153
- /** design.md frontmatter 可显式声明的合法 risk_level 值(与 detectChangeRisk 判级结果同集合) */
112
+ /** design.md frontmatter 可显式声明的合法 risk_level 值(存量五级词兼容集合) */
154
113
  const RISK_LEVELS = ['doc-only', 'unit-sufficient', 'contract-required', 'integration-critical', 'deployment-critical']
155
114
 
156
- // ============ 同句否定抑制(坑 risk-negation-blindness) ============
157
- //
158
- // detectChangeRisk 原为全文 blob 字面命中:design 写「本次不新增 daemon 协议」仍判
159
- // integration-critical、强制全套集成证据,对无状态后端变更误伤(用户实证 2026-08-24)。
160
- // 机械抑制规则(不做事语义理解,三段判定):
161
- // ① 子句切分:按 句号/分号/逗号/换行 切子句,否定只在同子句内生效——「不改动 A。新增 B」
162
- // 的 B 不继承否定;
163
- // ② 直接否定:命中位置前方 NEGATION_WINDOW 字符内出现否定提示 → 抑制;
164
- // ③ 枚举继承:「不改动 daemon / session / lifecycle」中后位关键词与前位被抑制关键词之间
165
- // 只隔枚举连接符(空格 / 、 , 和 与 及 and or)→ 继承抑制——纯距离窗口盖不住长枚举,
166
- // 而「不涉及 daemon 的情况下重构 session」的间隔是普通文字,不继承(session 正常命中)。
167
- // 同一关键词的**全部**命中都被抑制才不参与判级(任一处未抑制命中仍算数)。降级不静默:
168
- // design/verify 两道 gate 会把被抑制词打进 warning(可审计),design.md frontmatter
169
- // risk_level 仍是双向权威覆盖通道(声明优先级最高,行为不变)。
170
- //
171
- // 否定提示词排除假阳:不同/不断/不久/不时/不仅(「不」的非否定合成词)、无状态/无法
172
- // (「无状态后端」是常见肯定表述)、非常/非空/非法(「非空校验 session」是在改 session)。
173
- const NEGATION_CUES = /不(?!同|断|久|时|仅|妨)|未|无(?!状态|法)|非(?!常|空|法)|避免|排除|无需|不再|no\s+new|not\b|don'?t|without|avoid/i
174
- const NEGATION_WINDOW = 16
175
- const CLAUSE_SPLIT_RE = /[。;;,,\n]/
176
- const ENUM_GAP_RE = /^[\s/|、,,]*(?:(?:和|与|及|and|or)[\s/|、,,]*)*$/i
177
-
178
- /**
179
- * 单行内统计各关键词命中(同子句否定窗口 + 枚举继承)。
180
- * @returns {Map<string, {kept: number, suppressed: number}>} trigger 原文 → 命中统计
181
- */
182
- function collectLineTriggerStats(line, patterns, stats = new Map()) {
183
- for (const pattern of patterns) {
184
- const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g')
185
- let m
186
- while ((m = re.exec(line)) !== null) {
187
- if (m[0].length === 0) { re.lastIndex++; continue }
188
- // 行内绝对偏移 → 子句内相对偏移(子句内可能有多命中,逐一判定)
189
- const clauseStart = line.slice(0, m.index).split(CLAUSE_SPLIT_RE).pop().length
190
- const clauseStartAbs = m.index - clauseStart
191
- const clause = line.slice(clauseStartAbs).split(CLAUSE_SPLIT_RE)[0]
192
- const relIndex = m.index - clauseStartAbs
193
- const prefix = clause.slice(0, relIndex)
194
- let negated = NEGATION_CUES.test(prefix.slice(-NEGATION_WINDOW))
195
- if (!negated && prefix.length > 0) {
196
- // 枚举继承:查同子句内更早的关键词命中,且间隔只有枚举连接符
197
- for (const other of patterns) {
198
- const ore = new RegExp(other.source, other.flags.includes('g') ? other.flags : other.flags + 'g')
199
- let om
200
- while ((om = ore.exec(clause)) !== null) {
201
- if (om[0].length === 0) { ore.lastIndex++; continue }
202
- if (om.index >= relIndex) break
203
- const gap = clause.slice(om.index + om[0].length, relIndex)
204
- const prevSuppressed = NEGATION_CUES.test(clause.slice(0, om.index).slice(-NEGATION_WINDOW))
205
- if (prevSuppressed && ENUM_GAP_RE.test(gap)) { negated = true; break }
206
- }
207
- if (negated) break
208
- }
209
- }
210
- const stat = stats.get(m[0]) || { kept: 0, suppressed: 0 }
211
- if (negated) stat.suppressed++
212
- else stat.kept++
213
- stats.set(m[0], stat)
214
- }
215
- }
216
- return stats
217
- }
218
-
219
115
  /**
220
116
  * 从 design.md 顶部 frontmatter 提取显式 risk_level 声明。
221
117
  * 只认文档最开头 `---\n...\n---` 块内的 `risk_level: <level>` 单行(与 plan.md 的 plan_level 同款解析),
222
118
  * 不扫正文——避免正文里讨论 risk_level 措辞时被误当声明。返回合法 level 或 null。
119
+ * 2026-09-19-ceremony-pricing-five-cuts task-03 删除收口后保留:声明面判级 explicit 通道的
120
+ * 唯一提取器(stage-contract / review-tier / verify-quality-scan / run-gates 消费)。
223
121
  */
224
122
  export function extractExplicitRiskLevel(designContent = '') {
225
123
  const fm = designContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
@@ -230,75 +128,50 @@ export function extractExplicitRiskLevel(designContent = '') {
230
128
  return RISK_LEVELS.includes(value) ? value : null
231
129
  }
232
130
 
233
- export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
234
- const triggers = []
235
-
236
- // ── 显式豁免优先:design.md frontmatter 声明 risk_level → 以声明为准,跳过关键词判级 ──
237
- // 历史教训:detectChangeRisk 是机械字面匹配——design 写「本次不改动 daemon/session」曾命中关键词
238
- // 被误判 integration/deployment-critical,强制要全套集成证据,对无状态后端变更误伤。现给两条通道:
239
- // ① 同句否定抑制(上方 NEGATION_CUES,机械可审计、只认「关键词前方否定窗口」不做语义理解);
240
- // ② frontmatter 显式声明(显式、诚实、可审计,落在 design frontmatter + verify-result)——本分支。
241
- // 声明仍走门控(结论 FAIL 仍拦),但豁免级(unit-sufficient 等)不再强制集成证据。
242
- const explicit = extractExplicitRiskLevel(designContent)
243
- if (explicit) {
244
- const requiredVerification =
245
- explicit === 'deployment-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'real_startup_once'] :
246
- explicit === 'integration-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion'] :
247
- explicit === 'contract-required' ? ['unit_tests', 'contract_tests'] :
248
- explicit === 'doc-only' ? ['static_check'] :
249
- ['unit_tests']
250
- return { level: explicit, triggers: ['risk_level (explicit)'], suppressedTriggers: [], requiredVerification, explicit: true }
251
- }
252
-
253
- const combined = [designContent, planContent].join('\n')
254
-
255
- // 逐行收集命中:同句否定窗口内的命中进 suppressed(全部命中均被抑制才剔除该 trigger)
256
- const stats = new Map()
257
- for (const line of combined.split('\n')) {
258
- collectLineTriggerStats(line, INTEGRATION_CRITICAL_PATTERNS, stats)
259
- }
260
- const suppressedTriggers = []
261
- for (const [t, stat] of stats) {
262
- if (stat.kept > 0) triggers.push(t)
263
- else suppressedTriggers.push(t)
264
- }
265
-
266
- for (const file of changedFiles) {
267
- for (const pattern of INTEGRATION_FILE_PATTERNS) {
268
- if (pattern.test(file)) {
269
- pattern.lastIndex = 0
270
- const match = file.match(pattern)
271
- if (match && !triggers.includes(match[0])) triggers.push(match[0])
272
- }
273
- }
274
- }
275
-
276
- if (triggers.length === 0) {
277
- // 关键词命中全被否定语境抑制(或本就无命中)→ doc-only;抑制词随 suppressedTriggers 上报可审计
278
- return { level: 'doc-only', triggers: [], suppressedTriggers, requiredVerification: ['static_check'] }
279
- }
280
-
281
- const deploymentTrigger = triggers.some(t => /cli\.ts|main\.ts|server\.(js|ts)|bootstrap|entrypoint/i.test(t))
282
- const lifecycleTrigger = triggers.some(t => /session|lease|agent.?run|lifecycle|state.?transition|claim|heartbeat/i.test(t))
283
- const crossProcessTrigger = triggers.some(t => /daemon|backend|client.*api|grpc|websocket|cross.?process|ipc|message.?queue/i.test(t))
284
-
285
- let level
286
- const requiredVerification = ['unit_tests']
131
+ /**
132
+ * 五级风险词 → 仪式档位映射(integration 与 deployment 同归 S3:顶档仪式不分家)。
133
+ * 2026-09-19-ceremony-pricing-five-cuts task-02 自 ceremony-tier.js 迁入(判级域归属本文件,
134
+ * ceremony-tier re-export 保持既有 import 面不变);resolveChangeRisk 的 explicitRiskLevel
135
+ * 兼容通道消费本表(存量 frontmatter 五级词)。
136
+ */
137
+ export const RISK_TO_TIER = {
138
+ 'doc-only': 'S0',
139
+ 'unit-sufficient': 'S1',
140
+ 'contract-required': 'S2',
141
+ 'integration-critical': 'S3',
142
+ 'deployment-critical': 'S3',
143
+ }
287
144
 
288
- if (deploymentTrigger) {
289
- level = 'deployment-critical'
290
- requiredVerification.push('contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'real_startup_once')
291
- } else if (lifecycleTrigger || crossProcessTrigger) {
292
- level = 'integration-critical'
293
- requiredVerification.push('contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion')
294
- } else if (triggers.some(t => /api|client|contract|dto/i.test(t))) {
295
- level = 'contract-required'
296
- requiredVerification.push('contract_tests')
297
- } else {
298
- level = 'unit-sufficient'
299
- }
145
+ // ============ 声明面判级(2026-09-19-ceremony-pricing-five-cuts task-02 / D-008 / D-009)============
146
+ //
147
+ // 新判级入口:files × 项目声明危险面(loadBlastDeclarations 装载 map blast 段 + local 只升合并)
148
+ // → { tier, evidenceRequired, explicit, hitPrefixes, requiredVerification }。
149
+ // tier:命中声明面最高档(无命中 S1——未配置项目禁回退词表,D-008);explicitRiskLevel
150
+ // (frontmatter 五级词经 RISK_TO_TIER 存量兼容)存在时覆盖 tier(人工通道升/降均尊重),
151
+ // **不豁免 evidenceRequired**(D-009:证据豁免=改 map,git 可见——收掉旧显式短路连证据门
152
+ // 一起免的懒 agent 洞)。
153
+ // evidenceRequired → requiredVerification 取现行 integration-critical 组原样(证据门判据零改动)。
300
154
 
301
- return { level, triggers, suppressedTriggers, requiredVerification }
155
+ /**
156
+ * 声明面判级(纯函数)。
157
+ * @param {{ files?: string[], blastDeclarations?: Array<{prefixes: string[], tier: string, evidence?: boolean}>, explicitRiskLevel?: string|null }} opts
158
+ * @returns {{ tier: 'S0'|'S1'|'S2'|'S3', level: string, evidenceRequired: boolean, explicit: boolean, hitPrefixes: string[], requiredVerification: string[] }}
159
+ * level=五级词兼容字段(存量消费面零改动):evidenceRequired → 'integration-critical';
160
+ * 否则 tier 反查(S0 doc-only / S1 unit-sufficient / S2 contract-required / S3 integration-critical)。
161
+ */
162
+ const TIER_TO_COMPAT_LEVEL = { S0: 'doc-only', S1: 'unit-sufficient', S2: 'contract-required', S3: 'integration-critical' }
163
+
164
+ export function resolveChangeRisk({ files = [], blastDeclarations = [], explicitRiskLevel = null } = {}) {
165
+ const surfaces = resolveBlastSurfaces(files, blastDeclarations)
166
+ const level = typeof explicitRiskLevel === 'string' ? explicitRiskLevel.trim().toLowerCase() : ''
167
+ const explicitTier = level ? RISK_TO_TIER[level] : undefined
168
+ const tier = explicitTier ? explicitTier : surfaces.tier
169
+ const evidenceRequired = surfaces.evidence
170
+ const requiredVerification = evidenceRequired
171
+ ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion']
172
+ : ['unit_tests']
173
+ const compatLevel = evidenceRequired ? 'integration-critical' : TIER_TO_COMPAT_LEVEL[tier]
174
+ return { tier, level: compatLevel, evidenceRequired, explicit: Boolean(explicitTier), hitPrefixes: surfaces.hitPrefixes, requiredVerification }
302
175
  }
303
176
 
304
177
  // ============ 回执来源分类打标(X-10 / D-002 / D-006,2026-09-17-pass-cap-semantics task-02) ============
@@ -156,7 +156,7 @@ export const LOCAL_YAML_SCHEMA = {
156
156
  {
157
157
  id: 'gate_snapshot',
158
158
  title: '门禁隔离快照 copy 面',
159
- note: '门禁快照(gate-snapshot)= HEAD worktree + 会话文件 overlay + 环境目录 junction——gitignored 生成物(api-types/generated 类)不进 HEAD 也不在会话集,快照内 lint/test 环境性假败(2026-09-16 驾驭小结④,friction5-hardening R4 / D-002@v1)。按本清单在快照构建期从主仓 junction 链接补齐(失败回退复制);快照内 overlay 已覆盖的同名路径跳过(本变更最新态优先)。⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。',
159
+ note: '门禁快照(gate-snapshot)= HEAD worktree + 会话文件 overlay + 环境目录 junction——gitignored 生成物(类型生成/代码生成产物类)不进 HEAD 也不在会话集,快照内 lint/test 环境性假败(2026-09-16 驾驭小结④,friction5-hardening R4 / D-002@v1)。按本清单在快照构建期从主仓 junction 链接补齐(失败回退复制);快照内 overlay 已覆盖的同名路径跳过(本变更最新态优先)。⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。',
160
160
  keys: [
161
161
  { path: 'gate_snapshot.copy', type: 'array', optional: true, status: 'live', readers: ['createGateSnapshot (src/run/gate-snapshot.js)'], desc: '门禁隔离快照的生成物/额外路径 copy 面(元素 string,相对仓根):HEAD 快照缺 gitignored 生成物导致 lint/test 环境性假败;声明后快照构建期从主仓 junction 链接(失败回退复制)。⚠️ junction 是活链接,快照内再跑生成命令会写穿到主仓该目录。未配置/空清单 = 全段空转零行为变化。', example: 'gate_snapshot:\n copy:\n - src/generated' },
162
162
  { path: 'gate_snapshot.commands', type: 'array', optional: true, status: 'live', readers: ['parseGateSnapshotCommands + runGateSnapshotCommands (src/run/gate-snapshot.js — createGateSnapshot 环境预检后 copy 面前)'], desc: '门禁隔离快照的命令面(元素 string):快照构建期在环境目录链接后、copy 面前于快照根逐条执行(cwd=快照根、300s/条超时帽、非零退出/超时只 warn 继续——fail-open 不作废快照),产出「本仓应然态」生成物,收口主仓生成物缺失/过期(fresh clone 未跑 postinstall / build-id 类产物)时快照内全量 lint/test 必挂的坑(D-002@v2,2026-09-17-feedback-hardening FR-01)。⚠️ 环境目录(node_modules/venv)是活链接,命令写它们会穿透主仓——只放生成物命令(npm run gen:build-id 类),不放 install/build 全家桶。未配置 = 全段空转零行为。', example: 'npm run gen:build-id' },
@@ -226,6 +226,16 @@ export const LOCAL_YAML_SCHEMA = {
226
226
  { path: 'change-ownership.heartbeat_minutes', type: 'integer', optional: true, status: 'live', readers: ['assertChangeOwnership (src/progress/change-registry.js — 本变更 task-02 接线消费)'], desc: '所有者活跃心跳窗口(分钟):owner 非本会话且 last_active 距今在窗口内 → 拒绝接管类操作(--takeover 显式接管);窗口外 → 放行并自动接管。缺省 15。', example: '15' },
227
227
  ],
228
228
  },
229
+ {
230
+ id: 'ceremony',
231
+ title: '评审仪式档位(ceremony tier)',
232
+ note: '仪式档位(S0~S3)由 CLI 按 blast/span/friction 三轴风险客观定价(blast 轴输入=项目声明危险面 _module-map.yaml 顶层 blast 段,2026-09-19-ceremony-pricing-five-cuts;span 轴路径模式输入=项目声明 _module-map.yaml 顶层 span_risk 段,2026-09-19-span-risk-pattern-migration,无声明项目该维关闭不回退内置表);完成门声明追赶重定价——无摩擦迁移随声明面可升可降、有摩擦迁移地板不退,force_tier 仍只升不降;本段是逃生阀/影子期开关/逐机升档面。读键按「存在则读、不存在用缺省」容错(读取走 readCeremonyLocalConfig)。',
233
+ keys: [
234
+ { path: 'ceremony.blast_surfaces', type: 'json', optional: true, status: 'live', readers: ['loadBlastDeclarations (src/blast-surface.js — local 逐机升档,与 map 声明逐文件取 max)'], desc: 'blast 危险面逐机覆盖:[{ prefixes: [...], tier: S0~S3 }]——主声明在 _module-map.yaml 顶层 blast 段(进 git、可挂 evidence 位);本键只升不降(与 map 命中逐文件取 max,压低共享声明无效)、不承载 evidence(证据语义属共享 map,D-009)。非法条目静默跳过。', example: '[{ prefixes: [src/my-daemon/], tier: S3 }]' },
235
+ { path: 'ceremony.force_tier', type: 'enum', values: ['S0', 'S1', 'S2', 'S3'], optional: true, status: 'live', readers: ['readCeremonyLocalConfig (src/run/prompt.js — {REVIEW_TIER} 注入档位菜单/强制轻仪审计痕)', '影子派发前置校验 (src/review-dispatch.js — task-06 影子期框架接线)'], desc: '档位逃生阀:强制仪式档 S0~S3,绕过客观定价(过渡期「就是不信这套」用)。prompt 注入面只升不降——强制档低于客观定价档时不降档(防 prompt 面与 gate 侧判定分裂);非法值 warn 后忽略。', example: 'S3' },
236
+ { path: 'ceremony.shadow', type: 'boolean', optional: true, status: 'live', readers: ['readCeremonyLocalConfig (src/run/prompt.js — S0/S1 轻档注入影子期注记)', 'runReviewDispatch 影子前置校验 (src/review-dispatch.js — task-06 影子期框架接线)'], desc: '影子期开关,默认 true(on):S0/S1 轻档明面轻仪、后台静默派发重仪式只记账不阻断(影子派发框架随 task-06 落地);置 off = 轻仪转正(转正判据由 doctor 影子对照维度公示,CLI 只出判据不出手)。', example: 'true' },
237
+ ],
238
+ },
229
239
  ],
230
240
  };
231
241
 
@@ -435,5 +445,16 @@ docs-check:
435
445
  # owner 非本会话且 last_active 在窗口内 → 拒绝(--takeover 显式接管);窗口外 → 自动接管。
436
446
  # change-ownership:
437
447
  # heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
448
+
449
+ # ── 评审仪式档位(ceremony tier:S0~S3 由 CLI 按 blast/span/friction 三轴风险客观定价;本段为逃生阀/影子期开关/逐机升档面)──
450
+ # blast 轴输入=项目声明危险面(_module-map.yaml 顶层 blast 段);完成门声明追赶重定价(无摩擦随声明、有摩擦地板不退)。
451
+ # force_tier 强制档位绕过客观定价(过渡逃生阀,prompt 注入面只升不降);shadow 影子期默认 on——
452
+ # S0/S1 轻档明面轻仪、后台静默派发重仪式对照只记账不阻断;置 off = 轻仪转正(转正判据见 doctor 影子对照)。
453
+ # ceremony:
454
+ # force_tier: S3 # 强制全重仪式(逃生阀;S0 | S1 | S2 | S3)
455
+ # shadow: true # 影子期开关(默认 true)
456
+ # blast_surfaces: # blast 逐机升档(只升不降、不承载 evidence;主声明在 _module-map.yaml blast 段)
457
+ # - prefixes: [src/my-daemon/]
458
+ # tier: S3
438
459
  `;
439
460
  }
@@ -94,3 +94,35 @@ export function upsertDecision(changeDir, decision) {
94
94
  writeAtomicSync(filePath, eol === '\n' ? next : next.replace(/\n/g, '\r\n'))
95
95
  return { path: filePath, action: 'replaced' }
96
96
  }
97
+
98
+ /**
99
+ * hasDecisionId —— decisions.md 内决策 ID 的字面存在性校验(机械零语义)。
100
+ * (2026-09-18-preflight-slimming Phase 3,D-003@v2:`--wait --inherit-from D-xxx@vN` 的
101
+ * fail-closed 锚点校验——防伪造决策锚点完成 wait 记录态,R-04。)
102
+ *
103
+ * 语义边界(机械,本模块无现成标题解析器可复用——upsertDecision 是写入者,读取侧解析归
104
+ * decision-distill;此处只做字面存在性,对齐 upsertDecision 块定位的双形态匹配):
105
+ * - 只认 canonical 标题行 `## D-xxx@vN`(upsertDecision 的唯一产出格式),匹配形态与其
106
+ * 块定位逐字同源:行前缀 `## <id> `(后随标题)或整行恰为 `## <id>`。
107
+ * - 不做版本归一(D-1@v1 不匹配 D-001@v1)、不做别名展开、不做标题级漂移容收(###/####
108
+ * 形态归 distill parseDecisions 的知识库提炼语义,锚点校验不放宽——fail-closed 保守)。
109
+ * - 文件不存在 / 不可读 / 参数形态非法 → 一律 false 不抛错(调用方 exit 2 即可)。
110
+ *
111
+ * @param {string} changeDir 变更目录(其下 decisions.md)
112
+ * @param {string} id 决策 ID 字面(如 `D-001@v2`)
113
+ * @returns {boolean} 标题行字面存在与否
114
+ */
115
+ export function hasDecisionId(changeDir, id) {
116
+ if (typeof changeDir !== 'string' || !changeDir) return false
117
+ if (typeof id !== 'string' || id === '') return false
118
+ try {
119
+ const filePath = join(changeDir, 'decisions.md')
120
+ if (!existsSync(filePath)) return false
121
+ const raw = readFileSync(filePath, 'utf8')
122
+ // CRLF 归一(Windows 换行防御,与 upsertDecision 同款口径)
123
+ const lines = raw.replace(/\r\n/g, '\n').split('\n')
124
+ return lines.some((l) => l.startsWith(`## ${id} `) || l.trim() === `## ${id}`)
125
+ } catch {
126
+ return false
127
+ }
128
+ }
@@ -301,6 +301,7 @@ export function generateDesignSkeleton({ changeName, decisionsText, author, now
301
301
  lines.push(`# 设计文档(Design)— ${change}`)
302
302
  lines.push('')
303
303
  lines.push(`<!-- 由 sillyspec design-init 生成的骨架(${change})——逐节填散文后删除本注释;存量手写路径不受影响 -->`)
304
+ lines.push('<!-- 引用规范:全文源码位置写仓根相对全路径+行号(src/foo.js:123)——裸文件名在 docs-check 层1 靠 basename 全仓扫描找候选,找不到候选或关键词窗口不匹配即失效,到 pre-push 才拦(2026-09-19 实证 64 处返工) -->')
304
305
  lines.push('')
305
306
 
306
307
  lines.push('## 背景')
@@ -36,6 +36,10 @@ import { readFrictionLedger } from './friction-ledger.js';
36
36
  // FR 索引消费(2026-09-18-fr-index-l1 L1,D-007):D14 第四检查——epoch 后归档的索引在场+取代完整。
37
37
  // fr-index 是纯 fs 叶子链(仅 decision-distill 底座),doctor 静态引入无环。
38
38
  import { FR_INDEX_EPOCH, scanFrIndex, parseChangeRequirements } from './fr-index.js';
39
+ // 影子对照账消费(2026-09-18-ceremony-risk-pricing task-06 / FR-04 / R-04 / R-06):读
40
+ // stage-reviews-shadow/shadow-ledger.json 累计账。review-dispatch 依赖链(fs-atomic /
41
+ // stage-review / quicklog / ceremony-tier)均不回指 doctor,静态引入无环。
42
+ import { readShadowLedger, SHADOW_NAMESPACE_DIR, SHADOW_PROMOTION_SAMPLE_TARGET } from './review-dispatch.js';
39
43
 
40
44
  // db 角色标签
41
45
  const DB_ROLE = {
@@ -1292,6 +1296,117 @@ function detectArchiveIntegrity(cwd, authoritySpecDir) {
1292
1296
  };
1293
1297
  }
1294
1298
 
1299
+ // ── 影子对照维度(2026-09-18-ceremony-risk-pricing task-06 / FR-04 / R-04 / R-06 / D-007@v1)──
1300
+
1301
+ /**
1302
+ * 轻仪影子期对照(ceremony_shadow):读 stage-reviews-shadow/ 对照账,输出轻/重 catch 差异
1303
+ * 报告(影子重仪式抓到、主线轻仪没抓到的问题清单)+ 转正判据计数——N/10 轻档对照样本且
1304
+ * (漏检=0 ∨ 漏检均 advisory 级(checklist result=gap;result=fail 为非 advisory))→ 达标。
1305
+ *
1306
+ * 语义(降级先例对齐 detectLifecycleDocStaleness):
1307
+ * - 无影子数据(账不存在 / 零条目零 skip)→ pass + 跳过注记,不误报;
1308
+ * - local.yaml ceremony.shadow: off → 已转正,pass + 终态统计(转正后不再报缺口);
1309
+ * - 影子期 on:达标 → pass +「可转正」提示;未达标 → pass:false + WARNING(advisory 公示
1310
+ * 缺口——影子链路只记账不阻断,本维度同样不进任何 gate / verify / archive 判定面)。
1311
+ * R-04 公示:findings 明示影子期 token 不降反升属「花钱买标定」+ local.yaml 开关位置 + 判据。
1312
+ * 转正动作本身是人工置 ceremony.shadow: off——CLI 只出判据不出手(safe_actions 只描述)。
1313
+ */
1314
+ function detectCeremonyShadowComparison(cwd, authoritySpecDir) {
1315
+ const base = { name: 'ceremony_shadow', label: '影子对照', safe_actions: [] };
1316
+ try {
1317
+ const pointer = resolvePointer(cwd);
1318
+ const runtimeRoot = resolveRuntimeRoot(
1319
+ { runtimeRoot: pointer.present && pointer.runtimeRoot && existsSync(pointer.runtimeRoot) ? pointer.runtimeRoot : null },
1320
+ authoritySpecDir || join(cwd, '.sillyspec'),
1321
+ );
1322
+ const ledger = readShadowLedger(runtimeRoot);
1323
+ const entries = Array.isArray(ledger.entries) ? ledger.entries : [];
1324
+ const skips = Array.isArray(ledger.skips) ? ledger.skips : [];
1325
+ if (entries.length === 0 && skips.length === 0) {
1326
+ return { ...base, pass: true, severity: null, findings: [`无影子对照数据(${SHADOW_NAMESPACE_DIR}/ 未开账——影子期未跑或已转正),跳过`] };
1327
+ }
1328
+ // 开关面(R-04 公示位置;读取容错对齐 readCeremonyLocalConfig 口径——本维度同步,内联读)
1329
+ let shadowOn = true;
1330
+ try {
1331
+ const lp = join(cwd, '.sillyspec', 'local.yaml');
1332
+ if (existsSync(lp)) {
1333
+ const doc = jsYaml.load(readFileSync(lp, 'utf8'));
1334
+ if (doc && typeof doc === 'object' && doc.ceremony && typeof doc.ceremony === 'object' && typeof doc.ceremony.shadow === 'boolean') {
1335
+ shadowOn = doc.ceremony.shadow;
1336
+ }
1337
+ }
1338
+ } catch { /* 开关读取失败按缺省 on */ }
1339
+
1340
+ const completed = entries.filter((e) => e && e.state === 'completed');
1341
+ const inFlight = entries.filter((e) => e && e.state === 'in-flight');
1342
+ const dead = entries.filter((e) => e && (e.state === 'failed' || e.state === 'abandoned' || e.state === 'bad-review'));
1343
+
1344
+ // catch 差异聚合:漏检 = 影子抓到而主线轻仪没抓到(result=fail → 非 advisory 级;gap → advisory 级)
1345
+ const missed = [];
1346
+ for (const e of completed) {
1347
+ for (const m of (e.diff && Array.isArray(e.diff.missedByLight) ? e.diff.missedByLight : [])) {
1348
+ missed.push({ change: e.change, stage: e.stage, item: m.item, result: m.result, note: m.note });
1349
+ }
1350
+ }
1351
+ const nonAdvisory = missed.filter((m) => m.result === 'fail');
1352
+ const verdictDelta = completed.filter((e) => e.diff && e.diff.verdictDelta);
1353
+
1354
+ const findings = [];
1355
+ // ① catch 差异报告(影子抓到主线没抓到的问题列表)
1356
+ if (missed.length > 0) {
1357
+ for (const m of missed.slice(0, 5)) {
1358
+ findings.push(`影子抓到主线漏检(${m.change}/${m.stage}):[${m.result}] ${String(m.item).slice(0, 100)}${m.note ? `——${String(m.note).slice(0, 60)}` : ''}`);
1359
+ }
1360
+ if (missed.length > 5) findings.push(`…还有 ${missed.length - 5} 项漏检(明细见 ${SHADOW_NAMESPACE_DIR}/shadow-ledger.json)`);
1361
+ } else if (completed.length > 0) {
1362
+ findings.push(`已完成 ${completed.length} 例轻/重对照,零漏检——轻仪 catch 面与重仪式一致`);
1363
+ }
1364
+ if (verdictDelta.length > 0) findings.push(`verdict 对照:${verdictDelta.length}/${completed.length} 例主线轻仪通过而影子重仪式判 fail`);
1365
+ if (inFlight.length > 0) findings.push(`在途影子派发 ${inFlight.length} 条(回收走 dispatchShadowReview mode='status'——影子结论不进任何主线判定)`);
1366
+ if (dead.length > 0) findings.push(`失败/未回收影子派发 ${dead.length} 条(fire-and-forget 记账,不补派不阻断)`);
1367
+ if (skips.length > 0) {
1368
+ const lastSkip = skips[skips.length - 1];
1369
+ findings.push(`跳过留痕 ${skips.length} 条(档位非轻档/开关 off/平台通道不可用——最近一条:${lastSkip && lastSkip.reason ? String(lastSkip.reason).slice(0, 80) : '未知原因'})`);
1370
+ }
1371
+
1372
+ const N = completed.length;
1373
+ const target = SHADOW_PROMOTION_SAMPLE_TARGET;
1374
+ // ② 转正判据(计数逻辑非文案——acceptance:N≥10 且漏检=0 ∨ 漏检均 advisory 级)
1375
+ const promotable = N >= target && nonAdvisory.length === 0;
1376
+
1377
+ if (!shadowOn) {
1378
+ findings.push(`ceremony.shadow: off——轻仪已转正(影子对照账保留供审计:N=${N},累计漏检 ${missed.length} 项)`);
1379
+ return { ...base, pass: true, severity: CHECK_SEVERITY.PASSED, findings };
1380
+ }
1381
+
1382
+ findings.push(`转正进度:${N}/${target} 轻档对照样本,漏检 ${missed.length} 项(非 advisory 级 ${nonAdvisory.length} 项)——判据:N≥${target} 且漏检=0 或漏检均 advisory 级(checklist result=gap)`);
1383
+ // ③ R-04 token 面注记(影子期开销公示,防困惑)
1384
+ findings.push('影子期 token 不降反升属「花钱买标定」(重仪式后台并行跑攒对照证据)——开关 local.yaml ceremony.shadow(当前 on),转正由人工置 off,CLI 只出判据不出手');
1385
+
1386
+ if (N === 0) {
1387
+ findings.push('尚无完成对照——转正判据未开始累计(在途/失败条目不计入 N)');
1388
+ return { ...base, pass: false, severity: CHECK_SEVERITY.WARNING, findings };
1389
+ }
1390
+ if (promotable) {
1391
+ findings.push(`影子期达标可转正:N=${N}≥${target} 且漏检均为 advisory 级(或零漏检)——可人工置 local.yaml ceremony.shadow: off`);
1392
+ return {
1393
+ ...base, pass: true, severity: CHECK_SEVERITY.PASSED, findings,
1394
+ safe_actions: [{ dimension: 'ceremony_shadow', action: 'promote_light_ceremony', risk: 'manual_edit', rationale: '影子期达标,轻仪可转正', next_step: '人工编辑 .sillyspec/local.yaml 置 ceremony.shadow: off(CLI 只出判据不出手)' }],
1395
+ };
1396
+ }
1397
+ const gaps = [];
1398
+ if (N < target) gaps.push(`还差 ${target - N} 个轻档对照样本`);
1399
+ if (nonAdvisory.length > 0) gaps.push(`${nonAdvisory.length} 项非 advisory 级(fail)漏检待补救核对`);
1400
+ findings.push(`转正判据未达标:${gaps.join(';')}`);
1401
+ return {
1402
+ ...base, pass: false, severity: CHECK_SEVERITY.WARNING, findings,
1403
+ safe_actions: [{ dimension: 'ceremony_shadow', action: 'review_missed_findings', risk: 'advisory', rationale: '影子抓到主线漏检项——影子结论不阻断,人工裁决是否主线补救', next_step: `核对 ${SHADOW_NAMESPACE_DIR}/shadow-ledger.json 漏检明细(非 advisory 级项优先)` }],
1404
+ };
1405
+ } catch (e) {
1406
+ return { ...base, findings: [`探测降级(${e?.message || e})——skipped`], pass: true, severity: null, skipped: true };
1407
+ }
1408
+ }
1409
+
1295
1410
  /**
1296
1411
  * renderDoctorSummary(FR-01,全新输出契约——2026-09-09-doctor-noai):逐维
1297
1412
  * ✅/⚠️/❌ + label + findings 首行 + safe_actions 提示行。顶层非 --json 命令与
@@ -1346,7 +1461,10 @@ export async function runDoctorDiagnostics({ cwd }) {
1346
1461
  const selfMaintenanceTax = detectSelfMaintenanceTax(cwd, authoritySpecDir)
1347
1462
  // D14(2026-09-17 archive-rescan,OpenSpec validate --archived 对标):归档完整性事后重扫(只读 WARNING)
1348
1463
  const archiveIntegrity = detectArchiveIntegrity(cwd, authoritySpecDir)
1349
- const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat, repoNativeChain, lifecycleDoc, worktreeHealth, buildEnv, mcpEndpoints, applyManifestDrift, selfMaintenanceTax, archiveIntegrity];
1464
+ // task-06(2026-09-18-ceremony-risk-pricing / FR-04 / R-04):影子对照——轻/重 catch 差异报告 +
1465
+ // 转正判据计数(纯增量挂载,只读不阻断;无影子数据跳过不误报)
1466
+ const ceremonyShadow = detectCeremonyShadowComparison(cwd, authoritySpecDir)
1467
+ const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat, repoNativeChain, lifecycleDoc, worktreeHealth, buildEnv, mcpEndpoints, applyManifestDrift, selfMaintenanceTax, archiveIntegrity, ceremonyShadow];
1350
1468
 
1351
1469
  return {
1352
1470
  dimensions,