sillyspec 3.29.1 → 3.29.3

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.
@@ -0,0 +1,38 @@
1
+ // 转录命令级分析 v2:response.toolCalls 提取
2
+ // 用法: node analyze-transcript.cjs <model-io.jsonl>
3
+ const fs = require('fs');
4
+ const file = process.argv[2];
5
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
6
+ const calls = [];
7
+ const slowReqs = [];
8
+ for (const line of lines) {
9
+ let rec;
10
+ try { rec = JSON.parse(line); } catch { continue; }
11
+ const dur = rec.durationMs || 0;
12
+ const ts = rec.startedAt ? new Date(rec.startedAt).toTimeString().slice(0, 8) : '?';
13
+ if (dur > 30000) slowReqs.push({ ts, dur: Math.round(dur / 1000), n: (rec.response && rec.response.toolCalls || []).length });
14
+ for (const tc of (rec.response && rec.response.toolCalls) || []) {
15
+ const inp = tc.input || {};
16
+ let arg = '';
17
+ if (tc.name === 'Bash') arg = String(inp.command || '').replace(/\s+/g, ' ').slice(0, 130);
18
+ else if (tc.name === 'Read') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
19
+ else if (tc.name === 'Edit' || tc.name === 'Write') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
20
+ else if (tc.name === 'Grep') arg = String(inp.pattern || '').slice(0, 40) + ' @' + String(inp.path || '').split(/[\\/]/).slice(-1);
21
+ else if (tc.name === 'Agent') arg = 'dispatch: ' + String(inp.description || '').slice(0, 50);
22
+ else arg = JSON.stringify(inp).slice(0, 80);
23
+ calls.push({ ts, tool: tc.name, arg });
24
+ }
25
+ }
26
+ const byTool = {};
27
+ const byArg = {};
28
+ for (const c of calls) {
29
+ byTool[c.tool] = (byTool[c.tool] || 0) + 1;
30
+ byArg[c.tool + ' :: ' + c.arg] = (byArg[c.tool + ' :: ' + c.arg] || 0) + 1;
31
+ }
32
+ console.log('请求行:', lines.length, '| 工具调用:', calls.length, '| 分布:', JSON.stringify(byTool));
33
+ console.log('\n== 重复 ≥2 次调用 top20 ==');
34
+ for (const [k, n] of Object.entries(byArg).filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20)) console.log(n + 'x |', k.slice(0, 160));
35
+ console.log('\n== 慢请求 >30s ==');
36
+ for (const s of slowReqs.sort((a, b) => b.dur - a.dur)) console.log(s.ts, '|', s.dur + 's', '| 本轮工具数:', s.n);
37
+ console.log('\n== 全部 Bash 命令时间线 ==');
38
+ for (const c of calls.filter((c) => c.tool === 'Bash')) console.log(c.ts, '|', c.arg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.29.1",
3
+ "version": "3.29.3",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -497,12 +497,66 @@ function detectOpencode(ctx) {
497
497
  * 防「Cursor IDE 在别的项目聊天 / 其他项目 opencode 会话活跃」被误报进当前登记。
498
498
  * 新增 CLI = 加一个探测器对象 + protocol 文档补布局与格式说明。
499
499
  */
500
+ /**
501
+ * cursor-agent(CLI 子代理,非 Cursor IDE):transcript = <home>/.cursor/projects/
502
+ * <encoded-workspace-path>/agent-transcripts/<uuid>/<uuid>.jsonl({role,message} 行 +
503
+ * turn_ended 事件)。workspace 目录名是 cwd 的连字符编码(C:/Users/qinyi → C-Users-qinyi,
504
+ * 路径内合法连字符保留原样——编码不可逆,匹配方向为 cwd→编码名正向比对)→ **precise** 探测:
505
+ * 逐 cwdCandidate 编码后与目录名精确相等才登记(数字目录名无从解码,跳过不误报)。
506
+ * 归属规则与 zcode/claude-code 同款(本会话 cwd 即 workspace 根);token 不落盘属数据源
507
+ * 缺失(usage 未知,平台侧 FR-03 口径不伪造)。
508
+ * 坑 cursor-agent-transcript-report-pipeline(2026-09-19 平台侧就绪声明):daemon 解析器
509
+ * 注册键 `cursor-agent-transcript-jsonl` 必须与此处 format 串逐字一致。
510
+ */
511
+ function detectCursorAgentTranscripts(ctx) {
512
+ const { cwdCandidates, homeDir, now, windowMs } = ctx;
513
+ const projectsRoot = join(homeDir, '.cursor', 'projects');
514
+ let projectDirs;
515
+ try {
516
+ projectDirs = readdirSync(projectsRoot, { withFileTypes: true }).filter(d => d.isDirectory());
517
+ } catch { return []; }
518
+ // cwd → 编码名(正向,避免解码歧义):盘符冒号与路径分隔符全部 → '-'
519
+ const encodeCwd = (p) => toPosix(String(p)).replace(/^([a-zA-Z]):/, '$1').replace(/[/:]/g, '-');
520
+ const wanted = new Set(cwdCandidates.filter(Boolean).map(encodeCwd));
521
+ if (wanted.size === 0) return [];
522
+ const entries = [];
523
+ for (const proj of projectDirs) {
524
+ if (!wanted.has(proj.name)) continue;
525
+ const tsRoot = join(projectsRoot, proj.name, 'agent-transcripts');
526
+ let sessionDirs;
527
+ try {
528
+ sessionDirs = readdirSync(tsRoot, { withFileTypes: true }).filter(d => d.isDirectory());
529
+ } catch { continue; }
530
+ for (const sess of sessionDirs) {
531
+ let files;
532
+ try {
533
+ files = readdirSync(join(tsRoot, sess.name)).filter(f => f.endsWith('.jsonl'));
534
+ } catch { continue; }
535
+ for (const f of files) {
536
+ const full = join(tsRoot, sess.name, f);
537
+ const st = statSafe(full);
538
+ if (!st || now - st.mtimeMs > windowMs) continue;
539
+ entries.push({
540
+ harness: 'cursor-agent', format: 'cursor-agent-transcript-jsonl',
541
+ detected_via: 'cursor-agent-transcript-scan',
542
+ log_path: toPosix(full), agent_cwd: toPosix(cwdCandidates[0] || ''),
543
+ session_id: sess.name, originator: null,
544
+ mtime_ms: st.mtimeMs,
545
+ });
546
+ }
547
+ }
548
+ }
549
+ entries.sort((a, b) => b.mtime_ms - a.mtime_ms);
550
+ return entries.slice(0, 3);
551
+ }
552
+
500
553
  const HARNESS_DETECTORS = [
501
554
  { name: 'claude-code', tier: 'precise', detect: detectClaudeCode },
502
555
  { name: 'codex', tier: 'precise', detect: detectCodex },
503
556
  { name: 'zcode', tier: 'precise', detect: detectZcode },
504
557
  { name: 'pi', tier: 'precise', detect: detectPi },
505
558
  { name: 'deepseek-dsh', tier: 'precise', detect: detectDsh },
559
+ { name: 'cursor-agent', tier: 'precise', detect: detectCursorAgentTranscripts },
506
560
  { name: 'cursor', tier: 'loose', detect: detectCursor },
507
561
  { name: 'opencode', tier: 'loose', detect: detectOpencode },
508
562
  ];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ceremony-config.js — ceremony 定价项目级配置读取器(local.yaml `ceremony:` 段 pricing 五键)
3
+ *
4
+ * 2026-09-20 ceremony 项目化定价(对话裁决:每个项目体系不一样,S0~S3 判定阈值应可配置):
5
+ * 引擎(ceremony-tier.js)保持纯函数零 IO——配置由本读取器读 local.yaml 注入调用方。
6
+ * 可配的是「起点与阈值」(default_tier / span 两阈值 / friction 起爆线 / 五级词映射部分覆写);
7
+ * 「只升不降」全局纪律(friction 升档、显式降档须理由)无配置出口——防项目把自己配裸奔。
8
+ *
9
+ * 语义对齐 readCeremonyLocalConfig(src/run/prompt.js,force_tier/shadow 读取器):
10
+ * best-effort 绝不抛、非法值 warn 后回退默认、缺段返回空对象(引擎侧 normalize 回内置值)。
11
+ * blast 危险面 / span 风险路径**不在本读取器**——它们是项目共享声明(_module-map.yaml 顶层
12
+ * blast/span_risk 段,进 git),local.yaml 是逐机配置,语义不同不混载。
13
+ */
14
+ import { existsSync, readFileSync } from 'fs';
15
+ import { join } from 'path';
16
+ import jsYaml from 'js-yaml';
17
+
18
+ import { CEREMONY_TIERS } from './ceremony-tier.js';
19
+
20
+ /**
21
+ * 读 local.yaml `ceremony:` 段的 pricing 五键(camelCase 归一出参,与 normalizeTierConfig 入参对齐)。
22
+ * @param {string} specBase - .sillyspec 目录(local.yaml 所在);空/null → 空配置
23
+ * @returns {{ defaultTier?: string, spanFilesThreshold?: number, spanModulesThreshold?: number, frictionThreshold?: number, riskTierMap?: object }}
24
+ * 仅含合法键(非法/缺失键不发——引擎侧回退内置值)
25
+ */
26
+ export function readCeremonyPricingConfig(specBase) {
27
+ const out = {};
28
+ if (!specBase) return out;
29
+ let raw = null;
30
+ try {
31
+ const p = join(specBase, 'local.yaml');
32
+ if (!existsSync(p)) return out;
33
+ raw = jsYaml.load(readFileSync(p, 'utf8'));
34
+ } catch {
35
+ return out; // 坏 yaml → 空配置(引擎回内置值),不拦定价
36
+ }
37
+ const c = raw && typeof raw === 'object' && raw.ceremony && typeof raw.ceremony === 'object'
38
+ ? raw.ceremony
39
+ : null;
40
+ if (!c) return out;
41
+
42
+ if (CEREMONY_TIERS.includes(c.default_tier)) out.defaultTier = c.default_tier;
43
+ else if (c.default_tier != null) console.warn(`[sillyspec] ceremony.default_tier 非法「${c.default_tier}」已忽略(可选:${CEREMONY_TIERS.join(' | ')})`);
44
+
45
+ for (const [key, outKey] of [['span_files_threshold', 'spanFilesThreshold'], ['span_modules_threshold', 'spanModulesThreshold'], ['friction_escalation_threshold', 'frictionThreshold']]) {
46
+ const v = c[key];
47
+ if (Number.isInteger(v) && v >= 1) out[outKey] = v;
48
+ else if (v != null) console.warn(`[sillyspec] ceremony.${key} 非法「${v}」已忽略(≥1 整数)`);
49
+ }
50
+
51
+ if (c.risk_tier_map != null) {
52
+ if (typeof c.risk_tier_map === 'object' && !Array.isArray(c.risk_tier_map)) {
53
+ const map = {};
54
+ for (const [level, tier] of Object.entries(c.risk_tier_map)) {
55
+ if (CEREMONY_TIERS.includes(tier)) map[level] = tier;
56
+ else console.warn(`[sillyspec] ceremony.risk_tier_map.${level} 档位非法「${tier}」已忽略(可选:${CEREMONY_TIERS.join(' | ')})`);
57
+ }
58
+ if (Object.keys(map).length > 0) out.riskTierMap = map;
59
+ } else {
60
+ console.warn('[sillyspec] ceremony.risk_tier_map 非对象已忽略');
61
+ }
62
+ }
63
+ return out;
64
+ }
@@ -156,7 +156,12 @@ function matchModuleForFile(posix, modulesObj) {
156
156
  * friction 分量=升档后的档(未起爆为 S0,不参与封顶);reasons 逐分量留痕(含阈值与
157
157
  * 命中明细),供消费方审计打印与双跑比对
158
158
  */
159
- export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLevel, declaredFiles, moduleIndex, spanRiskPatterns = [], frictionCounts } = {}) {
159
+ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLevel, declaredFiles, moduleIndex, spanRiskPatterns = [], frictionCounts, config } = {}) {
160
+ // ── 项目级配置覆写(2026-09-20 ceremony 项目化定价):引擎保持纯函数零 IO,配置由调用方
161
+ // 读 local.yaml ceremony: 段注入(readCeremonyPricingConfig)。可配的是「起点与阈值」:
162
+ // 缺省档 / span 两阈值 / friction 起爆线 / 五级词映射。「只升不降」全局纪律(friction 升档、
163
+ // 显式降档须理由)不开放配置——防项目把自己配裸奔。非法值逐项回退默认并在 reasons 留痕。
164
+ const cfg = normalizeTierConfig(config)
160
165
  const reasons = []
161
166
  let explicitDowngradeAccepted = false
162
167
 
@@ -167,22 +172,22 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
167
172
  reasons.push(`blast=${blast}(声明面直入 blastTier——resolveChangeRisk 声明危险面判级,D-008)`)
168
173
  } else {
169
174
  const detectedLevel = riskDetection != null && typeof riskDetection === 'object' ? riskDetection.level : undefined
170
- const mappedTier = RISK_TO_TIER[detectedLevel]
175
+ const mappedTier = cfg.riskTierMap[detectedLevel]
171
176
  if (mappedTier) {
172
177
  blast = mappedTier
173
- reasons.push(`blast=${blast}(riskDetection.level=${detectedLevel} 经 RISK_TO_TIER 映射)`)
178
+ reasons.push(`blast=${blast}(riskDetection.level=${detectedLevel} 经 risk_tier_map 映射)`)
174
179
  } else {
175
- blast = 'S2'
180
+ blast = cfg.defaultTier
176
181
  const cause = detectedLevel === undefined ? 'riskDetection.level 缺失' : `riskDetection.level=${String(detectedLevel)} 未知`
177
- reasons.push(`blast=S2(保守缺省:${cause}——brownfield 不静默降级)`)
182
+ reasons.push(`blast=${cfg.defaultTier}(保守缺省:${cause}${cfg.defaultTierSource === 'config' ? ',项目配置 default_tier' : ',brownfield 不静默降级'})`)
178
183
  }
179
184
  }
