sillyspec 3.28.12 → 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 (47) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +222 -223
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -8
  4. package/.claude/skills/sillyspec-verify/SKILL.md +87 -91
  5. package/package.json +1 -1
  6. package/src/blast-surface.js +168 -0
  7. package/src/ceremony-tier.js +359 -0
  8. package/src/change-risk-profile.js +75 -202
  9. package/src/config-schema.js +21 -0
  10. package/src/decision-distill.js +32 -25
  11. package/src/decisions-io.js +32 -0
  12. package/src/design-facts.js +1 -0
  13. package/src/doctor-diagnostics.js +148 -1
  14. package/src/fr-index.js +359 -0
  15. package/src/index.js +92 -5
  16. package/src/knowledge-stats.js +124 -2
  17. package/src/modules.js +28 -0
  18. package/src/prefill.js +491 -0
  19. package/src/probe7-anchor-check.js +8 -6
  20. package/src/quick-gate-profile.js +8 -4
  21. package/src/quick-session-owner.js +63 -0
  22. package/src/review-dispatch.js +473 -1
  23. package/src/review-material-pack.js +265 -0
  24. package/src/review-tier.js +185 -41
  25. package/src/run/archive-distill.js +35 -0
  26. package/src/run/command.js +158 -87
  27. package/src/run/complete-handlers.js +82 -1
  28. package/src/run/complete.js +37 -1
  29. package/src/run/gates.js +301 -19
  30. package/src/run/prompt.js +454 -21
  31. package/src/run/shared.js +36 -0
  32. package/src/run/verify-quality-scan.js +14 -6
  33. package/src/scope-audit.js +8 -1
  34. package/src/span-risk-surface.js +127 -0
  35. package/src/stage-contract-spec.js +2 -2
  36. package/src/stage-contract.js +172 -98
  37. package/src/stage-review.js +26 -2
  38. package/src/stages/brainstorm.js +34 -16
  39. package/src/stages/execute.js +10 -2
  40. package/src/stages/plan.js +29 -12
  41. package/src/stages/verify.js +7 -7
  42. package/src/taskcard.js +3 -0
  43. package/src/verify-postcheck.js +375 -20
  44. package/src/verify-probes.js +964 -30
  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) ============
@@ -321,7 +194,7 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
321
194
  // 变动)。对齐目标:verify-probes.js classifyReceiptCommandSource(judgeIntegrationRan 消费)
322
195
  // 同款 sourceMark 参数与标记族——**任一侧增改标记族必须双侧同步**(G-3 先例:producer 侧
323
196
  // 曾漏 smoke 一词致口径分裂)。
