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.
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -223
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -8
- package/.claude/skills/sillyspec-verify/SKILL.md +87 -91
- package/package.json +1 -1
- package/src/blast-surface.js +168 -0
- package/src/ceremony-tier.js +359 -0
- package/src/change-risk-profile.js +75 -202
- package/src/config-schema.js +21 -0
- package/src/decision-distill.js +32 -25
- package/src/decisions-io.js +32 -0
- package/src/design-facts.js +1 -0
- package/src/doctor-diagnostics.js +148 -1
- package/src/fr-index.js +359 -0
- package/src/index.js +92 -5
- package/src/knowledge-stats.js +124 -2
- package/src/modules.js +28 -0
- package/src/prefill.js +491 -0
- package/src/probe7-anchor-check.js +8 -6
- package/src/quick-gate-profile.js +8 -4
- package/src/quick-session-owner.js +63 -0
- package/src/review-dispatch.js +473 -1
- package/src/review-material-pack.js +265 -0
- package/src/review-tier.js +185 -41
- package/src/run/archive-distill.js +35 -0
- package/src/run/command.js +158 -87
- package/src/run/complete-handlers.js +82 -1
- package/src/run/complete.js +37 -1
- package/src/run/gates.js +301 -19
- package/src/run/prompt.js +454 -21
- package/src/run/shared.js +36 -0
- package/src/run/verify-quality-scan.js +14 -6
- package/src/scope-audit.js +8 -1
- package/src/span-risk-surface.js +127 -0
- package/src/stage-contract-spec.js +2 -2
- package/src/stage-contract.js +172 -98
- package/src/stage-review.js +26 -2
- package/src/stages/brainstorm.js +34 -16
- package/src/stages/execute.js +10 -2
- package/src/stages/plan.js +29 -12
- package/src/stages/verify.js +7 -7
- package/src/taskcard.js +3 -0
- package/src/verify-postcheck.js +375 -20
- package/src/verify-probes.js +964 -30
- package/src/worktree.js +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- 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
|
-
//
|
|
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
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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
|
-
/**
|
|
100
|
+
/** 风险分级的判定来源(2026-09-19-ceremony-pricing-five-cuts task-03 改声明面口径) */
|
|
142
101
|
export const RISK_LEVEL_CAUSES = {
|
|
143
102
|
'deployment-critical':
|
|
144
|
-
'
|
|
145
|
-
'
|
|
103
|
+
'变更文件面命中项目声明危险面(_module-map.yaml 顶层 blast 段)且判级为顶档。' +
|
|
104
|
+
'这是按「文件路径 × 项目声明」判定的——若认为该路径不该定此价:改 map 声明(git 可见);危险链路该有真实启动证据。',
|
|
146
105
|
'integration-critical':
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'contract-required': '
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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 || '')
|
package/src/config-schema.js
CHANGED
|
@@ -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
|
}
|
package/src/decision-distill.js
CHANGED
|
@@ -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
|
-
|
|
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 行 +
|
|
323
|
+
/** 知识文件切段:preamble 行 + 条目段(段边界 = 下一节头)。
|
|
323
324
|
* 段的变更归属取段内 `变更:<name>` 行;无该行的历史条目 → change=null(legacy:
|
|
324
|
-
* 升级前落库的条目,不参与新条目的同号匹配/supersede——只共存不误删)。
|
|
325
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
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(
|
|
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
|
-
//
|
|
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] ===
|
|
434
|
+
if (h[1] === sectionName) secStart = i + 1
|
|
428
435
|
} else { secEnd = i; break }
|
|
429
436
|
}
|
|
430
437
|
|
|
431
|
-
const desired = domains.map(
|
|
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('',
|
|
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(
|
|
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(
|
|
458
|
+
const m = d.match(targetRe)
|
|
452
459
|
if (m && !seen.has(m[1])) kept.push(d) // 缺行补
|
|
453
460
|
}
|
|
454
461
|
kept.push(...tail)
|
package/src/decisions-io.js
CHANGED
|
@@ -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
|
+
}
|
package/src/design-facts.js
CHANGED
|
@@ -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('## 背景')
|