180
185
 
181
186
  const explicit = normalizeExplicit(explicitRiskLevel)
182
187
  if (explicit) {
183
- const explicitTier = RISK_TO_TIER[explicit.level]
188
+ const explicitTier = cfg.riskTierMap[explicit.level]
184
189
  if (!explicitTier) {
185
- reasons.push(`显式 risk_level 非法被忽略:${JSON.stringify(explicitRiskLevel)}(合法值:${Object.keys(RISK_TO_TIER).join(' / ')})`)
190
+ reasons.push(`显式 risk_level 非法被忽略:${JSON.stringify(explicitRiskLevel)}(合法值:${Object.keys(cfg.riskTierMap).join(' / ')})`)
186
191
  } else if (tierRank(explicitTier) > tierRank(blast)) {
187
192
  reasons.push(`blast 升档尊重显式声明 risk_level=${explicit.level} → ${explicitTier}(高于判级 ${blast})`)
188
193
  blast = explicitTier
@@ -203,9 +208,9 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
203
208
  .map((f) => String(f).replace(/\\/g, '/'))
204
209
  .filter(Boolean)
205
210
  let span = 'S0'
206
- if (files.length >= SPAN_FILES_THRESHOLD) {
211
+ if (files.length >= cfg.spanFilesThreshold) {
207
212
  span = 'S2'
208
- reasons.push(`span=S2(声明文件 ${files.length} ≥ 阈值 ${SPAN_FILES_THRESHOLD})`)
213
+ reasons.push(`span=S2(声明文件 ${files.length} ≥ 阈值 ${cfg.spanFilesThreshold}${cfg.spanFilesSource === 'config' ? '(项目配置)' : ''})`)
209
214
  }
210
215
 
211
216
  const modulesObj = resolveModulesObject(moduleIndex)
@@ -215,9 +220,9 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
215
220
  const id = matchModuleForFile(f, modulesObj)
216
221
  if (id != null) hitModules.add(id)
217
222
  }
218
- if (hitModules.size >= SPAN_MODULES_THRESHOLD) {
223
+ if (hitModules.size >= cfg.spanModulesThreshold) {
219
224
  span = 'S2'
220
- reasons.push(`span=S2(跨模块 ${hitModules.size} ≥ 阈值 ${SPAN_MODULES_THRESHOLD}:${[...hitModules].join('、')})`)
225
+ reasons.push(`span=S2(跨模块 ${hitModules.size} ≥ 阈值 ${cfg.spanModulesThreshold}${cfg.spanModulesSource === 'config' ? '(项目配置)' : ''}:${[...hitModules].join('、')})`)
221
226
  }
222
227
  } // moduleIndex 缺失/空 → 跨模块检查跳过,span 其余两维照常
223
228
 
@@ -240,15 +245,63 @@ export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLeve
240
245
  // ── friction 轴:ledger 两键合计达到阈值 → 在 blast/span 结果上 +1 档封顶 S3(只升不降)──
241
246
  const total = frictionTotal(frictionCounts)
242
247
  let friction = 'S0'
243
- if (total >= FRICTION_ESCALATION_THRESHOLD) {
248
+ if (total >= cfg.frictionThreshold) {
244
249
  const base = maxTier(blast, span)
245
250
  friction = CEREMONY_TIERS[Math.min(tierRank(base) + 1, CEREMONY_TIERS.length - 1)]
246
- reasons.push(`friction 升档(gate_rollback+review_rejected 合计 ${total} ≥ 阈值 ${FRICTION_ESCALATION_THRESHOLD},${base} → ${friction},封顶 S3)`)
251
+ reasons.push(`friction 升档(gate_rollback+review_rejected 合计 ${total} ≥ 阈值 ${cfg.frictionThreshold}${cfg.frictionSource === 'config' ? '(项目配置)' : ''},${base} → ${friction},封顶 S3)`)
247
252
  }
248
253
 
249
254
  return { tier: maxTier(maxTier(blast, span), friction), components: { blast, span, friction }, reasons, explicitDowngradeAccepted }
250
255
  }
251
256
 
257
+ /**
258
+ * 项目级定价配置归一(computeCeremonyTier 内部消费,纯函数):非法值逐项回退默认。
259
+ * 入参形态即 local.yaml `ceremony:` 段的 pricing 五键(由 readCeremonyPricingConfig 读取注入,
260
+ * 引擎自身不读文件):
261
+ * default_tier('S0'~'S3')/ span_files_threshold(≥1 整数)/ span_modules_threshold(≥1 整数)
262
+ * / friction_escalation_threshold(≥1 整数)/ risk_tier_map(五级词→档位映射的**部分覆写**,
263
+ * 与内置表浅合并——只覆写声明的词,未声明词保持内置值)。
264
+ * 「只升不降」纪律不在此面(friction 升档/显式降档复核为全局语义,无配置出口)。
265
+ */
266
+ function normalizeTierConfig(config) {
267
+ const c = config != null && typeof config === 'object' && !Array.isArray(config) ? config : {}
268
+ const out = {
269
+ defaultTier: 'S2',
270
+ defaultTierSource: 'builtin',
271
+ spanFilesThreshold: SPAN_FILES_THRESHOLD,
272
+ spanFilesSource: 'builtin',
273
+ spanModulesThreshold: SPAN_MODULES_THRESHOLD,
274
+ spanModulesSource: 'builtin',
275
+ frictionThreshold: FRICTION_ESCALATION_THRESHOLD,
276
+ frictionSource: 'builtin',
277
+ riskTierMap: { ...RISK_TO_TIER },
278
+ }
279
+ if (CEREMONY_TIERS.includes(c.defaultTier)) {
280
+ out.defaultTier = c.defaultTier
281
+ out.defaultTierSource = 'config'
282
+ }
283
+ if (Number.isInteger(c.spanFilesThreshold) && c.spanFilesThreshold >= 1) {
284
+ out.spanFilesThreshold = c.spanFilesThreshold
285
+ out.spanFilesSource = 'config'
286
+ }
287
+ if (Number.isInteger(c.spanModulesThreshold) && c.spanModulesThreshold >= 1) {
288
+ out.spanModulesThreshold = c.spanModulesThreshold
289
+ out.spanModulesSource = 'config'
290
+ }
291
+ if (Number.isInteger(c.frictionThreshold) && c.frictionThreshold >= 1) {
292
+ out.frictionThreshold = c.frictionThreshold
293
+ out.frictionSource = 'config'
294
+ }
295
+ if (c.riskTierMap != null && typeof c.riskTierMap === 'object' && !Array.isArray(c.riskTierMap)) {
296
+ for (const [level, tier] of Object.entries(c.riskTierMap)) {
297
+ if (Object.prototype.hasOwnProperty.call(RISK_TO_TIER, level) && CEREMONY_TIERS.includes(tier)) {
298
+ out.riskTierMap[level] = tier
299
+ }
300
+ }
301
+ }
302
+ return out
303
+ }
304
+
252
305
  /**
253
306
  * friction 升档(阶段门检查点用,D-008):两键合计达到阈值 → min(S3, 当前档+1);只升不降
254
307
  * ——未达阈 / 已在顶 / 当前档非法时原样返回不发明新档。escalated=档位实际上移(S3 再超阈
@@ -118,7 +118,7 @@ export const LOCAL_YAML_SCHEMA = {
118
118
  title: '测试策略',
119
119
  note: 'verify 阶段 CLI 对账的收窄策略(D-005@v2:skip 接线兑现 + evidence-auto 新增,full/module 语义不变)。',
120
120
  keys: [
121
- { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module 并注记)。缺省=全量。', example: 'full' },
121
+ { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module 并注记)。缺省:配了 modules: 块 → module(v3.29.3 起缺省收窄,ql-20260920-010);未配 modules → 全量。', example: 'full' },
122
122
  ],
123
123
  },
124
124
  {
@@ -229,9 +229,14 @@ export const LOCAL_YAML_SCHEMA = {
229
229
  {
230
230
  id: 'ceremony',
231
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)。',
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 仍只升不降;本段是逃生阀/影子期开关/逐机升档面 + 项目化定价阈值(2026-09-20:每个项目体系不一样,起点与阈值可配)。读键按「存在则读、不存在用缺省」容错(定价键读取走 readCeremonyPricingConfig,开关面走 readCeremonyLocalConfig)。',
233
233
  keys: [
234
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.default_tier', type: 'enum', values: ['S0', 'S1', 'S2', 'S3'], optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier blast 轴保守缺省——review-tier/verify-postcheck 双跑两侧同源注入)'], desc: '项目化定价·缺省档(default_tier):blast 轴无 risk 输入时的保守起点(内置 S2)。小工具仓可降 S1、强合规仓可升 S3——每个项目按自己的体系定。只改起点,不影响「只升不降」纪律。', example: 'ceremony: { default_tier: S1 }' },
236
+ { path: 'ceremony.span_files_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier span 轴文件数阈值)'], desc: '项目化定价·span 文件阈值(span_files_threshold):声明文件数 ≥N → 至少 S2(内置 8)。小仓 3 个文件就算摊子大就配 3。≥1 整数。', example: 'ceremony: { span_files_threshold: 3 }' },
237
+ { path: 'ceremony.span_modules_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier span 轴跨模块阈值)'], desc: '项目化定价·span 跨模块阈值(span_modules_threshold):跨模块数 ≥N → 至少 S2(内置 3)。≥1 整数。', example: 'ceremony: { span_modules_threshold: 2 }' },
238
+ { path: 'ceremony.friction_escalation_threshold', type: 'integer', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier friction 轴起爆线)'], desc: '项目化定价·friction 起爆线(friction_escalation_threshold):gate_rollback+review_rejected 合计 ≥N → 升一档封顶 S3(内置 2,两振出局)。强合规仓配 1 = 一次拦拒即升档。≥1 整数。', example: 'ceremony: { friction_escalation_threshold: 1 }' },
239
+ { path: 'ceremony.risk_tier_map', type: 'json', optional: true, status: 'live', readers: ['readCeremonyPricingConfig (src/ceremony-config.js → computeCeremonyTier 五级词映射部分覆写)'], desc: '项目化定价·五级词→档位映射覆写(部分覆写:只覆写声明的词,未声明词保持内置 doc-only→S0 / unit-sufficient→S1 / contract-required→S2 / integration-critical→S3 / deployment-critical→S3)。例:{ unit-sufficient: S2 } = 该仓单测级变更也走独立评审。非法词/档位 warn 后忽略。', example: 'ceremony: { risk_tier_map: { unit-sufficient: S2 } }' },
235
240
  { 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
241
  { 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
242
  ],
@@ -446,11 +451,19 @@ docs-check:
446
451
  # change-ownership:
447
452
  # heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
448
453
 
449
- # ── 评审仪式档位(ceremony tier:S0~S3 由 CLI 按 blast/span/friction 三轴风险客观定价;本段为逃生阀/影子期开关/逐机升档面)──
454
+ # ── 评审仪式档位(ceremony tier:S0~S3 由 CLI 按 blast/span/friction 三轴风险客观定价)──
450
455
  # blast 轴输入=项目声明危险面(_module-map.yaml 顶层 blast 段);完成门声明追赶重定价(无摩擦随声明、有摩擦地板不退)。
456
+ # 项目化定价(每个项目体系不一样,起点与阈值可配——只升不降纪律不开放配置):
457
+ # default_tier 缺省档 / span_files_threshold 文件阈值(内置8) / span_modules_threshold 跨模块阈值(内置3)
458
+ # / friction_escalation_threshold 起爆线(内置2) / risk_tier_map 五级词映射部分覆写。
451
459
  # force_tier 强制档位绕过客观定价(过渡逃生阀,prompt 注入面只升不降);shadow 影子期默认 on——
452
460
  # S0/S1 轻档明面轻仪、后台静默派发重仪式对照只记账不阻断;置 off = 轻仪转正(转正判据见 doctor 影子对照)。
453
461
  # ceremony:
462
+ # default_tier: S1 # 缺省档(内置 S2;小仓 S1 / 强合规 S3)
463
+ # span_files_threshold: 3 # 文件数阈值(内置 8)
464
+ # span_modules_threshold: 2 # 跨模块阈值(内置 3)
465
+ # friction_escalation_threshold: 1 # 起爆线(内置 2,两振出局;强合规 1)
466
+ # risk_tier_map: { unit-sufficient: S2 } # 五级词部分覆写(未声明词保持内置)
454
467
  # force_tier: S3 # 强制全重仪式(逃生阀;S0 | S1 | S2 | S3)
455
468
  # shadow: true # 影子期开关(默认 true)
456
469
  # blast_surfaces: # blast 逐机升档(只升不降、不承载 evidence;主声明在 _module-map.yaml blast 段)