@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
|
@@ -0,0 +1,276 @@
|
|
|
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 系统层 · policy(灰度与回滚,设计 §5.3)。
|
|
7
|
+
*
|
|
8
|
+
* 三档语义:
|
|
9
|
+
* * `observe` —— 只计量、只计算命中,**绝不替换**(3.2.x 现状,默认档);
|
|
10
|
+
* * `shadow` —— 决策层真实计算守卫结果并落证书,但**仍调用慢模型**(可对比「本可节省」);
|
|
11
|
+
* * `enforce` —— 守卫命中才真正替换,且受白名单(forkId / task 标签 / 工具集)限制。
|
|
12
|
+
*
|
|
13
|
+
* 粒度:`global → forkId → taskId`,取**最严格**档(observe 最严格,enforce 最宽松)。
|
|
14
|
+
* 默认 observe;enforce 只能由显式配置或用户指令开启(过门禁,不自动升级)。
|
|
15
|
+
* 任何时刻都可降级(回滚优先)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ENFORCE_GATE, POLICY_MODES } from './types.ts'
|
|
19
|
+
|
|
20
|
+
/** 越靠前越严格(限制越少自动化)。 */
|
|
21
|
+
const STRICTNESS = Object.freeze(['observe', 'shadow', 'enforce'])
|
|
22
|
+
|
|
23
|
+
function mostRestrictive(modes) {
|
|
24
|
+
const valid = modes.filter((mode) => POLICY_MODES.includes(mode))
|
|
25
|
+
if (valid.length === 0) return 'observe'
|
|
26
|
+
return valid.reduce((strict, current) => (STRICTNESS.indexOf(current) < STRICTNESS.indexOf(strict) ? current : strict))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeList(input) {
|
|
30
|
+
const list = Array.isArray(input) ? input : []
|
|
31
|
+
return Object.freeze([...new Set(list.map((item) => String(item ?? '').trim()).filter(Boolean))])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 归一化白名单:兼容 3.3.0 早期的扁平数组(等价于「forkId 或标签命中」),
|
|
36
|
+
* 也接受显式四维对象 `{forks, tasks, tags, tools}`。空维度 = 该维度不限制。
|
|
37
|
+
*/
|
|
38
|
+
function normalizeAllowlist(input) {
|
|
39
|
+
if (Array.isArray(input)) {
|
|
40
|
+
const flat = normalizeList(input)
|
|
41
|
+
return Object.freeze({ legacy: true, forks: flat, tasks: Object.freeze([]), tags: flat, tools: Object.freeze([]) })
|
|
42
|
+
}
|
|
43
|
+
const source = input && typeof input === 'object' ? input : {}
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
legacy: false,
|
|
46
|
+
forks: normalizeList(source.forks),
|
|
47
|
+
tasks: normalizeList(source.tasks),
|
|
48
|
+
tags: normalizeList(source.tags),
|
|
49
|
+
tools: normalizeList(source.tools),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {{
|
|
55
|
+
* mode?: string,
|
|
56
|
+
* scopes?: { global?: string, forks?: Record<string,string>, tasks?: Record<string,string> },
|
|
57
|
+
* allowlist?: string[] | { forks?: string[], tasks?: string[], tags?: string[], tools?: string[] },
|
|
58
|
+
* audit?: { append?: Function },
|
|
59
|
+
* now?: () => number,
|
|
60
|
+
* }} [options]
|
|
61
|
+
*/
|
|
62
|
+
export function createPolicy(options = {}) {
|
|
63
|
+
const now = typeof options.now === 'function' ? options.now : () => Date.now()
|
|
64
|
+
const audit = options.audit && typeof options.audit.append === 'function' ? options.audit : null
|
|
65
|
+
const allowlist = normalizeAllowlist(options.allowlist)
|
|
66
|
+
const requestedMode = String(options.mode ?? 'observe')
|
|
67
|
+
let mode = POLICY_MODES.includes(requestedMode) ? requestedMode : 'observe'
|
|
68
|
+
let lastRefusal = null
|
|
69
|
+
|
|
70
|
+
const scopes = options.scopes && typeof options.scopes === 'object' ? options.scopes : {}
|
|
71
|
+
const forkScopes = scopes.forks && typeof scopes.forks === 'object' ? { ...scopes.forks } : {}
|
|
72
|
+
const taskScopes = scopes.tasks && typeof scopes.tasks === 'object' ? { ...scopes.tasks } : {}
|
|
73
|
+
|
|
74
|
+
/** 命中计数(observe 也要算,不然「灰度」无从判断)。 */
|
|
75
|
+
const hits = new Map()
|
|
76
|
+
const CERTIFICATE_LIMIT = 200
|
|
77
|
+
const certificates = []
|
|
78
|
+
|
|
79
|
+
const record = (type, payload) => {
|
|
80
|
+
try { audit?.append({ type, actor: 'cost-policy', payload }) } catch { /* 审计失败不改变策略行为 */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 解析当前上下文的生效档:global → fork → task,取最严格。 */
|
|
84
|
+
function resolve(context = {}) {
|
|
85
|
+
const forkId = String(context.forkId ?? '')
|
|
86
|
+
const taskId = String(context.taskId ?? '')
|
|
87
|
+
const layers = [{ scope: 'global', mode }]
|
|
88
|
+
if (forkId && POLICY_MODES.includes(String(forkScopes[forkId]))) {
|
|
89
|
+
layers.push({ scope: 'fork', mode: String(forkScopes[forkId]) })
|
|
90
|
+
}
|
|
91
|
+
if (taskId && POLICY_MODES.includes(String(taskScopes[taskId]))) {
|
|
92
|
+
layers.push({ scope: 'task', mode: String(taskScopes[taskId]) })
|
|
93
|
+
}
|
|
94
|
+
const effective = mostRestrictive(layers.map((layer) => layer.mode))
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
mode: effective,
|
|
97
|
+
decidedBy: layers.find((layer) => layer.mode === effective)?.scope ?? 'global',
|
|
98
|
+
layers,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** enforce 是否被白名单放行(forkId / taskId / tags / toolName 四个维度,非空维度必须命中)。 */
|
|
103
|
+
function allowlisted(context = {}) {
|
|
104
|
+
const forkId = String(context.forkId ?? '')
|
|
105
|
+
const taskId = String(context.taskId ?? '')
|
|
106
|
+
const tags = Array.isArray(context.tags) ? context.tags.map((tag) => String(tag)) : []
|
|
107
|
+
const toolName = String(context.toolName ?? '')
|
|
108
|
+
if (allowlist.legacy) {
|
|
109
|
+
if (allowlist.forks.length === 0) return { ok: false, reason: 'allowlist-empty' }
|
|
110
|
+
const matched = (forkId !== '' && allowlist.forks.includes(forkId)) || tags.some((tag) => allowlist.tags.includes(tag))
|
|
111
|
+
return matched ? { ok: true, reason: null } : { ok: false, reason: 'allowlist-miss' }
|
|
112
|
+
}
|
|
113
|
+
if (allowlist.forks.length === 0 && allowlist.tasks.length === 0 && allowlist.tags.length === 0 && allowlist.tools.length === 0) {
|
|
114
|
+
return { ok: false, reason: 'allowlist-empty' }
|
|
115
|
+
}
|
|
116
|
+
if (allowlist.forks.length > 0 && !(forkId !== '' && allowlist.forks.includes(forkId))) return { ok: false, reason: 'fork-not-allowlisted' }
|
|
117
|
+
if (allowlist.tasks.length > 0 && !(taskId !== '' && allowlist.tasks.includes(taskId))) return { ok: false, reason: 'task-not-allowlisted' }
|
|
118
|
+
if (allowlist.tags.length > 0 && !tags.some((tag) => allowlist.tags.includes(tag))) return { ok: false, reason: 'tag-not-allowlisted' }
|
|
119
|
+
if (allowlist.tools.length > 0 && !(toolName !== '' && allowlist.tools.includes(toolName))) return { ok: false, reason: 'tool-not-allowlisted' }
|
|
120
|
+
return { ok: true, reason: null }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 决策复用模块在守卫「命中」时调用(B1/B2/B3 都会经过这里)。
|
|
125
|
+
* observe —— 只计数;
|
|
126
|
+
* shadow —— 计数 + 落证书(记录本可节省),**不替换**;
|
|
127
|
+
* enforce —— 计数 + 落证书 + 允许替换(还需白名单命中)。
|
|
128
|
+
*/
|
|
129
|
+
function recordHit(input = {}) {
|
|
130
|
+
const context = { forkId: input.forkId, taskId: input.taskId, tags: input.tags, toolName: input.toolName }
|
|
131
|
+
const resolved = resolve(context)
|
|
132
|
+
const key = `${resolved.mode}:${String(input.module ?? 'unattributed')}`
|
|
133
|
+
const row = hits.get(key) ?? { mode: resolved.mode, module: String(input.module ?? 'unattributed'), hits: 0, wouldSaveTokens: 0, substituted: 0 }
|
|
134
|
+
row.hits += 1
|
|
135
|
+
row.wouldSaveTokens += Math.max(0, Number(input.wouldSaveTokens ?? 0) || 0)
|
|
136
|
+
hits.set(key, row)
|
|
137
|
+
|
|
138
|
+
let certificate = null
|
|
139
|
+
if (resolved.mode === 'observe') {
|
|
140
|
+
return Object.freeze({ ok: true, mode: 'observe', substituted: false, counted: true, certificate: null, resolved })
|
|
141
|
+
}
|
|
142
|
+
const allowed = resolved.mode === 'enforce' ? allowlisted(context) : { ok: false, reason: 'shadow-does-not-substitute' }
|
|
143
|
+
const substituted = resolved.mode === 'enforce' && allowed.ok
|
|
144
|
+
if (substituted) row.substituted += 1
|
|
145
|
+
certificate = Object.freeze({
|
|
146
|
+
at: now(),
|
|
147
|
+
module: String(input.module ?? 'unattributed'),
|
|
148
|
+
guardId: input.guardId === undefined ? null : String(input.guardId),
|
|
149
|
+
edgeId: input.edgeId === undefined ? null : String(input.edgeId),
|
|
150
|
+
receiptHash: input.receiptHash === undefined ? null : String(input.receiptHash),
|
|
151
|
+
mode: resolved.mode,
|
|
152
|
+
substituted,
|
|
153
|
+
wouldSaveTokens: Math.max(0, Number(input.wouldSaveTokens ?? 0) || 0),
|
|
154
|
+
allowlist: allowed.ok ? 'matched' : allowed.reason,
|
|
155
|
+
})
|
|
156
|
+
certificates.push(certificate)
|
|
157
|
+
if (certificates.length > CERTIFICATE_LIMIT) certificates.shift()
|
|
158
|
+
record('policy.hit', { ...certificate })
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
ok: true,
|
|
161
|
+
mode: resolved.mode,
|
|
162
|
+
substituted,
|
|
163
|
+
counted: true,
|
|
164
|
+
certificate,
|
|
165
|
+
resolved,
|
|
166
|
+
allowlist: allowed,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function describe() {
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
mode,
|
|
173
|
+
requestedMode,
|
|
174
|
+
scopes: Object.freeze({ global: mode, forks: Object.freeze({ ...forkScopes }), tasks: Object.freeze({ ...taskScopes }) }),
|
|
175
|
+
allowlist: Object.freeze({
|
|
176
|
+
legacy: allowlist.legacy,
|
|
177
|
+
forks: allowlist.forks.slice(),
|
|
178
|
+
tasks: allowlist.tasks.slice(),
|
|
179
|
+
tags: allowlist.tags.slice(),
|
|
180
|
+
tools: allowlist.tools.slice(),
|
|
181
|
+
}),
|
|
182
|
+
gate: { minAccuracy: ENFORCE_GATE.minAccuracy, minSamples: ENFORCE_GATE.minSamples, allowlistRequired: true },
|
|
183
|
+
lastRefusal,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function canEnforce(context = {}) {
|
|
188
|
+
if (resolve(context).mode !== 'enforce') return false
|
|
189
|
+
return allowlisted(context).ok
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function requestMode(next, evidence = {}) {
|
|
193
|
+
const target = String(next)
|
|
194
|
+
if (!POLICY_MODES.includes(target)) {
|
|
195
|
+
return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` }
|
|
196
|
+
}
|
|
197
|
+
if (target === mode) return { ok: true, mode, unchanged: true, at: now() }
|
|
198
|
+
const from = mode
|
|
199
|
+
|
|
200
|
+
if (target === 'enforce') {
|
|
201
|
+
const reasons = []
|
|
202
|
+
if (from === 'observe' && evidence.force !== true) reasons.push('shadow-required')
|
|
203
|
+
const accuracy = Number(evidence.accuracy)
|
|
204
|
+
if (!Number.isFinite(accuracy) || accuracy < ENFORCE_GATE.minAccuracy) reasons.push('accuracy-below-threshold')
|
|
205
|
+
const samples = Number(evidence.samples)
|
|
206
|
+
if (!Number.isFinite(samples) || samples < ENFORCE_GATE.minSamples) reasons.push('samples-below-threshold')
|
|
207
|
+
const destructiveHits = Number(evidence.destructiveHits ?? 0)
|
|
208
|
+
if (!Number.isFinite(destructiveHits) || destructiveHits > 0) reasons.push('destructive-hits')
|
|
209
|
+
const allowlistEmpty = allowlist.legacy
|
|
210
|
+
? allowlist.forks.length === 0
|
|
211
|
+
: allowlist.forks.length === 0 && allowlist.tasks.length === 0 && allowlist.tags.length === 0 && allowlist.tools.length === 0
|
|
212
|
+
if (allowlistEmpty) reasons.push('allowlist-empty')
|
|
213
|
+
if (reasons.length > 0) {
|
|
214
|
+
lastRefusal = Object.freeze({ at: now(), next: target, reasons })
|
|
215
|
+
record('policy.refused', { from, to: target, reasons })
|
|
216
|
+
return { ok: false, code: 'enforce-gate-failed', reasons, mode, at: lastRefusal.at }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
mode = target
|
|
221
|
+
lastRefusal = null
|
|
222
|
+
record('policy.mode', { from, to: target, evidence: { ...evidence } })
|
|
223
|
+
return { ok: true, mode, from, at: now() }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setMode(next) {
|
|
227
|
+
const target = String(next)
|
|
228
|
+
if (!POLICY_MODES.includes(target)) {
|
|
229
|
+
return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` }
|
|
230
|
+
}
|
|
231
|
+
if (target === mode) return { ok: true, mode, unchanged: true }
|
|
232
|
+
const from = mode
|
|
233
|
+
mode = target
|
|
234
|
+
record('policy.rollback', { from, to: target })
|
|
235
|
+
return { ok: true, mode, from }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 设置某个 fork / task 的档位(只能更严格或经 enforce 门禁前先下沉)。 */
|
|
239
|
+
function setScope(scope, id, next) {
|
|
240
|
+
const target = String(next)
|
|
241
|
+
if (!POLICY_MODES.includes(target)) {
|
|
242
|
+
return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` }
|
|
243
|
+
}
|
|
244
|
+
const key = String(id ?? '').trim()
|
|
245
|
+
if (!key) return { ok: false, code: 'scope-id-invalid', message: 'scope id must be a non-empty string' }
|
|
246
|
+
if (scope === 'fork') forkScopes[key] = target
|
|
247
|
+
else if (scope === 'task') taskScopes[key] = target
|
|
248
|
+
else return { ok: false, code: 'scope-invalid', message: 'scope must be fork or task' }
|
|
249
|
+
record('policy.scope', { scope, id: key, to: target })
|
|
250
|
+
return { ok: true, scope, id: key, mode: target }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stats() {
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
ok: true,
|
|
256
|
+
rows: [...hits.values()].map((row) => Object.freeze({ ...row })),
|
|
257
|
+
totalHits: [...hits.values()].reduce((sum, row) => sum + row.hits, 0),
|
|
258
|
+
totalWouldSaveTokens: [...hits.values()].reduce((sum, row) => sum + row.wouldSaveTokens, 0),
|
|
259
|
+
totalSubstituted: [...hits.values()].reduce((sum, row) => sum + row.substituted, 0),
|
|
260
|
+
certificates: certificates.slice(-50),
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
describe,
|
|
266
|
+
resolve,
|
|
267
|
+
allowlisted,
|
|
268
|
+
recordHit,
|
|
269
|
+
stats,
|
|
270
|
+
canEnforce,
|
|
271
|
+
requestMode,
|
|
272
|
+
setMode,
|
|
273
|
+
setScope,
|
|
274
|
+
isEnforce: (context = {}) => resolve(context).mode === 'enforce',
|
|
275
|
+
})
|
|
276
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
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 系统层 · report(两条链路联动报表,设计 §8)。
|
|
7
|
+
*
|
|
8
|
+
* 两条链路的节省**必须分列、且不重复计**:
|
|
9
|
+
*
|
|
10
|
+
* * **单价项(A)**:A1 压缩回执 → 每次交互更便宜。来源 = execution 账里 `phase:'tool'`
|
|
11
|
+
* 的压缩归因条目(`meta.kind` 为 attribution)。
|
|
12
|
+
* * **次数项(B)**:B2/B3 命中 → 少调一次模型。来源 = **替换证书**(不是账本),
|
|
13
|
+
* 每个 dispatched 证书代表一次被跳过的回调,金额 = 该证书的 `savedTokens`(基线单价)。
|
|
14
|
+
*
|
|
15
|
+
* 总节省 = Σ(跳过的调用数 × 该步基线单价) + Σ(每次交互的单价下降),两项分列。
|
|
16
|
+
*
|
|
17
|
+
* 作者的代价必须摊进去:EDG 编写与 RSI 离线成本都在 governance 账;
|
|
18
|
+
* 若某任务 `execution 节省 < governance 摊销`,该任务记为**净亏**并显式列出,不隐藏。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function num(value) {
|
|
22
|
+
const n = Number(value)
|
|
23
|
+
return Number.isFinite(n) ? n : 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nonNegative = (value) => Math.max(0, num(value))
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 判定某条账本事件属于哪一列(互斥):
|
|
30
|
+
* * B 列**只认证书**:替换产生的账本事件(`meta.kind === 'substitution'`)单列出来,
|
|
31
|
+
* 既不进 A 也不进 B —— 否则同一次节省会被证书与账本各算一遍;
|
|
32
|
+
* * A 列只认 execution + tool 相位 + 非外部(external 是宿主面插件收益,单列)。
|
|
33
|
+
*/
|
|
34
|
+
export function classifyEntry(entry) {
|
|
35
|
+
if (!entry || entry.account !== 'execution') return 'governance'
|
|
36
|
+
if (entry.external === true) return 'external'
|
|
37
|
+
if (entry.meta?.kind === 'substitution') return 'skipped-call-ledger'
|
|
38
|
+
if (entry.phase === 'tool' && entry.savedTokens > 0) return 'unit-price'
|
|
39
|
+
if (entry.savedTokens > 0) return 'other-execution'
|
|
40
|
+
return 'consumption'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{
|
|
45
|
+
* entries?: object[],
|
|
46
|
+
* certificates?: object[],
|
|
47
|
+
* amortize?: 'even' | 'per-task',
|
|
48
|
+
* moduleOf?: (entry: object) => string,
|
|
49
|
+
* }} input
|
|
50
|
+
*/
|
|
51
|
+
export function buildSavingsReport(input = {}) {
|
|
52
|
+
const entries = Array.isArray(input.entries) ? input.entries : []
|
|
53
|
+
const certificates = Array.isArray(input.certificates) ? input.certificates : []
|
|
54
|
+
const amortize = input.amortize === 'per-task' ? 'per-task' : 'even'
|
|
55
|
+
|
|
56
|
+
// ---- 列 A:单价下降(A1 回执压缩) ----
|
|
57
|
+
const unitPrice = { savedTokens: 0, interactions: 0, byModule: {}, byTask: {} }
|
|
58
|
+
// ---- 列 B:跳过调用(B2/B3 复用) ----
|
|
59
|
+
const skippedCalls = { savedTokens: 0, calls: 0, byTool: {}, byTask: {} }
|
|
60
|
+
// ---- 治理成本(作者 + 离线) ----
|
|
61
|
+
const governance = { totalTokens: 0, byKind: {}, byTask: {} }
|
|
62
|
+
// ---- 其它(消耗、外部)----
|
|
63
|
+
const consumption = { tokens: 0, entries: 0 }
|
|
64
|
+
const externalSaved = 0
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const bucket = classifyEntry(entry)
|
|
68
|
+
if (bucket === 'governance') {
|
|
69
|
+
const tokens = nonNegative(entry.total)
|
|
70
|
+
governance.totalTokens += tokens
|
|
71
|
+
const kind = String(entry.meta?.kind ?? entry.source ?? 'governance')
|
|
72
|
+
governance.byKind[kind] = (governance.byKind[kind] ?? 0) + tokens
|
|
73
|
+
const taskKey = entry.taskId ?? 'unattributed'
|
|
74
|
+
governance.byTask[taskKey] = (governance.byTask[taskKey] ?? 0) + tokens
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (bucket === 'unit-price') {
|
|
78
|
+
const saved = nonNegative(entry.savedTokens)
|
|
79
|
+
unitPrice.savedTokens += saved
|
|
80
|
+
unitPrice.interactions += 1
|
|
81
|
+
const moduleKey = String(entry.module ?? 'unattributed')
|
|
82
|
+
unitPrice.byModule[moduleKey] = (unitPrice.byModule[moduleKey] ?? 0) + saved
|
|
83
|
+
const taskKey = entry.taskId ?? 'unattributed'
|
|
84
|
+
unitPrice.byTask[taskKey] = (unitPrice.byTask[taskKey] ?? 0) + saved
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (bucket === 'consumption') {
|
|
88
|
+
consumption.tokens += nonNegative(entry.total)
|
|
89
|
+
consumption.entries += 1
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// B 列:只来自证书,且只算真正派发成功的
|
|
94
|
+
for (const cert of certificates) {
|
|
95
|
+
if (cert?.dispatched === false) continue
|
|
96
|
+
const saved = nonNegative(cert?.savedTokens)
|
|
97
|
+
skippedCalls.calls += 1
|
|
98
|
+
skippedCalls.savedTokens += saved
|
|
99
|
+
const tool = String(cert?.toolName ?? 'unknown')
|
|
100
|
+
skippedCalls.byTool[tool] = (skippedCalls.byTool[tool] ?? 0) + saved
|
|
101
|
+
const taskKey = cert?.taskId ?? 'unattributed'
|
|
102
|
+
skippedCalls.byTask[taskKey] = (skippedCalls.byTask[taskKey] ?? 0) + saved
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 任务级净额:execution 节省(A+B) − governance 摊销 ----
|
|
106
|
+
const taskIds = [...new Set([
|
|
107
|
+
...Object.keys(unitPrice.byTask),
|
|
108
|
+
...Object.keys(skippedCalls.byTask),
|
|
109
|
+
...Object.keys(governance.byTask),
|
|
110
|
+
])].filter((taskId) => taskId !== 'unattributed').sort()
|
|
111
|
+
const amortized = {}
|
|
112
|
+
if (amortize === 'even') {
|
|
113
|
+
// RSI/离线等无法落到具体任务的成本,按任务数均摊(显式声明策略)。
|
|
114
|
+
const shared = governance.byTask.unattributed ?? 0
|
|
115
|
+
const perTask = taskIds.length > 0 ? shared / taskIds.length : 0
|
|
116
|
+
for (const taskId of taskIds) amortized[taskId] = (governance.byTask[taskId] ?? 0) + perTask
|
|
117
|
+
} else {
|
|
118
|
+
for (const taskId of taskIds) amortized[taskId] = governance.byTask[taskId] ?? 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tasks = taskIds.map((taskId) => {
|
|
122
|
+
const executionSaved = (unitPrice.byTask[taskId] ?? 0) + (skippedCalls.byTask[taskId] ?? 0)
|
|
123
|
+
const governanceAmortized = amortized[taskId] ?? 0
|
|
124
|
+
const net = executionSaved - governanceAmortized
|
|
125
|
+
return {
|
|
126
|
+
taskId,
|
|
127
|
+
executionSaved,
|
|
128
|
+
unitPriceSaved: unitPrice.byTask[taskId] ?? 0,
|
|
129
|
+
skippedCallSaved: skippedCalls.byTask[taskId] ?? 0,
|
|
130
|
+
governanceAmortized,
|
|
131
|
+
net,
|
|
132
|
+
/** §8:净亏任务必须显示,不隐藏。 */
|
|
133
|
+
netLoss: net < 0,
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
const netLossTasks = tasks.filter((task) => task.netLoss)
|
|
137
|
+
|
|
138
|
+
const totalSavedTokens = unitPrice.savedTokens + skippedCalls.savedTokens
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
ok: true,
|
|
141
|
+
amortize,
|
|
142
|
+
/** 两项分列——报表必须分别展示,禁止只报合计。 */
|
|
143
|
+
columns: Object.freeze({
|
|
144
|
+
unitPrice: Object.freeze({ ...unitPrice, byModule: { ...unitPrice.byModule }, byTask: { ...unitPrice.byTask } }),
|
|
145
|
+
skippedCalls: Object.freeze({ ...skippedCalls, byTool: { ...skippedCalls.byTool }, byTask: { ...skippedCalls.byTask } }),
|
|
146
|
+
}),
|
|
147
|
+
totalSavedTokens,
|
|
148
|
+
governance: Object.freeze({ ...governance, byKind: { ...governance.byKind }, byTask: { ...governance.byTask } }),
|
|
149
|
+
consumption: Object.freeze({ ...consumption }),
|
|
150
|
+
/** 宿主面外部插件自报收益:只展示,永不并入总节省。 */
|
|
151
|
+
externalSaved,
|
|
152
|
+
/** A/B 两列来源互斥(A=账本、B=证书),结构上不可能重复计。 */
|
|
153
|
+
doubleCountGuard: true,
|
|
154
|
+
tasks: Object.freeze(tasks.map((task) => Object.freeze(task))),
|
|
155
|
+
netLossTasks: Object.freeze(netLossTasks.map((task) => Object.freeze(task))),
|
|
156
|
+
netSaved: totalSavedTokens - governance.totalTokens,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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 · M3 交付物:**命中率与「本可节省」报表**(B1 EDG + B2 GDR,shadow 只算不替换)。
|
|
7
|
+
*
|
|
8
|
+
* 口径纪律(M3 最关键的一条):
|
|
9
|
+
*
|
|
10
|
+
* * **shadow 的节省是「本可节省」,不是节省**。报表字段名就叫 `wouldSaveTokens`,
|
|
11
|
+
* 并且 `actualSavedTokens` 在 shadow 下恒为 0;
|
|
12
|
+
* * **分母是「每次复用尝试」**,不是每次工具调用、也不是每个任务;
|
|
13
|
+
* * 未命中必须给原因分布(`schema-drift` / `guard-miss` / `arg-unextractable` …),
|
|
14
|
+
* 这是 RSI(B4)唯一能拿来做改进的输入;
|
|
15
|
+
* * shadow 证书**不进审计链**(它们没有派发,没有 native dispatcher 证据)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const GDR_REASONS = Object.freeze([
|
|
19
|
+
'no-edge',
|
|
20
|
+
'tool-mismatch',
|
|
21
|
+
'schema-drift',
|
|
22
|
+
'dependency-incomplete',
|
|
23
|
+
'guard-miss',
|
|
24
|
+
'arg-unextractable',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
const num = (value) => {
|
|
28
|
+
const n = Number(value)
|
|
29
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* M3 报表。
|
|
34
|
+
*
|
|
35
|
+
* @param {{
|
|
36
|
+
* attempts?: Array<{toolName?: string, hit?: boolean, reason?: string | null, stage?: string | null, edgeId?: string | null, wouldSaveTokens?: number}>,
|
|
37
|
+
* shadowCertificates?: Array<object>,
|
|
38
|
+
* substitutions?: Array<object>,
|
|
39
|
+
* mode?: 'off' | 'shadow' | 'enforce',
|
|
40
|
+
* }} input
|
|
41
|
+
*/
|
|
42
|
+
export function buildReuseReport(input = {}) {
|
|
43
|
+
const attempts = Array.isArray(input.attempts) ? input.attempts : []
|
|
44
|
+
const shadowCertificates = Array.isArray(input.shadowCertificates) ? input.shadowCertificates : []
|
|
45
|
+
const substitutions = Array.isArray(input.substitutions) ? input.substitutions : []
|
|
46
|
+
const mode = input.mode === 'enforce' ? 'enforce' : input.mode === 'off' ? 'off' : 'shadow'
|
|
47
|
+
|
|
48
|
+
const byReason = Object.fromEntries(GDR_REASONS.map((reason) => [reason, 0]))
|
|
49
|
+
const byTool = {}
|
|
50
|
+
const byStage = {}
|
|
51
|
+
let hits = 0
|
|
52
|
+
let wouldSaveTokens = 0
|
|
53
|
+
|
|
54
|
+
for (const attempt of attempts) {
|
|
55
|
+
const toolName = String(attempt?.toolName ?? 'unknown')
|
|
56
|
+
if (!byTool[toolName]) byTool[toolName] = { attempts: 0, hits: 0, wouldSaveTokens: 0 }
|
|
57
|
+
const row = byTool[toolName]
|
|
58
|
+
row.attempts += 1
|
|
59
|
+
if (attempt?.hit === true) {
|
|
60
|
+
hits += 1
|
|
61
|
+
row.hits += 1
|
|
62
|
+
const wouldSave = num(attempt?.wouldSaveTokens)
|
|
63
|
+
wouldSaveTokens += wouldSave
|
|
64
|
+
row.wouldSaveTokens += wouldSave
|
|
65
|
+
} else {
|
|
66
|
+
const reason = String(attempt?.reason ?? 'guard-miss')
|
|
67
|
+
if (reason in byReason) byReason[reason] += 1
|
|
68
|
+
else byReason['guard-miss'] += 1
|
|
69
|
+
const stage = String(attempt?.stage ?? 'unknown')
|
|
70
|
+
byStage[stage] = (byStage[stage] ?? 0) + 1
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const total = attempts.length
|
|
75
|
+
const hitRate = total > 0 ? hits / total : 0
|
|
76
|
+
// shadow 与 enforce 的节省口径必须分开:shadow 的命中只是「本可替换」
|
|
77
|
+
const dispatchedSubstitutions = substitutions.filter((cert) => cert?.dispatched === true)
|
|
78
|
+
const actualSavedTokens = mode === 'enforce'
|
|
79
|
+
? dispatchedSubstitutions.reduce((sum, cert) => sum + num(cert?.savedTokens), 0)
|
|
80
|
+
: 0
|
|
81
|
+
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
ok: true,
|
|
84
|
+
mode,
|
|
85
|
+
attempts: total,
|
|
86
|
+
hits,
|
|
87
|
+
hitRate,
|
|
88
|
+
/** 分母口径写进报表,避免被误读成「每次工具调用的命中率」。 */
|
|
89
|
+
denominator: 'reuse-attempts',
|
|
90
|
+
byReason: Object.freeze({ ...byReason }),
|
|
91
|
+
byStage: Object.freeze({ ...byStage }),
|
|
92
|
+
byTool: Object.freeze(Object.fromEntries(Object.entries(byTool).map(([key, row]) => [key, Object.freeze({ ...row })]))),
|
|
93
|
+
/** M3 的核心数字:shadow 下的「本可节省」。 */
|
|
94
|
+
wouldSaveTokens,
|
|
95
|
+
/** 真正派发成功的替换节省;shadow 恒为 0,绝不会把「本可」写成「已省」。 */
|
|
96
|
+
actualSavedTokens,
|
|
97
|
+
shadowCertificates: shadowCertificates.length,
|
|
98
|
+
shadowCertificateDispatched: shadowCertificates.filter((cert) => cert?.dispatched === true).length,
|
|
99
|
+
substitutions: dispatchedSubstitutions.length,
|
|
100
|
+
/** 三档语义:shadow 只算不替换;enforce 才可能真的替换成功。 */
|
|
101
|
+
discipline: Object.freeze({
|
|
102
|
+
shadowNeverBooksSavings: mode === 'shadow' ? actualSavedTokens === 0 : true,
|
|
103
|
+
shadowCertificatesNotAudited: shadowCertificates.every((cert) => cert?.dispatched !== true),
|
|
104
|
+
denominatorIsAttempts: true,
|
|
105
|
+
}),
|
|
106
|
+
/** 数据不足时的诚实提示(样本太小不能下结论)。 */
|
|
107
|
+
sufficiency: Object.freeze({
|
|
108
|
+
samples: total,
|
|
109
|
+
enoughForConclusion: total >= 20,
|
|
110
|
+
note: total >= 20 ? null : '样本不足 20:命中率尚不足以下结论',
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 人读行(CLI / 面板共用)。 */
|
|
116
|
+
export function formatReuseReport(report) {
|
|
117
|
+
const lines = []
|
|
118
|
+
lines.push(`复用报表(${report.mode}) 尝试 ${report.attempts} · 命中 ${report.hits} · 命中率 ${(report.hitRate * 100).toFixed(1)}%`)
|
|
119
|
+
lines.push(`本可节省 ${report.wouldSaveTokens} tokens · 实际节省 ${report.actualSavedTokens} tokens`)
|
|
120
|
+
lines.push(`shadow 证书 ${report.shadowCertificates}(未派发 ${report.shadowCertificates - report.shadowCertificateDispatched})· 真实替换 ${report.substitutions}`)
|
|
121
|
+
lines.push('')
|
|
122
|
+
lines.push('未命中原因分布')
|
|
123
|
+
for (const [reason, count] of Object.entries(report.byReason)) lines.push(` ${reason.padEnd(22)} ${count}`)
|
|
124
|
+
lines.push('')
|
|
125
|
+
lines.push('按工具')
|
|
126
|
+
for (const [tool, row] of Object.entries(report.byTool)) {
|
|
127
|
+
lines.push(` ${tool.padEnd(20)} 尝试 ${row.attempts} · 命中 ${row.hits} · 本可节省 ${row.wouldSaveTokens}`)
|
|
128
|
+
}
|
|
129
|
+
if (report.sufficiency.note) lines.push(`\n注意:${report.sufficiency.note}`)
|
|
130
|
+
return lines.join('\n')
|
|
131
|
+
}
|