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.
- 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 +72 -199
- package/src/config-schema.js +22 -1
- package/src/decisions-io.js +32 -0
- package/src/design-facts.js +1 -0
- package/src/doctor-diagnostics.js +119 -1
- package/src/index.js +92 -5
- package/src/knowledge-stats.js +25 -0
- 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/redlines.js +195 -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 +6 -0
- package/src/run/command.js +158 -87
- package/src/run/complete-handlers.js +82 -1
- package/src/run/complete.js +90 -3
- package/src/run/gate-snapshot.js +30 -1
- package/src/run/gates.js +301 -19
- package/src/run/prompt.js +423 -21
- package/src/run/quick-audit.js +12 -2
- package/src/run/shared.js +36 -0
- package/src/run/verify-quality-scan.js +15 -7
- 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 +102 -81
- package/src/stage-review.js +26 -2
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +35 -19
- package/src/stages/execute.js +11 -3
- package/src/stages/plan.js +45 -24
- package/src/stages/scan.js +1 -1
- 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 +194 -10
- package/src/worktree-apply.js +1 -1
- package/src/worktree.js +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blast-surface.js — 项目声明危险面(blast 轴唯一输入源,2026-09-19-ceremony-pricing-five-cuts D-008)
|
|
3
|
+
*
|
|
4
|
+
* 背景(词表退役的根因,详见 knowledge/conventions.md「判级/定价/门禁输入必须项目声明」条):
|
|
5
|
+
* 旧 detectChangeRisk 用硬编码全宇宙词表对文档措辞/文件内容做机械匹配——「撞词≠危险」(换仓
|
|
6
|
+
* HTTP session / lifecycle 回调 / 变量名 entryPoint 全是良性词)且自指(判级引擎自身源码与教学
|
|
7
|
+
* 文案写满关键词,改引擎的变更被自家门禁打顶档)。本模块按 D-008 口径取而代之:
|
|
8
|
+
*
|
|
9
|
+
* 主声明 = `_module-map.yaml` 顶层 `blast:` 段(进 git、手工维护、modules rebuild --force
|
|
10
|
+
* 写盘时从 existingMap 文本回插保留——见 modules.js):按路径前缀挂 `tier: S0~S3` 与
|
|
11
|
+
* `evidence: true`(需要真实集成证据的显式标记,D-009——证据门只认它,不从仪式档推断)。
|
|
12
|
+
* local.yaml `ceremony.blast_surfaces` 只升不降(逐文件取 max;不承载 evidence——证据语义
|
|
13
|
+
* 属共享 map)。
|
|
14
|
+
* 未命中声明面 → blast S1;未配置项目禁止回退旧词表(D-008:无默认词表兜底)。
|
|
15
|
+
*
|
|
16
|
+
* 零依赖纯函数 + 一个 IO 装载器;前缀匹配复用 matchModuleForFile 同款语义(字面量或目录
|
|
17
|
+
* 前缀,matchModuleForFile 先例:src/ceremony-tier.js / src/quick-gate-profile.js)——零新
|
|
18
|
+
* 正则族、不造第二套文法。
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
21
|
+
import { join } from 'path'
|
|
22
|
+
import jsYaml from 'js-yaml'
|
|
23
|
+
|
|
24
|
+
/** 未命中任何声明面时的 blast 缺省档(D-008:S1 起步,无词表兜底) */
|
|
25
|
+
export const BLAST_NO_HIT_TIER = 'S1'
|
|
26
|
+
|
|
27
|
+
/** 档位序(与 ceremony-tier.js CEREMONY_TIERS 同序;本地声明避免 core-engine↔本模块循环依赖) */
|
|
28
|
+
const TIER_ORDER = ['S0', 'S1', 'S2', 'S3']
|
|
29
|
+
|
|
30
|
+
function tierRank(t) {
|
|
31
|
+
return TIER_ORDER.indexOf(t)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function maxTier(a, b) {
|
|
35
|
+
return tierRank(a) >= tierRank(b) ? a : b
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** POSIX 归一(Windows 反斜杠兼容) */
|
|
39
|
+
function toPosix(p) {
|
|
40
|
+
return String(p).replace(/\\/g, '/')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 单文件 × 单前缀匹配(matchModuleForFile 同款语义):字面量相等或目录前缀命中
|
|
45
|
+
* (prefix 以 / 结尾时先剥尾斜杠;`src/a` 命中 `src/a.js` 与 `src/a/x.js`,不命中 `src/ab.js`)。
|
|
46
|
+
*/
|
|
47
|
+
export function blastPrefixMatches(file, prefix) {
|
|
48
|
+
const posix = toPosix(file)
|
|
49
|
+
const p = toPosix(prefix).replace(/\/+$/, '')
|
|
50
|
+
if (!p) return false
|
|
51
|
+
return posix === p || posix.startsWith(p + '/')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 声明面解析(纯函数):files × 声明条目 → { tier, evidence, hitPrefixes }。
|
|
56
|
+
* tier = 命中条目最高档(无命中 BLAST_NO_HIT_TIER);evidence = 任一命中条目带
|
|
57
|
+
* `evidence: true`;hitPrefixes = 命中的前缀(POSIX 化去重,审计/文案用)。
|
|
58
|
+
* 非法条目(tier 不在序、prefixes 非数组)逐条跳过不抛错(声明面是手工维护数据,
|
|
59
|
+
* 容错立场同 parseModuleMapSimple:坏段不缺省不拦截)。
|
|
60
|
+
*
|
|
61
|
+
* @param {string[]} files 变更文件面(声明面或实际 diff;反斜杠自动归一)
|
|
62
|
+
* @param {Array<{prefixes: string[], tier: string, evidence?: boolean}>} declarations
|
|
63
|
+
*/
|
|
64
|
+
export function resolveBlastSurfaces(files, declarations) {
|
|
65
|
+
const norm = (Array.isArray(files) ? files : []).map(toPosix).filter(Boolean)
|
|
66
|
+
const list = Array.isArray(declarations) ? declarations : []
|
|
67
|
+
let tier = BLAST_NO_HIT_TIER
|
|
68
|
+
let evidence = false
|
|
69
|
+
const hitSet = new Set()
|
|
70
|
+
for (const entry of list) {
|
|
71
|
+
if (entry == null || typeof entry !== 'object') continue
|
|
72
|
+
const prefixes = Array.isArray(entry.prefixes) ? entry.prefixes : []
|
|
73
|
+
const entryTier = TIER_ORDER.includes(entry.tier) ? entry.tier : null
|
|
74
|
+
const entryEvidence = entry.evidence === true
|
|
75
|
+
if (!entryTier && !entryEvidence) continue
|
|
76
|
+
for (const file of norm) {
|
|
77
|
+
for (const rawPrefix of prefixes) {
|
|
78
|
+
const prefix = toPosix(rawPrefix).replace(/\/+$/, '')
|
|
79
|
+
if (!prefix) continue
|
|
80
|
+
if (blastPrefixMatches(file, prefix)) {
|
|
81
|
+
if (entryTier) tier = maxTier(tier, entryTier)
|
|
82
|
+
if (entryEvidence) evidence = true
|
|
83
|
+
hitSet.add(prefix)
|
|
84
|
+
break // 该文件对该条目命中一次即止
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { tier, evidence, hitPrefixes: [...hitSet] }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 声明面装载(IO):`_module-map.yaml` 顶层 `blast:` 段 + `local.yaml ceremony.blast_surfaces`
|
|
94
|
+
* 合并——**local 只升不降**(D-008:逐文件取 max;local 不承载 evidence,证据语义属共享 map)。
|
|
95
|
+
* 合并实现口径:map 声明原样透传;local 条目以「影子 tier 条目」参与解析(evidence 恒 false),
|
|
96
|
+
* resolveBlastSurfaces 的 max 语义天然保证只升不降。
|
|
97
|
+
*
|
|
98
|
+
* 容错:map/local 缺失、坏 YAML、blast 段/键形态非法 → 该源按空表(不缺省不拦截)。
|
|
99
|
+
*
|
|
100
|
+
* @param {{ specBase: string, project?: string|null, localCeremonyConfig?: object|null }} opts
|
|
101
|
+
* specBase=.sillyspec 根;project=子项目名(缺省只试 docs/<project> 不猜);
|
|
102
|
+
* localCeremonyConfig=已读好的 local ceremony 段(缺省内部直读 local.yaml,容错)
|
|
103
|
+
* @returns {{ declarations: Array, mapDeclarations: Array, localDeclarations: Array }}
|
|
104
|
+
*/
|
|
105
|
+
export function loadBlastDeclarations({ specBase, project = null, localCeremonyConfig = null } = {}) {
|
|
106
|
+
const mapDeclarations = []
|
|
107
|
+
if (specBase && project) {
|
|
108
|
+
const mapPath = join(specBase, 'docs', project, 'modules', '_module-map.yaml')
|
|
109
|
+
try {
|
|
110
|
+
if (existsSync(mapPath)) {
|
|
111
|
+
const doc = jsYaml.load(readFileSync(mapPath, 'utf8'))
|
|
112
|
+
const blast = doc && typeof doc === 'object' ? doc.blast : null
|
|
113
|
+
if (Array.isArray(blast)) {
|
|
114
|
+
for (const entry of blast) {
|
|
115
|
+
if (entry == null || typeof entry !== 'object') continue
|
|
116
|
+
if (!Array.isArray(entry.prefixes) || entry.prefixes.length === 0) continue
|
|
117
|
+
if (!TIER_ORDER.includes(entry.tier)) continue
|
|
118
|
+
mapDeclarations.push({ prefixes: entry.prefixes.map(String), tier: entry.tier, evidence: entry.evidence === true })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch { /* 坏 map → 空表(手工数据容错,不拦截判级) */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let localCeremony = localCeremonyConfig
|
|
126
|
+
if (localCeremony == null && specBase) {
|
|
127
|
+
try {
|
|
128
|
+
const localPath = join(specBase, 'local.yaml')
|
|
129
|
+
if (existsSync(localPath)) {
|
|
130
|
+
const doc = jsYaml.load(readFileSync(localPath, 'utf8'))
|
|
131
|
+
localCeremony = doc && typeof doc === 'object' ? doc.ceremony : null
|
|
132
|
+
}
|
|
133
|
+
} catch { /* 坏 local → 无本地覆盖 */ }
|
|
134
|
+
}
|
|
135
|
+
const localDeclarations = []
|
|
136
|
+
const surfaces = localCeremony && Array.isArray(localCeremony.blast_surfaces) ? localCeremony.blast_surfaces : []
|
|
137
|
+
for (const entry of surfaces) {
|
|
138
|
+
if (entry == null || typeof entry !== 'object') continue
|
|
139
|
+
if (!Array.isArray(entry.prefixes) || entry.prefixes.length === 0) continue
|
|
140
|
+
if (!TIER_ORDER.includes(entry.tier)) continue
|
|
141
|
+
// local 只升 tier 不承载 evidence(evidence 恒 false——证据语义属共享 map,D-009)
|
|
142
|
+
localDeclarations.push({ prefixes: entry.prefixes.map(String), tier: entry.tier, evidence: false })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { declarations: [...mapDeclarations, ...localDeclarations], mapDeclarations, localDeclarations }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 多项目并集装载(事实面用——调用方无 project 语境时扫各 docs/<project>/modules/_module-map.yaml
|
|
150
|
+
* 取全部项目声明并集):多项目仓对 repo 级文件面的变更按全声明面保守计价;local.yaml 覆盖
|
|
151
|
+
* 同入(specBase 只有一份 local,全项目共享升档面)。任一项目装载异常 → 该项目按空表跳过。
|
|
152
|
+
* @param {{ specBase: string, localCeremonyConfig?: object|null }} opts
|
|
153
|
+
* @returns {Array<{prefixes: string[], tier: string, evidence?: boolean}>}
|
|
154
|
+
*/
|
|
155
|
+
export function loadBlastDeclarationsAllProjects({ specBase, localCeremonyConfig = null } = {}) {
|
|
156
|
+
const all = []
|
|
157
|
+
if (!specBase) return all
|
|
158
|
+
try {
|
|
159
|
+
const docsDir = join(specBase, 'docs')
|
|
160
|
+
if (!existsSync(docsDir)) return all
|
|
161
|
+
for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
|
|
162
|
+
if (!entry.isDirectory()) continue
|
|
163
|
+
const { declarations } = loadBlastDeclarations({ specBase, project: entry.name, localCeremonyConfig })
|
|
164
|
+
all.push(...declarations)
|
|
165
|
+
}
|
|
166
|
+
} catch { /* docs 目录不可读 → 空表(手工数据容错) */ }
|
|
167
|
+
return all
|
|
168
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ceremony-tier.js — 仪式强度定价引擎(三轴客观计价,纯函数零 IO)
|
|
3
|
+
*
|
|
4
|
+
* 2026-09-18-ceremony-risk-pricing task-01 / FR-01(D-001 引擎骨架、D-004 显式声明通道、
|
|
5
|
+
* D-008 friction 升档规则)。ceremony_tier = max(blast, span, friction):评审轮次/计划厚度/
|
|
6
|
+
* Grill 深度不再由 agent 自报的 plan_level 定价,而是三分量客观取封顶。自报契约
|
|
7
|
+
* (2026-09-19-ceremony-pricing-five-cuts D-003 修订):**无摩擦迁移时随声明面重定价**
|
|
8
|
+
* (完成门声明追赶,applyDeclarationCatchUp——后补 risk_level/声明面变化可升可降);
|
|
9
|
+
* **有摩擦迁移时地板不退**(transitions 最高 to 档托底);摩擦升档本身与 force_tier 仍
|
|
10
|
+
* 只升不降;本 computeCeremonyTier 内的 explicitRiskLevel 降档仍须 reason 并标记
|
|
11
|
+
* explicitDowngradeAccepted 待收口双跑复核。本模块是定价的单点真相源,消费面:
|
|
12
|
+
* review-tier.js(评审档)、run/gates.js(阶段门升档)、verify-postcheck.js /
|
|
13
|
+
* complete-handlers.js(双跑收口)。
|
|
14
|
+
*
|
|
15
|
+
* 三轴口径:
|
|
16
|
+
* blast = blastTier 声明面直入(resolveChangeRisk:变更文件 × _module-map.yaml 顶层
|
|
17
|
+
* blast 段声明危险面,D-008——2026-09-19-ceremony-pricing-five-cuts 起的
|
|
18
|
+
* 主通道;未命中 S1、词表判级退役)> riskDetection.level 经 RISK_TO_TIER 五档
|
|
19
|
+
* 映射(legacy 兼容层)+ 显式声明升降规则;两者皆缺 → 保守缺省 S2
|
|
20
|
+
* (brownfield 兼容:≈现状 independent×1,不静默降级)。
|
|
21
|
+
* span = 声明文件数 ≥ SPAN_FILES_THRESHOLD ∨ 跨模块数 ≥ SPAN_MODULES_THRESHOLD ∨
|
|
22
|
+
* 声明文件命中项目声明 span_risk 表(matchSpanRiskPatterns,
|
|
23
|
+
* 2026-09-19-span-risk-pattern-migration D-003)→ 至少 S2。moduleIndex 沿用
|
|
24
|
+
* computeGateProfile 的输入形态(_module-map.yaml 解析结果,先例
|
|
25
|
+
* quick-gate-profile.js),缺失/空则跨模块检查跳过(不缺省不拦截);声明文件数
|
|
26
|
+
* 按 declaredFiles 全量计(任务卡口径,不复刻 quick-gate 的文档剔除)。
|
|
27
|
+
* friction = friction-ledger 累计账的 gate_rollback + review_rejected 合计达到
|
|
28
|
+
* FRICTION_ESCALATION_THRESHOLD → 在 blast/span 结果上 +1 档封顶 S3,只升不降。
|
|
29
|
+
* 只认 ledger 口径两键(禁读 friction-tally——其 consumeFrictionHint 消费即删,
|
|
30
|
+
* 会给 verify 门引入隐式次序依赖);入参携第三键 verify_run_failed 时透传容忍:
|
|
31
|
+
* 不抛错不拒收、不参与升档计算。
|
|
32
|
+
*
|
|
33
|
+
* 零依赖纯函数:无 fs / 锁 / 网络 / 日志副作用;仓内 import 是 change-risk-profile.js 的
|
|
34
|
+
* 纯常量 RISK_TO_TIER 与 span-risk-surface.js 的纯函数 matchSpanRiskPatterns(span 轴
|
|
35
|
+
* 风险路径表自 2026-09-19-span-risk-pattern-migration D-003 起改项目声明 span_risk 段经
|
|
36
|
+
* 参数注入,默认 [] = 模式维度关闭,不再内嵌全宇宙表)。阈值常量集中文件顶部可调
|
|
37
|
+
* (R-02:上线前用 friction-ledger 历史变更回放标定再定值)。
|
|
38
|
+
*/
|
|
39
|
+
import { RISK_TO_TIER } from './change-risk-profile.js'
|
|
40
|
+
import { matchSpanRiskPatterns } from './span-risk-surface.js'
|
|
41
|
+
|
|
42
|
+
/** 仪式档位序(只升不降的偏序基准,index 即高低:S0=0 … S3=3) */
|
|
43
|
+
export const CEREMONY_TIERS = ['S0', 'S1', 'S2', 'S3']
|
|
44
|
+
|
|
45
|
+
// RISK_TO_TIER 迁源 re-export(2026-09-19-ceremony-pricing-five-cuts task-02):五级词→档位映射表
|
|
46
|
+
// 判级域归属 change-risk-profile.js(resolveChangeRisk 消费方),本文件 re-export 保持既有
|
|
47
|
+
// import 面(test/ceremony-tier.test.mjs 等)不变——依赖方向 ceremony-tier→change-risk-profile
|
|
48
|
+
// 已有先例。
|
|
49
|
+
export { RISK_TO_TIER }
|
|
50
|
+
|
|
51
|
+
// ============ 阈值常量(集中可调;R-02 friction-ledger 历史回放标定的落点) ============
|
|
52
|
+
|
|
53
|
+
/** span 轴:声明文件数 ≥ 此值 → 至少 S2 */
|
|
54
|
+
export const SPAN_FILES_THRESHOLD = 8
|
|
55
|
+
|
|
56
|
+
/** span 轴:跨模块数 ≥ 此值 → 至少 S2 */
|
|
57
|
+
export const SPAN_MODULES_THRESHOLD = 3
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* friction 轴起爆阈值:gate_rollback + review_rejected 合计 ≥ 此值 → 在 blast/span 结果上
|
|
61
|
+
* +1 档封顶 S3。口径=达到即起爆(默认 2 = 两次摩擦事件升档,两振出局;R-02 回放可调)。
|
|
62
|
+
*/
|
|
63
|
+
export const FRICTION_ESCALATION_THRESHOLD = 2
|
|
64
|
+
|
|
65
|
+
/** friction 计价消费的 ledger 两键(第三键 verify_run_failed 透传容忍、不消费) */
|
|
66
|
+
const FRICTION_CONSUMED_KEYS = ['gate_rollback', 'review_rejected']
|
|
67
|
+
|
|
68
|
+
/** 档位→序数(非法/缺省 → -1) */
|
|
69
|
+
function tierRank(tier) {
|
|
70
|
+
return CEREMONY_TIERS.indexOf(tier)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 两档取高(防御非法值:任一档非法时返回另一档;两档均合法时返回高者) */
|
|
74
|
+
function maxTier(a, b) {
|
|
75
|
+
return tierRank(a) >= tierRank(b) ? a : b
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** friction 合计:只消费 ledger 两键;脏值(非有限数/负数)按 0,第三键透传不读 */
|
|
79
|
+
function frictionTotal(frictionCounts) {
|
|
80
|
+
if (frictionCounts == null || typeof frictionCounts !== 'object') return 0
|
|
81
|
+
let total = 0
|
|
82
|
+
for (const key of FRICTION_CONSUMED_KEYS) {
|
|
83
|
+
const n = Number(frictionCounts[key])
|
|
84
|
+
if (Number.isFinite(n) && n > 0) total += n
|
|
85
|
+
}
|
|
86
|
+
return total
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 升档阈值覆写解析:数字直接作阈值;对象取 frictionEscalation 键;非法静默回退默认(纯函数不打印) */
|
|
90
|
+
function resolveFrictionThreshold(thresholds) {
|
|
91
|
+
let v = null
|
|
92
|
+
if (typeof thresholds === 'number') v = thresholds
|
|
93
|
+
else if (thresholds != null && typeof thresholds === 'object' && !Array.isArray(thresholds)) v = thresholds.frictionEscalation
|
|
94
|
+
return Number.isInteger(v) && v >= 1 ? v : FRICTION_ESCALATION_THRESHOLD
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 显式声明入参归一:字符串或 {level, reason} → {level, reason};reason 空白视为无理由 */
|
|
98
|
+
function normalizeExplicit(v) {
|
|
99
|
+
if (v == null) return null
|
|
100
|
+
if (typeof v === 'string') return { level: v.trim().toLowerCase(), reason: null }
|
|
101
|
+
if (typeof v === 'object' && !Array.isArray(v)) {
|
|
102
|
+
return {
|
|
103
|
+
level: typeof v.level === 'string' ? v.level.trim().toLowerCase() : v.level,
|
|
104
|
+
reason: typeof v.reason === 'string' && v.reason.trim() !== '' ? v.reason.trim() : null,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { level: v, reason: null } // 非法形态:RISK_TO_TIER 查不到 → 忽略留痕
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** moduleIndex 可用性归一(computeGateProfile 同口径):null/非对象/空 → null(跨模块检查跳过);
|
|
111
|
+
* 兼容 { modules: {...} } 包装与裸路径→模块映射两种形态 */
|
|
112
|
+
function resolveModulesObject(moduleIndex) {
|
|
113
|
+
if (moduleIndex == null || typeof moduleIndex !== 'object' || Array.isArray(moduleIndex)) return null
|
|
114
|
+
const wrapped = moduleIndex.modules
|
|
115
|
+
const obj = wrapped != null && typeof wrapped === 'object' && !Array.isArray(wrapped) ? wrapped : moduleIndex
|
|
116
|
+
return Object.keys(obj).length > 0 ? obj : null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 一级模块归属(quick-gate-profile matchModuleForFile 同口径):paths/core_files 字面量或目录前缀命中 */
|
|
120
|
+
function matchModuleForFile(posix, modulesObj) {
|
|
121
|
+
for (const [id, m] of Object.entries(modulesObj)) {
|
|
122
|
+
if (m == null || typeof m !== 'object') continue
|
|
123
|
+
const prefixes = [...(Array.isArray(m.paths) ? m.paths : []), ...(Array.isArray(m.core_files) ? m.core_files : [])]
|
|
124
|
+
for (const raw of prefixes) {
|
|
125
|
+
const p = String(raw).replace(/\\/g, '/').replace(/\/+$/, '')
|
|
126
|
+
if (!p) continue
|
|
127
|
+
if (posix === p || posix.startsWith(p + '/')) return id
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 三轴定价:ceremony_tier = max(blast, span, friction)(客观计算,一切自报只升不可降)。
|
|
135
|
+
*
|
|
136
|
+
* @param {object} [opts]
|
|
137
|
+
* @param {{level: string}} [opts.riskDetection] legacy 判级输入(五级词 level,只消费 level;词表判级已退役——保留为既有调用方兼容通道,新调用走 blastTier);
|
|
138
|
+
* 缺失 / level 未知 → blast 保守缺省 S2(brownfield:不静默降级)
|
|
139
|
+
* @param {string} [opts.blastTier] blast 轴直入(2026-09-19-ceremony-pricing-five-cuts task-03:
|
|
140
|
+
* resolveChangeRisk 声明面判级产物 tier——在场合跳过 riskDetection.level 映射直接起算;
|
|
141
|
+
* 与 riskDetection 并存时 blastTier 优先;显式声明升降规则照常套用)
|
|
142
|
+
* @param {string|{level: string, reason: string}} [opts.explicitRiskLevel] 显式声明
|
|
143
|
+
* (design frontmatter risk_level 通道):升档永远尊重;降档须 reason(无 reason 的降档
|
|
144
|
+
* 忽略并在 reasons 留痕),采纳时 explicitDowngradeAccepted=true 待收口双跑复核
|
|
145
|
+
* @param {string[]} [opts.declaredFiles] 声明变更文件(span 轴;反斜杠自动归一 POSIX)
|
|
146
|
+
* @param {object|null} [opts.moduleIndex] _module-map.yaml 解析结果(跨模块数口径);
|
|
147
|
+
* null/undefined/空 → 跨模块检查跳过(不缺省不拦截)
|
|
148
|
+
* @param {Array<{pattern: string, re: RegExp}>} [opts.spanRiskPatterns] span 轴风险路径声明表
|
|
149
|
+
* (span-risk-surface.js compile/load 产物;默认 [] = 模式维度关闭——项目声明 span_risk
|
|
150
|
+
* 段由调用方装载注入,2026-09-19-span-risk-pattern-migration D-003;命中 → 至少 S2,
|
|
151
|
+
* reasons 记 token 与命中文件)
|
|
152
|
+
* @param {object} [opts.frictionCounts] friction-ledger 累计账口径 { gate_rollback,
|
|
153
|
+
* review_rejected }——携 verify_run_failed 第三键透传容忍不拒收、不参与计算
|
|
154
|
+
* @returns {{ tier: 'S0'|'S1'|'S2'|'S3', components: { blast: string, span: string, friction: string },
|
|
155
|
+
* reasons: string[], explicitDowngradeAccepted: boolean }}
|
|
156
|
+
* friction 分量=升档后的档(未起爆为 S0,不参与封顶);reasons 逐分量留痕(含阈值与
|
|
157
|
+
* 命中明细),供消费方审计打印与双跑比对
|
|
158
|
+
*/
|
|
159
|
+
export function computeCeremonyTier({ riskDetection, blastTier, explicitRiskLevel, declaredFiles, moduleIndex, spanRiskPatterns = [], frictionCounts } = {}) {
|
|
160
|
+
const reasons = []
|
|
161
|
+
let explicitDowngradeAccepted = false
|
|
162
|
+
|
|
163
|
+
// ── blast 轴:声明面直入(优先)> 判级映射 > 保守缺省 ──
|
|
164
|
+
let blast
|
|
165
|
+
if (CEREMONY_TIERS.includes(blastTier)) {
|
|
166
|
+
blast = blastTier
|
|
167
|
+
reasons.push(`blast=${blast}(声明面直入 blastTier——resolveChangeRisk 声明危险面判级,D-008)`)
|
|
168
|
+
} else {
|
|
169
|
+
const detectedLevel = riskDetection != null && typeof riskDetection === 'object' ? riskDetection.level : undefined
|
|
170
|
+
const mappedTier = RISK_TO_TIER[detectedLevel]
|
|
171
|
+
if (mappedTier) {
|
|
172
|
+
blast = mappedTier
|
|
173
|
+
reasons.push(`blast=${blast}(riskDetection.level=${detectedLevel} 经 RISK_TO_TIER 映射)`)
|
|
174
|
+
} else {
|
|
175
|
+
blast = 'S2'
|
|
176
|
+
const cause = detectedLevel === undefined ? 'riskDetection.level 缺失' : `riskDetection.level=${String(detectedLevel)} 未知`
|
|
177
|
+
reasons.push(`blast=S2(保守缺省:${cause}——brownfield 不静默降级)`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const explicit = normalizeExplicit(explicitRiskLevel)
|
|
182
|
+
if (explicit) {
|
|
183
|
+
const explicitTier = RISK_TO_TIER[explicit.level]
|
|
184
|
+
if (!explicitTier) {
|
|
185
|
+
reasons.push(`显式 risk_level 非法被忽略:${JSON.stringify(explicitRiskLevel)}(合法值:${Object.keys(RISK_TO_TIER).join(' / ')})`)
|
|
186
|
+
} else if (tierRank(explicitTier) > tierRank(blast)) {
|
|
187
|
+
reasons.push(`blast 升档尊重显式声明 risk_level=${explicit.level} → ${explicitTier}(高于判级 ${blast})`)
|
|
188
|
+
blast = explicitTier
|
|
189
|
+
} else if (tierRank(explicitTier) < tierRank(blast)) {
|
|
190
|
+
if (explicit.reason) {
|
|
191
|
+
reasons.push(`blast 降档采纳显式声明 risk_level=${explicit.level}(理由:${explicit.reason})→ ${explicitTier},待收口双跑复核(explicitDowngradeAccepted=true)`)
|
|
192
|
+
blast = explicitTier
|
|
193
|
+
explicitDowngradeAccepted = true
|
|
194
|
+
} else {
|
|
195
|
+
reasons.push(`显式降级缺理由被忽略(声明 ${explicit.level}→${explicitTier} 低于判级 ${blast},无 reason——一切自报只升不降)`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 等档声明:无行为差异,不留痕
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── span 轴:文件数 / 跨模块数 / 风险路径模式,任一命中 → 至少 S2 ──
|
|
202
|
+
const files = (Array.isArray(declaredFiles) ? declaredFiles : [])
|
|
203
|
+
.map((f) => String(f).replace(/\\/g, '/'))
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
let span = 'S0'
|
|
206
|
+
if (files.length >= SPAN_FILES_THRESHOLD) {
|
|
207
|
+
span = 'S2'
|
|
208
|
+
reasons.push(`span=S2(声明文件 ${files.length} ≥ 阈值 ${SPAN_FILES_THRESHOLD})`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const modulesObj = resolveModulesObject(moduleIndex)
|
|
212
|
+
if (modulesObj) {
|
|
213
|
+
const hitModules = new Set()
|
|
214
|
+
for (const f of files) {
|
|
215
|
+
const id = matchModuleForFile(f, modulesObj)
|
|
216
|
+
if (id != null) hitModules.add(id)
|
|
217
|
+
}
|
|
218
|
+
if (hitModules.size >= SPAN_MODULES_THRESHOLD) {
|
|
219
|
+
span = 'S2'
|
|
220
|
+
reasons.push(`span=S2(跨模块 ${hitModules.size} ≥ 阈值 ${SPAN_MODULES_THRESHOLD}:${[...hitModules].join('、')})`)
|
|
221
|
+
}
|
|
222
|
+
} // moduleIndex 缺失/空 → 跨模块检查跳过,span 其余两维照常
|
|
223
|
+
|
|
224
|
+
// 风险路径模式维:共享 matcher(matchSpanRiskPatterns——files 已归一,其内部重复归一幂等;
|
|
225
|
+
// 防御口径 null/非对象条目跳过与 /g lastIndex 归零由 matcher 承接),按 pattern 聚合 files
|
|
226
|
+
// 保持 riskHitsByPattern 语义与 reasons 文案形态不变(只换表来源:注入声明表,默认 [] 关维)
|
|
227
|
+
const riskHitsByPattern = new Map()
|
|
228
|
+
for (const { pattern, file } of matchSpanRiskPatterns(files, spanRiskPatterns)) {
|
|
229
|
+
const list = riskHitsByPattern.get(pattern) || []
|
|
230
|
+
list.push(file)
|
|
231
|
+
riskHitsByPattern.set(pattern, list)
|
|
232
|
+
}
|
|
233
|
+
if (riskHitsByPattern.size > 0) {
|
|
234
|
+
span = 'S2'
|
|
235
|
+
for (const [pattern, hitFiles] of riskHitsByPattern) {
|
|
236
|
+
reasons.push(`span=S2(风险路径命中 ${pattern}:${hitFiles.join('、')})`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── friction 轴:ledger 两键合计达到阈值 → 在 blast/span 结果上 +1 档封顶 S3(只升不降)──
|
|
241
|
+
const total = frictionTotal(frictionCounts)
|
|
242
|
+
let friction = 'S0'
|
|
243
|
+
if (total >= FRICTION_ESCALATION_THRESHOLD) {
|
|
244
|
+
const base = maxTier(blast, span)
|
|
245
|
+
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)`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { tier: maxTier(maxTier(blast, span), friction), components: { blast, span, friction }, reasons, explicitDowngradeAccepted }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* friction 升档(阶段门检查点用,D-008):两键合计达到阈值 → min(S3, 当前档+1);只升不降
|
|
254
|
+
* ——未达阈 / 已在顶 / 当前档非法时原样返回不发明新档。escalated=档位实际上移(S3 再超阈
|
|
255
|
+
* 返回 { tier:'S3', escalated:false }:已在顶无可升,摩擦在场事实由 computeCeremonyTier 的
|
|
256
|
+
* reasons 留痕承载)。
|
|
257
|
+
*
|
|
258
|
+
* @param {string} currentTier 当前档('S0'~'S3')
|
|
259
|
+
* @param {object} frictionCounts ledger 累计账口径(携 verify_run_failed 第三键透传容忍)
|
|
260
|
+
* @param {number|{frictionEscalation: number}} [thresholds] 阈值覆写(≥1 整数;非法静默回退默认)
|
|
261
|
+
* @returns {{ tier: string, escalated: boolean }}
|
|
262
|
+
*/
|
|
263
|
+
export function escalateByFriction(currentTier, frictionCounts, thresholds) {
|
|
264
|
+
const currentRank = tierRank(currentTier)
|
|
265
|
+
if (currentRank < 0) return { tier: currentTier, escalated: false }
|
|
266
|
+
const th = resolveFrictionThreshold(thresholds)
|
|
267
|
+
if (frictionTotal(frictionCounts) < th) return { tier: currentTier, escalated: false }
|
|
268
|
+
const nextRank = Math.min(currentRank + 1, CEREMONY_TIERS.length - 1)
|
|
269
|
+
return { tier: CEREMONY_TIERS[nextRank], escalated: nextRank > currentRank }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 声明追赶重定价(2026-09-19-ceremony-pricing-five-cuts task-03 / D-003 纯函数单点):
|
|
274
|
+
* 档位文件在场时完成门按当前声明面重算开跑价,「只升不降」契约修订为三分支——
|
|
275
|
+
* ① currentDoc.transitions 为空且 frictionTotal < 阈值 → 整档换重算结果(可升可降),
|
|
276
|
+
* reasons 记「声明追赶重定价」(后补 risk_level/声明面变化由此生效,不再粘住);
|
|
277
|
+
* ② transitions 为空且 frictionTotal ≥ 阈值 → 同样整档换、不设地板——后续 escalateByFriction
|
|
278
|
+
* 在同一锁内按既有 +1 语义即时兑现摩擦价(重算 S1 → 终 S2),摩擦价不因追赶丢失也不预支
|
|
279
|
+
* (可达入参:gate 失败先记摩擦于级联前段、升档检查点在级联末位,Grill P1-1 边界);
|
|
280
|
+
* ③ transitions 非空 → 最终档 = maxTier(重算档, 摩擦地板),地板 = transitions 内最高 to 档
|
|
281
|
+
* (已付摩擦价不白付;后续 escalate 若仍超阈按既有语义再 +1,封顶 S3)。
|
|
282
|
+
* 重定价不记 transitions(该数组专属摩擦迁移审计);transitions 原样透传。
|
|
283
|
+
*
|
|
284
|
+
* @param {object} opts
|
|
285
|
+
* @param {{tier: string, components?: object, reasons?: string[], transitions?: Array<{to: string}>}} [opts.currentDoc] 现档位 doc(读档侧)
|
|
286
|
+
* @param {string} opts.recomputedTier 按当前声明面重算的档(computeInitialCeremonyTierDoc 产物 tier)
|
|
287
|
+
* @param {object} [opts.recomputedComponents] 重算分量(透传进新 doc)
|
|
288
|
+
* @param {string[]} [opts.recomputedReasons] 重算 reasons(事件文案由调用方以参数区分——追赶场景不追加「初始档/首见」行)
|
|
289
|
+
* @param {object} [opts.frictionCounts] ledger 累计账口径(携 verify_run_failed 第三键透传容忍)
|
|
290
|
+
* @returns {{ tier: string, components: object, reasons: string[], transitions: Array<object> }}
|
|
291
|
+
*/
|
|
292
|
+
export function applyDeclarationCatchUp({ currentDoc, recomputedTier, recomputedComponents = {}, recomputedReasons = [], frictionCounts } = {}) {
|
|
293
|
+
const transitions = Array.isArray(currentDoc && currentDoc.transitions) ? currentDoc.transitions : []
|
|
294
|
+
const baseReasons = Array.isArray(currentDoc && currentDoc.reasons) ? currentDoc.reasons : []
|
|
295
|
+
const recomputed = CEREMONY_TIERS.includes(recomputedTier) ? recomputedTier : (currentDoc && currentDoc.tier) || 'S1'
|
|
296
|
+
const frictionOver = frictionTotal(frictionCounts) >= FRICTION_ESCALATION_THRESHOLD
|
|
297
|
+
if (transitions.length === 0) {
|
|
298
|
+
// 分支①②:无摩擦迁移——整档换(超阈不设地板,摩擦价交由同锁 escalate 即时兑现)
|
|
299
|
+
return {
|
|
300
|
+
tier: recomputed,
|
|
301
|
+
components: recomputedComponents,
|
|
302
|
+
reasons: [
|
|
303
|
+
...recomputedReasons,
|
|
304
|
+
`声明追赶重定价(${(currentDoc && currentDoc.tier) || '无'} → ${recomputed},无摩擦迁移${frictionOver ? '但摩擦已超阈——地板不设,同锁 escalate 即时 +1 兑现' : '且未超阈'},blast/span 分量随当前声明面刷新,D-003)`,
|
|
305
|
+
],
|
|
306
|
+
transitions,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// 分支③:摩擦地板不退——最终档 = max(重算档, transitions 最高 to)
|
|
310
|
+
const floor = transitions.reduce((acc, t) => maxTier(acc, t && t.to), 'S0')
|
|
311
|
+
const final = maxTier(recomputed, floor)
|
|
312
|
+
return {
|
|
313
|
+
tier: final,
|
|
314
|
+
components: recomputedComponents,
|
|
315
|
+
reasons: [
|
|
316
|
+
...recomputedReasons,
|
|
317
|
+
...baseReasons.slice(-3), // 摩擦地板语境留最近三条旧痕防断代(全部保留会随每道门膨胀)
|
|
318
|
+
`摩擦地板 ${floor} 不退,重算 ${recomputed} 被地板托底 → ${final}(transitions ${transitions.length} 条在场,D-003)`,
|
|
319
|
+
],
|
|
320
|
+
transitions,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 双跑收口对账(verify --done / archive confirm 两出口):事实面仅重跑 blast+span 两轴
|
|
326
|
+
* (实际 diff 无声明通道;摩擦属过程面,阶段门已计过价,不重复升档),与开跑声明档比对。
|
|
327
|
+
* 声明档低于事实档 → mismatch=true、severity='error'(懒 agent 低报硬 flag);声明档高于
|
|
328
|
+
* 事实档 → 高报态 severity='warn'(D-005:只记账不阻断——高报代价是更重仪式自罚机制天然
|
|
329
|
+
* 在场,硬拦会把「误伤粘住」变成「收口再打回一轮」反向涨 token);等档 'none'。声明档
|
|
330
|
+
* 非法/缺失按最低档对账(fail-safe:无法证明声明到位 → 视为低报,宁严勿松)。
|
|
331
|
+
*
|
|
332
|
+
* @param {object} [opts]
|
|
333
|
+
* @param {string} [opts.declaredTier] 开跑定价落档的声明档
|
|
334
|
+
* @param {object} [opts.factRiskDetection] legacy 事实面判级输入(新调用走 factBlastTier)
|
|
335
|
+
* @param {string} [opts.factBlastTier] 实际 diff × 声明面判级 tier(resolveChangeRisk 产物——
|
|
336
|
+
* 在场合跳过 factRiskDetection 映射直入,2026-09-19-ceremony-pricing-five-cuts task-03)
|
|
337
|
+
* @param {string[]} [opts.factFiles] 实际变更文件(scope-audit / apply-pathspec 文件集)
|
|
338
|
+
* @param {object|null} [opts.factModuleIndex] _module-map.yaml 解析结果
|
|
339
|
+
* @param {Array<{pattern: string, re: RegExp}>} [opts.factSpanRiskPatterns] 事实面 span 风险
|
|
340
|
+
* 声明表(穿透内部 computeCeremonyTier 的 spanRiskPatterns;与 factBlastTier 同装载源
|
|
341
|
+
* ——项目声明 span_risk 段,默认 [] = 模式维度关闭,2026-09-19-span-risk-pattern-migration D-003)
|
|
342
|
+
* @returns {{ factTier: 'S0'|'S1'|'S2'|'S3', mismatch: boolean, severity: 'error'|'warn'|'none' }}
|
|
343
|
+
*/
|
|
344
|
+
export function reconcileDualRun({ declaredTier, factRiskDetection, factBlastTier, factFiles, factModuleIndex, factSpanRiskPatterns = [] } = {}) {
|
|
345
|
+
const fact = computeCeremonyTier({
|
|
346
|
+
riskDetection: factRiskDetection,
|
|
347
|
+
blastTier: factBlastTier,
|
|
348
|
+
declaredFiles: factFiles,
|
|
349
|
+
moduleIndex: factModuleIndex,
|
|
350
|
+
spanRiskPatterns: factSpanRiskPatterns,
|
|
351
|
+
frictionCounts: {},
|
|
352
|
+
})
|
|
353
|
+
const declaredRank = tierRank(declaredTier)
|
|
354
|
+
const factRank = tierRank(fact.tier)
|
|
355
|
+
// 声明档非法/缺失(rank<0)按最低档对账 → 事实高于声明即 error(fail-safe 宁严勿松)
|
|
356
|
+
const mismatch = factRank > declaredRank
|
|
357
|
+
const overReport = declaredRank > factRank && declaredRank >= 0
|
|
358
|
+
return { factTier: fact.tier, mismatch, severity: mismatch ? 'error' : overReport ? 'warn' : 'none' }
|
|
359
|
+
}
|