324
- const RECEIPT_SOURCE_CROSS_LAYER_RE = new RegExp([
197
+ export const RECEIPT_SOURCE_CROSS_LAYER_RE = new RegExp([
325
198
  '\\bcurl\\b', '\\bwget\\b', '\\bhttpie\\b', '\\bInvoke-WebRequest\\b', '\\bInvoke-RestMethod\\b',
326
199
  '\\biwr\\b', '\\birm\\b', 'https?://',
327
200
  'spring-boot:run', '\\bjava\\b[^|\\n]*\\s-jar\\b',
@@ -329,9 +202,9 @@ const RECEIPT_SOURCE_CROSS_LAYER_RE = new RegExp([
329
202
  '\\bdotnet\\s+run\\b', '\\bflask\\s+run\\b', '\\buvicorn\\b', '\\bgunicorn\\b',
330
203
  '\\b(?:nc|netcat|telnet|socat)\\b',
331
204
  ].join('|'), 'i')
332
- const RECEIPT_SOURCE_UNIT_RE = /\bJUnitCore\b|\bnode\s+--test\b|\bmocha\b|\bjest\b|\bvitest\b|\bpytest\b|\bphpunit\b|\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b/i
205
+ export const RECEIPT_SOURCE_UNIT_RE = /\bJUnitCore\b|\bnode\s+--test\b|\bmocha\b|\bjest\b|\bvitest\b|\bpytest\b|\bphpunit\b|\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b/i
333
206
  /** CLI 机器段来源标记(与 verify-probes.js SMOKE_RECEIPT_SOURCE_MARK / task-01 记录 source 同值,双侧同步) */
334
- const CLI_SMOKE_SOURCE_MARK = 'cli-noai-smoke'
207
+ export const CLI_SMOKE_SOURCE_MARK = 'cli-noai-smoke'
335
208
  function classifyReceiptSourceTag(command, sourceMark) {
336
209
  if (sourceMark === CLI_SMOKE_SOURCE_MARK) return 'cross-layer'
337
210
  const cmd = String(command || '')
@@ -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
  }
@@ -211,8 +211,9 @@ function parseModulePathsSubset(content) {
211
211
  /** 未注入 moduleIndex 时的尽力发现:knowledgeRoot 同级 docs/<项目>/modules/_module-map.yaml。
212
212
  * 多子项目仓(如本仓 sillyspec+dashboard)各项目有自己的 map——dogfood 实证(2026-08-24):
213
213
  * 只取首个命中会把主项目条目全部误落 unmapped(dashboard 的 map 按字母序先命中)。
214
- * 改为合并全部项目的 map(同名模块合并 paths/core_files 去重),失败归 null → 全部 unmapped。 */
215
- function discoverModuleIndex(knowledgeRoot) {
214
+ * 改为合并全部项目的 map(同名模块合并 paths/core_files 去重),失败归 null → 全部 unmapped。
215
+ * 2026-09-18-fr-index-l1:导出供 fr-index 域解析复用。 */
216
+ export function discoverModuleIndex(knowledgeRoot) {
216
217
  try {
217
218
  const docsDir = join(dirname(knowledgeRoot), 'docs')
218
219
  if (!existsSync(docsDir)) return null
@@ -319,15 +320,19 @@ function renderBlockLines(entry, headHash, supersedesNote, changeName) {
319
320
  return lines
320
321
  }
321
322
 
322
- /** 知识文件切段:preamble 行 + D-xxx@vN 条目段(段边界 = 下一 `## D-` 头)。
323
+ /** 知识文件切段:preamble 行 + 条目段(段边界 = 下一节头)。
323
324
  * 段的变更归属取段内 `变更:<name>` 行;无该行的历史条目 → change=null(legacy:
324
- * 升级前落库的条目,不参与新条目的同号匹配/supersede——只共存不误删)。 */
325
- function splitKnowledgeSections(content) {
325
+ * 升级前落库的条目,不参与新条目的同号匹配/supersede——只共存不误删)。
326
+ * 2026-09-18-fr-index-l1 参数化:sectionRegex/buildId 经参(decisions 缺省不变;fr 侧传 FR 节头
327
+ * 正则与恒等 id),decisions 调用点零改动。 */
328
+ export function splitKnowledgeSections(content, opts = {}) {
329
+ const sectionRegex = opts.sectionRegex || /^## (D-\d+)@v(\d+)\s*(.*)$/
330
+ const buildId = opts.buildId || ((n, v) => `${n}@v${v}`)
326
331
  const sections = []
327
332
  const preamble = []
328
333
  let cur = null
329
334
  for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
330
- const m = line.match(/^## (D-\d+)@v(\d+)\s*(.*)$/)
335
+ const m = line.match(sectionRegex)
331
336
  if (m) {
332
337
  if (cur) sections.push(cur)
333
338
  cur = { number: m[1], version: parseInt(m[2], 10), title: m[3].trim(), lines: [line] }
@@ -338,7 +343,7 @@ function splitKnowledgeSections(content) {
338
343
  }
339
344
  if (cur) sections.push(cur)
340
345
  for (const s of sections) {
341
- s.id = `${s.number}@v${s.version}`
346
+ s.id = buildId(s.number, s.version)
342
347
  const cm = s.lines.find(l => /^变更[::]/.test(l))
343
348
  s.change = cm ? cm.replace(/^变更[::]\s*/, '').trim() || null : null
344
349
  }
@@ -346,7 +351,7 @@ function splitKnowledgeSections(content) {
346
351
  }
347
352
 
348
353
  /** 段落重组落盘文本(幂等归一:段间恰好一个空行 + 文件单一尾换行) */
349
- function joinKnowledgeFile(preamble, sections) {
354
+ export function joinKnowledgeFile(preamble, sections) {
350
355
  const parts = []
351
356
  const pre = preamble.join('\n').replace(/\s+$/, '')
352
357
  if (pre) parts.push(pre)
@@ -394,19 +399,21 @@ function applyEntryToSections(sections, entry, headHash) {
394
399
  // INDEX.md decisions 路由行幂等维护(FR-05:写入责任在本模块;只动 ## Decisions 段内指向 decisions/ 的行)
395
400
  // ---------------------------------------------------------------------------
396
401
 
397
- /** INDEX 路由行格式与 knowledge-match 解析口径一致:`- <域>|decision|决策 → [decisions/<域>.md](decisions/<域>.md)` */
398
- function routingLine(domain) {
399
- return `- ${domain}|decision|决策 → [decisions/${domain}.md](decisions/${domain}.md)`
400
- }
401
-
402
- const ROUTING_TARGET_RE = /^-\s+.*→\s*\[[^\]]*\]\(decisions\/([^)#]+?)(?:\.md)?(?:#[^)]*)?\)\s*$/
403
-
404
- /** 域文件实况 ↔ ## Decisions 段路由行对账:缺行补、失效行删、重复行去重;不动其他类别行 */
405
- function syncIndexRoutingLines(knowledgeRoot) {
406
- const decisionsDir = join(knowledgeRoot, 'decisions')
402
+ /**
403
+ * 域文件实况 ↔ INDEX 段路由行对账:缺行补、失效行删、重复行去重;不动其他类别行。
404
+ * 2026-09-18-fr-index-l1 参数化:section/subdir/makeLine/matchTarget 经参(decisions 缺省不变),
405
+ * fr 侧传 'FR 需求索引' 段与 fr/ 子目录——两类路由行各守各段互不干扰。
406
+ */
407
+ export function syncIndexRoutingLines(knowledgeRoot, opts = {}) {
408
+ const sectionName = opts.section || 'Decisions'
409
+ const subdir = opts.subdir || 'decisions'
410
+ const makeLine = opts.makeLine || ((d) => `- ${d}|decision|决策 → [${subdir}/${d}.md](${subdir}/${d}.md)`)
411
+ const esc = subdir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
412
+ const targetRe = opts.matchTarget || new RegExp(`^-\\s+.*→\\s*\\[[^\\]]*\\]\\(${esc}/([^)#]+?)(?:\\.md)?(?:#[^)]*)?\\)\\s*$`)
413
+ const domainDir = join(knowledgeRoot, subdir)
407
414
  let domains
408
415
  try {
409
- domains = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, ''))
416
+ domains = readdirSync(domainDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, ''))
410
417
  } catch { return false }
411
418
  if (domains.length === 0) return false
412
419
 
@@ -417,29 +424,29 @@ function syncIndexRoutingLines(knowledgeRoot) {
417
424
  }
418
425
  const lines = original ? original.replace(/\r\n/g, '\n').split('\n') : ['# Knowledge Index', '']
419
426
 
420
- // 定位 ## Decisions 段边界
427
+ // 定位段边界(参数化段名)
421
428
  let secStart = -1
422
429
  let secEnd = lines.length
423
430
  for (let i = 0; i < lines.length; i++) {
424
431
  const h = lines[i].match(/^##\s+(.+?)\s*$/)
425
432
  if (!h) continue
426
433
  if (secStart === -1) {
427
- if (h[1] === 'Decisions') secStart = i + 1
434
+ if (h[1] === sectionName) secStart = i + 1
428
435
  } else { secEnd = i; break }
429
436
  }
430
437
 
431
- const desired = domains.map(routingLine)
438
+ const desired = domains.map(makeLine)
432
439
  if (secStart === -1) {
433
440
  // 无段则追加(保留原文件其余内容)
434
441
  while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
435
- lines.push('', '## Decisions', ...desired)
442
+ lines.push('', `## ${sectionName}`, ...desired)
436
443
  } else {
437
444
  const body = lines.slice(secStart, secEnd)
438
445
  const kept = []
439
446
  const seen = new Set()
440
447
  const domainSet = new Set(domains)
441
448
  for (const l of body) {
442
- const m = l.match(ROUTING_TARGET_RE)
449
+ const m = l.match(targetRe)
443
450
  if (!m) { kept.push(l); continue } // 非路由行(注释/空行)原样保留
444
451
  // 目标域文件仍存在的路由行原样保留(关键词可能被人工扩充);失效行(文件已不存在)与重复行删
445
452
  if (domainSet.has(m[1]) && !seen.has(m[1])) { seen.add(m[1]); kept.push(l) }
@@ -448,7 +455,7 @@ function syncIndexRoutingLines(knowledgeRoot) {
448
455
  const tail = []
449
456
  while (kept.length > 0 && kept[kept.length - 1].trim() === '') tail.unshift(kept.pop())
450
457
  for (const d of desired) {
451
- const m = d.match(ROUTING_TARGET_RE)
458
+ const m = d.match(targetRe)
452
459
  if (m && !seen.has(m[1])) kept.push(d) // 缺行补
453
460
  }
454
461
  kept.push(...tail)
@@ -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('## 背景')