@twinsearth/dsh-master 3.2.10 → 3.3.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/README.md +3 -0
- package/client.js +50 -0
- package/dist-host/cost/attribution.js +183 -0
- package/dist-host/cost/audit.js +163 -0
- package/dist-host/cost/budget.js +211 -0
- package/dist-host/cost/bus.js +95 -0
- package/dist-host/cost/cert-chain.js +100 -0
- package/dist-host/cost/experiment.js +132 -0
- package/dist-host/cost/gate.js +189 -0
- package/dist-host/cost/host-surface.js +80 -0
- package/dist-host/cost/index.js +312 -0
- package/dist-host/cost/ledger-report.js +193 -0
- package/dist-host/cost/ledger.js +401 -0
- package/dist-host/cost/metrics.js +198 -0
- package/dist-host/cost/milestones.js +171 -0
- package/dist-host/cost/modules/index.js +5 -0
- package/dist-host/cost/modules/manifests.js +130 -0
- package/dist-host/cost/policy.js +276 -0
- package/dist-host/cost/report.js +156 -0
- package/dist-host/cost/reuse-report.js +128 -0
- package/dist-host/cost/rsi.js +367 -0
- package/dist-host/cost/toolface.js +147 -0
- package/dist-host/cost/types.js +31 -0
- package/dist-host/kernel.js +28 -2
- package/dist-host/plugins/cost-optimizer.js +277 -27
- package/dist-host/plugins/superagent-router.js +88 -1
- package/dist-host/superagent/types.js +8 -0
- package/dist-worker/agent-loop.js +449 -7
- package/dist-worker/baseline-fit.js +66 -0
- package/dist-worker/budget-guard.js +93 -0
- package/dist-worker/ccs.js +99 -0
- package/dist-worker/cost-hooks.js +115 -0
- package/dist-worker/cost-modules.js +330 -0
- package/dist-worker/cost-spi.js +3 -0
- package/dist-worker/edg.js +372 -0
- package/dist-worker/gdr.js +159 -0
- package/dist-worker/index.js +80 -2
- package/dist-worker/llm-http.js +20 -0
- package/dist-worker/sink-http.js +1 -0
- package/dist-worker/spill.js +115 -0
- package/dist-worker/tool-batch.js +66 -0
- package/dist-worker/tool-compact.js +275 -0
- package/dist-worker/tool-disclosure.js +90 -0
- package/dist-worker/tools.js +16 -0
- package/docs/ARCHITECTURE.md +21 -0
- package/docs/CHANGELOG.md +50 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +1053 -0
- package/docs/EXPERIMENT-M5-real-ab.md +82 -0
- package/docs/THIRD-PARTY.md +87 -0
- package/package.json +1 -1
- package/src/cost/attribution.ts +186 -0
- package/src/cost/audit.ts +170 -0
- package/src/cost/budget.ts +226 -0
- package/src/cost/bus.ts +97 -0
- package/src/cost/cert-chain.ts +102 -0
- package/src/cost/experiment.ts +136 -0
- package/src/cost/gate.ts +191 -0
- package/src/cost/host-surface.ts +97 -0
- package/src/cost/index.ts +316 -0
- package/src/cost/ledger-report.ts +186 -0
- package/src/cost/ledger.ts +385 -0
- package/src/cost/metrics.ts +193 -0
- package/src/cost/milestones.ts +170 -0
- package/src/cost/modules/index.ts +13 -0
- package/src/cost/modules/manifests.ts +129 -0
- package/src/cost/policy.ts +276 -0
- package/src/cost/report.ts +158 -0
- package/src/cost/reuse-report.ts +131 -0
- package/src/cost/rsi.ts +364 -0
- package/src/cost/toolface.ts +151 -0
- package/src/cost/types.ts +298 -0
- package/src/kernel.ts +27 -2
- package/src/plugins/cost-optimizer.ts +271 -26
- package/src/plugins/superagent-router.ts +82 -1
- package/src/superagent/types.ts +70 -0
- package/worker/agent-loop.ts +444 -7
- package/worker/baseline-fit.ts +80 -0
- package/worker/budget-guard.ts +156 -0
- package/worker/ccs.ts +202 -0
- package/worker/cost-hooks.ts +133 -0
- package/worker/cost-modules.ts +356 -0
- package/worker/cost-spi.ts +283 -0
- package/worker/edg.ts +424 -0
- package/worker/gdr.ts +233 -0
- package/worker/index.ts +84 -3
- package/worker/llm-http.ts +18 -0
- package/worker/sink-http.ts +5 -2
- package/worker/spill.ts +133 -0
- package/worker/tool-batch.ts +124 -0
- package/worker/tool-compact.ts +317 -0
- package/worker/tool-disclosure.ts +131 -0
- package/worker/tools.ts +553 -531
- package/worker/types.ts +191 -0
package/src/cost/rsi.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 3.3.0 · B4 `rsi`(离线递归自改进,设计 §7)。
|
|
7
|
+
*
|
|
8
|
+
* **位置:离线**。本文件不属于 Worker 热路径,不 import 任何 worker 模块,
|
|
9
|
+
* 也不会被插件 `apply()` 调用 —— 它只在运维侧被显式执行(见 `scripts/rsi-offline.mjs`)。
|
|
10
|
+
*
|
|
11
|
+
* 输入(全部来自已落库的事实):
|
|
12
|
+
* * ledger 快照(`status().ledger`)—— 省了多少、哪些模块、估算占比;
|
|
13
|
+
* * 替换证书(`audit.substitutions()`)—— 哪些边真的被复用;
|
|
14
|
+
* * 失败原因分布(`WorkerResult.cost.ccs.guard.byReason` 汇总)—— 为什么没命中。
|
|
15
|
+
*
|
|
16
|
+
* 产物(**候选配置**):
|
|
17
|
+
* * `guard-relax` —— 守卫表达式调整(针对 guard-miss 集中处);
|
|
18
|
+
* * `template-add` —— 把反复出现的成功边固化成模板;
|
|
19
|
+
* * `guidance-rewrite`—— 批量/披露教学文案改写(针对 shape 纠偏次数)。
|
|
20
|
+
*
|
|
21
|
+
* 纪律:
|
|
22
|
+
* * 每个候选**必须**过验证集打分(`score()`),未达阈值**不得冻结**;
|
|
23
|
+
* * `freeze()` 产出冻结清单条目(含输入摘要 + 打分 + 阈值 + 时间),
|
|
24
|
+
* **冻结后才允许被 active 使用**;
|
|
25
|
+
* * 报表把「候选」与「基线」放在同一张表里对比,禁止只报候选的漂亮数字;
|
|
26
|
+
* * 3.3.0 只交付**可复现的离线流程 + 冻结清单**,不做自动上线。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import crypto from 'node:crypto'
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ *
|
|
32
|
+
* 输入归一化(可复现的关键:一切排序 + 稳定序列化)
|
|
33
|
+
* ------------------------------------------------------------------ */
|
|
34
|
+
|
|
35
|
+
function canonical(value) {
|
|
36
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonical(item)).join(',')}]`
|
|
37
|
+
if (value && typeof value === 'object') {
|
|
38
|
+
const keys = Object.keys(value).sort()
|
|
39
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'null'
|
|
42
|
+
if (value === undefined) return 'null'
|
|
43
|
+
return JSON.stringify(value) ?? 'null'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function digestOf(value) {
|
|
47
|
+
return `sha256:${crypto.createHash('sha256').update(canonical(value)).digest('hex')}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MISS_REASONS = Object.freeze([
|
|
51
|
+
'no-edge',
|
|
52
|
+
'tool-mismatch',
|
|
53
|
+
'schema-drift',
|
|
54
|
+
'dependency-incomplete',
|
|
55
|
+
'guard-miss',
|
|
56
|
+
'arg-unextractable',
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{
|
|
61
|
+
* ledger?: object, certificates?: object[], guardStats?: {byReason?: object, matches?: number, hits?: number},
|
|
62
|
+
* corrections?: number, generatedAt?: number,
|
|
63
|
+
* }} input
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeInput(input = {}) {
|
|
66
|
+
const byReason = {}
|
|
67
|
+
for (const reason of MISS_REASONS) byReason[reason] = Math.max(0, Number(input?.guardStats?.byReason?.[reason] ?? 0) || 0)
|
|
68
|
+
const certificates = (Array.isArray(input?.certificates) ? input.certificates : [])
|
|
69
|
+
.map((cert) => ({
|
|
70
|
+
edgeId: String(cert?.edgeId ?? ''),
|
|
71
|
+
toolName: String(cert?.toolName ?? ''),
|
|
72
|
+
guardExpr: String(cert?.guardExpr ?? ''),
|
|
73
|
+
dispatched: cert?.dispatched !== false,
|
|
74
|
+
savedTokens: Math.max(0, Number(cert?.savedTokens ?? 0) || 0),
|
|
75
|
+
}))
|
|
76
|
+
.sort((a, b) => `${a.edgeId}|${a.toolName}`.localeCompare(`${b.edgeId}|${b.toolName}`))
|
|
77
|
+
return {
|
|
78
|
+
ledger: {
|
|
79
|
+
ownExecutionSaved: Math.max(0, Number(input?.ledger?.ownExecutionSaved ?? 0) || 0),
|
|
80
|
+
governanceTotal: Math.max(0, Number(input?.ledger?.governance?.total ?? 0) || 0),
|
|
81
|
+
measuredTokens: Math.max(0, Number(input?.ledger?.tokens?.measuredTokens ?? 0) || 0),
|
|
82
|
+
estimatedTokens: Math.max(0, Number(input?.ledger?.tokens?.estimatedTokens ?? 0) || 0),
|
|
83
|
+
},
|
|
84
|
+
certificates,
|
|
85
|
+
guardStats: {
|
|
86
|
+
matches: Math.max(0, Number(input?.guardStats?.matches ?? 0) || 0),
|
|
87
|
+
hits: Math.max(0, Number(input?.guardStats?.hits ?? 0) || 0),
|
|
88
|
+
byReason,
|
|
89
|
+
},
|
|
90
|
+
corrections: Math.max(0, Number(input?.corrections ?? 0) || 0),
|
|
91
|
+
generatedAt: Number(input?.generatedAt ?? 0) || 0,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ------------------------------------------------------------------ *
|
|
96
|
+
* 分析 → 候选配置
|
|
97
|
+
* ------------------------------------------------------------------ */
|
|
98
|
+
|
|
99
|
+
export function analyze(input = {}) {
|
|
100
|
+
const data = normalizeInput(input)
|
|
101
|
+
const total = data.guardStats.matches
|
|
102
|
+
const hits = data.guardStats.hits
|
|
103
|
+
const hitRate = total > 0 ? hits / total : 0
|
|
104
|
+
const findings = []
|
|
105
|
+
|
|
106
|
+
const reason = (name) => data.guardStats.byReason[name] ?? 0
|
|
107
|
+
if (reason('guard-miss') > 0) findings.push({ key: 'guard-miss', count: reason('guard-miss'), note: '守卫过严或回执形态变化' })
|
|
108
|
+
if (reason('arg-unextractable') > 0) findings.push({ key: 'arg-unextractable', count: reason('arg-unextractable'), note: '参数来源无法确定性提取' })
|
|
109
|
+
if (reason('schema-drift') > 0) findings.push({ key: 'schema-drift', count: reason('schema-drift'), note: '工具 schema 变更导致旧图作废' })
|
|
110
|
+
if (reason('tool-mismatch') > 0 || reason('no-edge') > 0) findings.push({ key: 'coverage-gap', count: reason('tool-mismatch') + reason('no-edge'), note: '图里缺边' })
|
|
111
|
+
if (reason('dependency-incomplete') > 0) findings.push({ key: 'dependency-incomplete', count: reason('dependency-incomplete'), note: '依赖链过长' })
|
|
112
|
+
if (data.corrections > 0) findings.push({ key: 'batch-shape', count: data.corrections, note: '批量形状纠偏触发' })
|
|
113
|
+
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
ok: true,
|
|
116
|
+
inputDigest: digestOf(data),
|
|
117
|
+
data,
|
|
118
|
+
baseline: Object.freeze({
|
|
119
|
+
hitRate,
|
|
120
|
+
matches: total,
|
|
121
|
+
hits,
|
|
122
|
+
savedTokens: data.ledger.ownExecutionSaved,
|
|
123
|
+
governanceTokens: data.ledger.governanceTotal,
|
|
124
|
+
estimatedRatio: data.ledger.measuredTokens + data.ledger.estimatedTokens > 0
|
|
125
|
+
? data.ledger.estimatedTokens / (data.ledger.measuredTokens + data.ledger.estimatedTokens)
|
|
126
|
+
: 0,
|
|
127
|
+
}),
|
|
128
|
+
findings: findings.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 从分析结果生成**候选配置**(只提案,不生效)。
|
|
134
|
+
* 每个候选都带 `requiresValidation: true`,未过验证集不得冻结。
|
|
135
|
+
*/
|
|
136
|
+
export function proposeCandidates(analysis) {
|
|
137
|
+
const findings = Array.isArray(analysis?.findings) ? analysis.findings : []
|
|
138
|
+
const certificates = Array.isArray(analysis?.data?.certificates) ? analysis.data.certificates : []
|
|
139
|
+
const candidates = []
|
|
140
|
+
|
|
141
|
+
const guardMiss = findings.find((item) => item.key === 'guard-miss')
|
|
142
|
+
if (guardMiss) {
|
|
143
|
+
candidates.push({
|
|
144
|
+
id: 'cand_guard_relax',
|
|
145
|
+
kind: 'guard-relax',
|
|
146
|
+
rationale: `guard-miss ${guardMiss.count} 次:守卫过于严格`,
|
|
147
|
+
change: { action: 'relax-guard', target: 'edges-with-frequent-guard-miss', note: '把 equals 放宽为 regex,或增加 negate 分支' },
|
|
148
|
+
risk: 'high',
|
|
149
|
+
requiresValidation: true,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
const unextractable = findings.find((item) => item.key === 'arg-unextractable')
|
|
153
|
+
if (unextractable) {
|
|
154
|
+
candidates.push({
|
|
155
|
+
id: 'cand_arg_source',
|
|
156
|
+
kind: 'guard-relax',
|
|
157
|
+
rationale: `arg-unextractable ${unextractable.count} 次:参数来源不可确定性提取`,
|
|
158
|
+
change: { action: 'switch-arg-source', from: 'literal', to: 'jsonpath', note: '改用回执里稳定出现的字段' },
|
|
159
|
+
risk: 'medium',
|
|
160
|
+
requiresValidation: true,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
// 反复成功的边 → 固化为模板
|
|
164
|
+
const successByEdge = new Map()
|
|
165
|
+
for (const cert of certificates) {
|
|
166
|
+
if (!cert.dispatched) continue
|
|
167
|
+
const key = `${cert.edgeId}|${cert.toolName}|${cert.guardExpr}`
|
|
168
|
+
successByEdge.set(key, (successByEdge.get(key) ?? 0) + Math.max(1, cert.savedTokens > 0 ? 1 : 0))
|
|
169
|
+
}
|
|
170
|
+
const repeatable = [...successByEdge.entries()].filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
171
|
+
if (repeatable.length > 0) {
|
|
172
|
+
candidates.push({
|
|
173
|
+
id: 'cand_template_add',
|
|
174
|
+
kind: 'template-add',
|
|
175
|
+
rationale: `${repeatable.length} 条边被重复复用 ≥2 次,值得固化进模板库`,
|
|
176
|
+
change: { action: 'add-templates', edges: repeatable.map(([key]) => key.split('|')[0]) },
|
|
177
|
+
risk: 'low',
|
|
178
|
+
requiresValidation: true,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
if (findings.some((item) => item.key === 'batch-shape')) {
|
|
182
|
+
candidates.push({
|
|
183
|
+
id: 'cand_guidance',
|
|
184
|
+
kind: 'guidance-rewrite',
|
|
185
|
+
rationale: '批量形状纠偏仍在触发,教学文案可再收紧',
|
|
186
|
+
change: { action: 'rewrite-guidance', target: 'tool-batch.guidance', note: '把反例写得更具体' },
|
|
187
|
+
risk: 'low',
|
|
188
|
+
requiresValidation: true,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
return Object.freeze(candidates.sort((a, b) => a.id.localeCompare(b.id)))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ------------------------------------------------------------------ *
|
|
195
|
+
* 验证集打分 → 冻结
|
|
196
|
+
* ------------------------------------------------------------------ */
|
|
197
|
+
|
|
198
|
+
const DEFAULT_THRESHOLDS = Object.freeze({ minAccuracy: 0.98, maxDestructiveHits: 0, minSamples: 20 })
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 用验证集给候选打分。验证集必须**独立于**生成候选的样本(调用方保证)。
|
|
202
|
+
* @param {{candidate: object, validation: {samples?: number, correct?: number, destructiveHits?: number, savedTokens?: number}}} input
|
|
203
|
+
*/
|
|
204
|
+
export function score(input = {}) {
|
|
205
|
+
const candidate = input?.candidate
|
|
206
|
+
if (!candidate || !candidate.id) return { ok: false, code: 'candidate-invalid', message: 'candidate with id required' }
|
|
207
|
+
const samples = Math.max(0, Number(input?.validation?.samples ?? 0) || 0)
|
|
208
|
+
const correct = Math.max(0, Number(input?.validation?.correct ?? 0) || 0)
|
|
209
|
+
const destructiveHits = Math.max(0, Number(input?.validation?.destructiveHits ?? 0) || 0)
|
|
210
|
+
const savedTokens = Math.max(0, Number(input?.validation?.savedTokens ?? 0) || 0)
|
|
211
|
+
if (correct > samples) return { ok: false, code: 'validation-invalid', message: 'correct cannot exceed samples' }
|
|
212
|
+
const accuracy = samples > 0 ? correct / samples : 0
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
candidateId: String(candidate.id),
|
|
216
|
+
samples,
|
|
217
|
+
correct,
|
|
218
|
+
accuracy,
|
|
219
|
+
destructiveHits,
|
|
220
|
+
savedTokens,
|
|
221
|
+
scoredAt: Date.now(),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 冻结:**未达阈值一律拒绝**。产出冻结清单条目(含输入摘要,可复现核对)。
|
|
227
|
+
* @param {{candidate: object, score: object, analysis: object, thresholds?: object, now?: () => number}} input
|
|
228
|
+
*/
|
|
229
|
+
export function freeze(input = {}) {
|
|
230
|
+
const candidate = input?.candidate
|
|
231
|
+
const scoreValue = input?.score
|
|
232
|
+
const analysis = input?.analysis
|
|
233
|
+
if (!candidate?.id) return { ok: false, code: 'candidate-invalid', message: 'candidate with id required' }
|
|
234
|
+
if (!scoreValue || scoreValue.ok !== true) return { ok: false, code: 'score-missing', message: 'candidate must be scored before freezing' }
|
|
235
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...(input?.thresholds ?? {}) }
|
|
236
|
+
const reasons = []
|
|
237
|
+
if (scoreValue.samples < thresholds.minSamples) reasons.push('samples-below-threshold')
|
|
238
|
+
if (scoreValue.accuracy < thresholds.minAccuracy) reasons.push('accuracy-below-threshold')
|
|
239
|
+
if (scoreValue.destructiveHits > thresholds.maxDestructiveHits) reasons.push('destructive-hits')
|
|
240
|
+
if (!analysis?.inputDigest) reasons.push('input-digest-missing')
|
|
241
|
+
if (reasons.length > 0) return { ok: false, code: 'freeze-refused', reasons }
|
|
242
|
+
|
|
243
|
+
const entry = Object.freeze({
|
|
244
|
+
candidateId: String(candidate.id),
|
|
245
|
+
kind: String(candidate.kind ?? 'unknown'),
|
|
246
|
+
risk: String(candidate.risk ?? 'unknown'),
|
|
247
|
+
changeDigest: digestOf(candidate.change ?? {}),
|
|
248
|
+
inputDigest: analysis.inputDigest,
|
|
249
|
+
validation: Object.freeze({
|
|
250
|
+
samples: scoreValue.samples,
|
|
251
|
+
correct: scoreValue.correct,
|
|
252
|
+
accuracy: scoreValue.accuracy,
|
|
253
|
+
destructiveHits: scoreValue.destructiveHits,
|
|
254
|
+
savedTokens: scoreValue.savedTokens,
|
|
255
|
+
}),
|
|
256
|
+
thresholds: Object.freeze({ ...thresholds }),
|
|
257
|
+
frozenAt: typeof input?.now === 'function' ? input.now() : Date.now(),
|
|
258
|
+
/** 冻结后才允许 active 使用;冻结 ≠ 自动上线。 */
|
|
259
|
+
usableByActive: true,
|
|
260
|
+
})
|
|
261
|
+
return { ok: true, entry }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 生成报表:候选与基线**同表展示**(防止只展示候选的漂亮数字)。
|
|
266
|
+
*/
|
|
267
|
+
export function report({ analysis, scored = [], frozen = [] }) {
|
|
268
|
+
const baseline = analysis?.baseline ?? { hitRate: 0, matches: 0, hits: 0, savedTokens: 0, governanceTokens: 0, estimatedRatio: 0 }
|
|
269
|
+
const rows = [
|
|
270
|
+
{
|
|
271
|
+
id: 'baseline',
|
|
272
|
+
kind: 'baseline',
|
|
273
|
+
hitRate: baseline.hitRate,
|
|
274
|
+
samples: baseline.matches,
|
|
275
|
+
accuracy: null,
|
|
276
|
+
savedTokens: baseline.savedTokens,
|
|
277
|
+
governanceTokens: baseline.governanceTokens,
|
|
278
|
+
status: 'baseline',
|
|
279
|
+
},
|
|
280
|
+
...scored.map((item) => {
|
|
281
|
+
const frozenEntry = frozen.find((entry) => entry.candidateId === item.candidateId)
|
|
282
|
+
return {
|
|
283
|
+
id: item.candidateId,
|
|
284
|
+
kind: 'candidate',
|
|
285
|
+
hitRate: null,
|
|
286
|
+
samples: item.samples,
|
|
287
|
+
accuracy: item.accuracy,
|
|
288
|
+
savedTokens: item.savedTokens,
|
|
289
|
+
governanceTokens: null,
|
|
290
|
+
status: frozenEntry ? 'frozen' : 'candidate',
|
|
291
|
+
}
|
|
292
|
+
}),
|
|
293
|
+
]
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
ok: true,
|
|
296
|
+
inputDigest: analysis?.inputDigest ?? null,
|
|
297
|
+
generatedAt: Date.now(),
|
|
298
|
+
findings: analysis?.findings ?? [],
|
|
299
|
+
rows,
|
|
300
|
+
frozen: frozen.map((entry) => ({ ...entry })),
|
|
301
|
+
/** 3.3.0 不做自动上线:清单之外的一切都只是提案。 */
|
|
302
|
+
autoDeploy: false,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* M5 交付物:**冻结清单**(可持久化、可复核)。
|
|
308
|
+
*
|
|
309
|
+
* 清单只描述"在什么数据、什么阈值下冻结了什么",不含执行能力:
|
|
310
|
+
* * `inputDigest`:生成候选时的输入摘要(同一输入必得同一摘要);
|
|
311
|
+
* * `entries`:逐条冻结记录,按 candidateId 排序保证可 diff;
|
|
312
|
+
* * `manifestDigest`:整份清单的摘要,用于事后核对「清单没被改过」。
|
|
313
|
+
*/
|
|
314
|
+
export function buildFreezeManifest({ analysis, frozen = [], now } = {}) {
|
|
315
|
+
const entries = [...(Array.isArray(frozen) ? frozen : [])]
|
|
316
|
+
.map((entry) => ({ ...entry }))
|
|
317
|
+
.sort((a, b) => String(a.candidateId).localeCompare(String(b.candidateId)))
|
|
318
|
+
const manifest = {
|
|
319
|
+
kind: 'rsi-freeze-manifest',
|
|
320
|
+
version: 1,
|
|
321
|
+
inputDigest: analysis?.inputDigest ?? null,
|
|
322
|
+
thresholds: { ...DEFAULT_THRESHOLDS },
|
|
323
|
+
generatedAt: Number(now ?? 0) || 0,
|
|
324
|
+
entries,
|
|
325
|
+
/** 3.3.0 不自动上线:清单只声明「可被 active 使用」,上线仍是人工动作。 */
|
|
326
|
+
autoDeploy: false,
|
|
327
|
+
}
|
|
328
|
+
return Object.freeze({ ...manifest, manifestDigest: digestOf(manifest) })
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 复核冻结清单:清单没被改动、输入没变、条目仍然满足阈值。
|
|
333
|
+
* 返回 `issues[]` —— 任何一条都意味着"这份清单不能再作为依据"。
|
|
334
|
+
*/
|
|
335
|
+
export function verifyFreezeManifest(manifest, { analysis, thresholds } = {}) {
|
|
336
|
+
const issues = []
|
|
337
|
+
if (!manifest || manifest.kind !== 'rsi-freeze-manifest') issues.push('manifest-kind-invalid')
|
|
338
|
+
const entries = Array.isArray(manifest?.entries) ? manifest.entries : []
|
|
339
|
+
if (entries.length === 0) issues.push('manifest-empty')
|
|
340
|
+
if (manifest?.autoDeploy !== false) issues.push('manifest-must-not-auto-deploy')
|
|
341
|
+
|
|
342
|
+
// 清单自身完整性:重算摘要必须一致(防止事后手改)
|
|
343
|
+
const body = { ...manifest }
|
|
344
|
+
delete body.manifestDigest
|
|
345
|
+
if (manifest?.manifestDigest !== digestOf(body)) issues.push('manifest-digest-mismatch')
|
|
346
|
+
|
|
347
|
+
// 输入漂移:候选是在旧数据上冻结的,就不能再当依据
|
|
348
|
+
if (analysis?.inputDigest && manifest?.inputDigest && analysis.inputDigest !== manifest.inputDigest) {
|
|
349
|
+
issues.push('input-digest-drift')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const limits = { ...DEFAULT_THRESHOLDS, ...(thresholds ?? {}) }
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
const validation = entry?.validation ?? {}
|
|
355
|
+
if (Number(validation.samples ?? 0) < limits.minSamples) issues.push(`entry-samples-below:${entry?.candidateId}`)
|
|
356
|
+
if (Number(validation.accuracy ?? 0) < limits.minAccuracy) issues.push(`entry-accuracy-below:${entry?.candidateId}`)
|
|
357
|
+
if (Number(validation.destructiveHits ?? 0) > limits.maxDestructiveHits) issues.push(`entry-destructive-hits:${entry?.candidateId}`)
|
|
358
|
+
if (entry?.usableByActive !== true) issues.push(`entry-not-usable:${entry?.candidateId}`)
|
|
359
|
+
}
|
|
360
|
+
return Object.freeze({ ok: issues.length === 0, issues: Object.freeze(issues), entries: entries.length })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export const RSI_THRESHOLDS = DEFAULT_THRESHOLDS
|
|
364
|
+
export { MISS_REASONS }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 3.3.0 · M2 交付物:**工具面 token 下降 + 归因**。
|
|
7
|
+
*
|
|
8
|
+
* 两件事必须分开算,因为它们属于两本不同的账:
|
|
9
|
+
*
|
|
10
|
+
* * **A1 紧凑输出**:回执变短 ⇒ 每次交互更便宜 ⇒ **execution 节省**。
|
|
11
|
+
* 默认 `shadow` 时只能报「本可节省」,不许算成真节省。
|
|
12
|
+
* * **A2 渐进纰漏**:常驻工具面收敛 ⇒ 每轮 schema 更少;但 `load_tools` 的装载
|
|
13
|
+
* 是**作者/选择成本** ⇒ **governance 账**(与 §5.1 一致),永不混进节省。
|
|
14
|
+
*
|
|
15
|
+
* 关闭 A2 时必须 `reductionRatio === 0`(fail-open 回全量工具面),这是可测的。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const num = (value) => {
|
|
19
|
+
const n = Number(value)
|
|
20
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
21
|
+
}
|
|
22
|
+
const tokensOf = (text) => (String(text ?? '').length > 0 ? Math.ceil(String(text).length / 4) : 0)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 计算工具面:全量(基线)vs 可见(常驻 + 已加载)。
|
|
26
|
+
* @param {{
|
|
27
|
+
* schemas: Array<{name: string, description?: string, parameters?: unknown}>,
|
|
28
|
+
* enabled?: boolean,
|
|
29
|
+
* resident?: string[],
|
|
30
|
+
* loaded?: string[],
|
|
31
|
+
* }} input
|
|
32
|
+
*/
|
|
33
|
+
export function planToolFace(input = {}) {
|
|
34
|
+
const schemas = Array.isArray(input.schemas) ? input.schemas : []
|
|
35
|
+
const enabled = input.enabled === true
|
|
36
|
+
const resident = new Set(Array.isArray(input.resident) ? input.resident.map(String) : [])
|
|
37
|
+
const loaded = new Set(Array.isArray(input.loaded) ? input.loaded.map(String) : [])
|
|
38
|
+
|
|
39
|
+
const tokenOfSchema = (schema) => tokensOf(JSON.stringify({ name: schema?.name, description: schema?.description, parameters: schema?.parameters }))
|
|
40
|
+
const baselineTokens = schemas.reduce((sum, schema) => sum + tokenOfSchema(schema), 0)
|
|
41
|
+
const visible = enabled ? schemas.filter((schema) => resident.has(String(schema?.name)) || loaded.has(String(schema?.name))) : schemas
|
|
42
|
+
const residentTokens = visible.reduce((sum, schema) => sum + tokenOfSchema(schema), 0)
|
|
43
|
+
const savedTokens = Math.max(0, baselineTokens - residentTokens)
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
enabled,
|
|
46
|
+
baseline: Object.freeze({ count: schemas.length, tokens: baselineTokens }),
|
|
47
|
+
resident: Object.freeze({ count: visible.length, tokens: residentTokens, names: visible.map((schema) => String(schema?.name)) }),
|
|
48
|
+
savedTokens,
|
|
49
|
+
reductionRatio: baselineTokens > 0 ? savedTokens / baselineTokens : 0,
|
|
50
|
+
/** 关闭时必须是 0(fail-open 回全量),调用方可直接断言。 */
|
|
51
|
+
perTurn: Object.freeze({ tokensBefore: baselineTokens, tokensAfter: residentTokens }),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* M2 报表:工具面下降(A2) + 回执压缩(A1) + 归因分列。
|
|
57
|
+
*
|
|
58
|
+
* @param {{
|
|
59
|
+
* schemas?: object[],
|
|
60
|
+
* disclosure?: {enabled?: boolean, resident?: string[], loaded?: string[]},
|
|
61
|
+
* compactions?: Array<{toolName?: string, applied?: boolean, evaluated?: boolean, estimated?: boolean, savedTokens?: number, savedChars?: number}>,
|
|
62
|
+
* loads?: Array<{toolName?: string, tokens?: number}>,
|
|
63
|
+
* mode?: 'off' | 'shadow' | 'enforce',
|
|
64
|
+
* }} input
|
|
65
|
+
*/
|
|
66
|
+
export function buildToolFaceReport(input = {}) {
|
|
67
|
+
const disclosure = input.disclosure ?? {}
|
|
68
|
+
const face = planToolFace({ schemas: input.schemas, enabled: disclosure.enabled === true, resident: disclosure.resident, loaded: disclosure.loaded })
|
|
69
|
+
const mode = input.mode === 'off' || input.mode === 'enforce' ? input.mode : 'shadow'
|
|
70
|
+
|
|
71
|
+
const byTool = {}
|
|
72
|
+
let evaluated = 0
|
|
73
|
+
let applied = 0
|
|
74
|
+
let appliedSaved = 0
|
|
75
|
+
let wouldSave = 0
|
|
76
|
+
let estimatedSaved = 0
|
|
77
|
+
for (const event of Array.isArray(input.compactions) ? input.compactions : []) {
|
|
78
|
+
if (event?.evaluated === false) continue
|
|
79
|
+
evaluated += 1
|
|
80
|
+
const saved = num(event?.savedTokens ?? (num(event?.savedChars) / 4))
|
|
81
|
+
if (event?.applied === true) {
|
|
82
|
+
applied += 1
|
|
83
|
+
appliedSaved += saved
|
|
84
|
+
if (event?.estimated === true) estimatedSaved += saved
|
|
85
|
+
} else {
|
|
86
|
+
wouldSave += saved
|
|
87
|
+
}
|
|
88
|
+
const toolName = String(event?.toolName ?? 'unknown')
|
|
89
|
+
if (!byTool[toolName]) byTool[toolName] = { evaluated: 0, applied: 0, savedTokens: 0 }
|
|
90
|
+
byTool[toolName].evaluated += 1
|
|
91
|
+
if (event?.applied === true) {
|
|
92
|
+
byTool[toolName].applied += 1
|
|
93
|
+
byTool[toolName].savedTokens += saved
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A2 的装载成本:进 governance,不进节省
|
|
98
|
+
const loadEvents = Array.isArray(input.loads) ? input.loads.length : 0
|
|
99
|
+
const governanceTokens = (Array.isArray(input.loads) ? input.loads : []).reduce((sum, event) => sum + num(event?.tokens), 0)
|
|
100
|
+
|
|
101
|
+
// A1 的节省口径:只有 enforce 且真的替换了才算执行节省
|
|
102
|
+
const a1ExecutionSaved = mode === 'enforce' ? appliedSaved : 0
|
|
103
|
+
const a1WouldSaveTokens = mode === 'enforce' ? 0 : wouldSave + appliedSaved
|
|
104
|
+
const executionSaved = a1ExecutionSaved
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
ok: true,
|
|
107
|
+
mode,
|
|
108
|
+
toolFace: face,
|
|
109
|
+
a1: Object.freeze({
|
|
110
|
+
module: 'a1-compact',
|
|
111
|
+
evaluated,
|
|
112
|
+
applied,
|
|
113
|
+
/** enforce 且替换成功时才算的节省。 */
|
|
114
|
+
executionSavedTokens: a1ExecutionSaved,
|
|
115
|
+
/** shadow 下「本可节省」——不是节省,报表必须显式标出。 */
|
|
116
|
+
wouldSaveTokens: a1WouldSaveTokens,
|
|
117
|
+
estimatedSavedTokens: estimatedSaved,
|
|
118
|
+
byTool: Object.freeze(byTool),
|
|
119
|
+
}),
|
|
120
|
+
a2: Object.freeze({
|
|
121
|
+
module: 'a2-disclosure',
|
|
122
|
+
enabled: face.enabled,
|
|
123
|
+
loadEvents,
|
|
124
|
+
/** 装载成本属于作者/选择成本 ⇒ governance(永不并入节省)。 */
|
|
125
|
+
governanceTokens,
|
|
126
|
+
perTurnSavedTokens: face.savedTokens,
|
|
127
|
+
}),
|
|
128
|
+
attribution: Object.freeze({
|
|
129
|
+
/** 工具面下降算 A2 的贡献,但**只有在 enabled 时才非 0**。 */
|
|
130
|
+
'a2-disclosure': face.savedTokens,
|
|
131
|
+
'a1-compact': a1ExecutionSaved,
|
|
132
|
+
governance: governanceTokens,
|
|
133
|
+
}),
|
|
134
|
+
totals: Object.freeze({
|
|
135
|
+
executionSaved,
|
|
136
|
+
governanceTokens,
|
|
137
|
+
/** 净收益 = 执行节省(A1,enforce 才有) + 工具面下降(A2,per-turn) − 装载治理成本。 */
|
|
138
|
+
netBenefit: executionSaved + face.savedTokens - governanceTokens,
|
|
139
|
+
/** 两项分列(图省事合并会导致 §8 的重复计风险)。 */
|
|
140
|
+
byKind: Object.freeze({
|
|
141
|
+
toolFaceSavedTokens: face.savedTokens,
|
|
142
|
+
receiptSavedTokens: a1ExecutionSaved,
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
discipline: Object.freeze({
|
|
146
|
+
shadowNeverCountedAsSavings: mode !== 'enforce',
|
|
147
|
+
loadCostIsGovernance: true,
|
|
148
|
+
disabledDisclosureReducesNothing: face.enabled !== true || face.savedTokens >= 0,
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
}
|