@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.
Files changed (93) hide show
  1. package/README.md +3 -0
  2. package/client.js +50 -0
  3. package/dist-host/cost/attribution.js +183 -0
  4. package/dist-host/cost/audit.js +163 -0
  5. package/dist-host/cost/budget.js +211 -0
  6. package/dist-host/cost/bus.js +95 -0
  7. package/dist-host/cost/cert-chain.js +100 -0
  8. package/dist-host/cost/experiment.js +132 -0
  9. package/dist-host/cost/gate.js +189 -0
  10. package/dist-host/cost/host-surface.js +80 -0
  11. package/dist-host/cost/index.js +312 -0
  12. package/dist-host/cost/ledger-report.js +193 -0
  13. package/dist-host/cost/ledger.js +401 -0
  14. package/dist-host/cost/metrics.js +198 -0
  15. package/dist-host/cost/milestones.js +171 -0
  16. package/dist-host/cost/modules/index.js +5 -0
  17. package/dist-host/cost/modules/manifests.js +130 -0
  18. package/dist-host/cost/policy.js +276 -0
  19. package/dist-host/cost/report.js +156 -0
  20. package/dist-host/cost/reuse-report.js +128 -0
  21. package/dist-host/cost/rsi.js +367 -0
  22. package/dist-host/cost/toolface.js +147 -0
  23. package/dist-host/cost/types.js +31 -0
  24. package/dist-host/kernel.js +28 -2
  25. package/dist-host/plugins/cost-optimizer.js +277 -27
  26. package/dist-host/plugins/superagent-router.js +88 -1
  27. package/dist-host/superagent/types.js +8 -0
  28. package/dist-worker/agent-loop.js +449 -7
  29. package/dist-worker/baseline-fit.js +66 -0
  30. package/dist-worker/budget-guard.js +93 -0
  31. package/dist-worker/ccs.js +99 -0
  32. package/dist-worker/cost-hooks.js +115 -0
  33. package/dist-worker/cost-modules.js +330 -0
  34. package/dist-worker/cost-spi.js +3 -0
  35. package/dist-worker/edg.js +372 -0
  36. package/dist-worker/gdr.js +159 -0
  37. package/dist-worker/index.js +80 -2
  38. package/dist-worker/llm-http.js +20 -0
  39. package/dist-worker/sink-http.js +1 -0
  40. package/dist-worker/spill.js +115 -0
  41. package/dist-worker/tool-batch.js +66 -0
  42. package/dist-worker/tool-compact.js +275 -0
  43. package/dist-worker/tool-disclosure.js +90 -0
  44. package/dist-worker/tools.js +16 -0
  45. package/docs/ARCHITECTURE.md +21 -0
  46. package/docs/CHANGELOG.md +50 -0
  47. package/docs/DESIGN-v3.3.0-cost-optimization.md +1053 -0
  48. package/docs/EXPERIMENT-M5-real-ab.md +82 -0
  49. package/docs/THIRD-PARTY.md +87 -0
  50. package/package.json +1 -1
  51. package/src/cost/attribution.ts +186 -0
  52. package/src/cost/audit.ts +170 -0
  53. package/src/cost/budget.ts +226 -0
  54. package/src/cost/bus.ts +97 -0
  55. package/src/cost/cert-chain.ts +102 -0
  56. package/src/cost/experiment.ts +136 -0
  57. package/src/cost/gate.ts +191 -0
  58. package/src/cost/host-surface.ts +97 -0
  59. package/src/cost/index.ts +316 -0
  60. package/src/cost/ledger-report.ts +186 -0
  61. package/src/cost/ledger.ts +385 -0
  62. package/src/cost/metrics.ts +193 -0
  63. package/src/cost/milestones.ts +170 -0
  64. package/src/cost/modules/index.ts +13 -0
  65. package/src/cost/modules/manifests.ts +129 -0
  66. package/src/cost/policy.ts +276 -0
  67. package/src/cost/report.ts +158 -0
  68. package/src/cost/reuse-report.ts +131 -0
  69. package/src/cost/rsi.ts +364 -0
  70. package/src/cost/toolface.ts +151 -0
  71. package/src/cost/types.ts +298 -0
  72. package/src/kernel.ts +27 -2
  73. package/src/plugins/cost-optimizer.ts +271 -26
  74. package/src/plugins/superagent-router.ts +82 -1
  75. package/src/superagent/types.ts +70 -0
  76. package/worker/agent-loop.ts +444 -7
  77. package/worker/baseline-fit.ts +80 -0
  78. package/worker/budget-guard.ts +156 -0
  79. package/worker/ccs.ts +202 -0
  80. package/worker/cost-hooks.ts +133 -0
  81. package/worker/cost-modules.ts +356 -0
  82. package/worker/cost-spi.ts +283 -0
  83. package/worker/edg.ts +424 -0
  84. package/worker/gdr.ts +233 -0
  85. package/worker/index.ts +84 -3
  86. package/worker/llm-http.ts +18 -0
  87. package/worker/sink-http.ts +5 -2
  88. package/worker/spill.ts +133 -0
  89. package/worker/tool-batch.ts +124 -0
  90. package/worker/tool-compact.ts +317 -0
  91. package/worker/tool-disclosure.ts +131 -0
  92. package/worker/tools.ts +553 -531
  93. package/worker/types.ts +191 -0
@@ -0,0 +1,312 @@
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
+ * 3.3.0 系统层入口:把 ledger / budget / policy / audit 组装成一个系统。
6
+ *
7
+ * `cost-optimizer` 插件通过它拿到统一的记账、预算、灰度与审计口径;
8
+ * M1 只做 observe(不接入任何替换路径),所以这些组件只被读取与记账,
9
+ * 不改变 Worker/模型调用行为。
10
+ */
11
+ import { createLedger } from "./ledger.js";
12
+ import { createBudget } from "./budget.js";
13
+ import { createPolicy } from "./policy.js";
14
+ import { createAuditChain } from "./audit.js";
15
+ import { createAttribution } from "./attribution.js";
16
+ import { createCostBus } from "./bus.js";
17
+ import { createHostSurfaceAdapter } from "./host-surface.js";
18
+ import { buildSavingsReport, classifyEntry } from "./report.js";
19
+ import { buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP } from "./metrics.js";
20
+ import { MODULE_MANIFESTS, MODULE_IDS, validateManifests } from "./modules/manifests.js";
21
+ import { buildLedgerReport, formatLedgerReport, parseLedgerJsonl } from "./ledger-report.js";
22
+ import { buildToolFaceReport, planToolFace } from "./toolface.js";
23
+ import { buildReuseReport, formatReuseReport } from "./reuse-report.js";
24
+ export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport };
25
+ export * from "./types.js";
26
+ /**
27
+ * @param {{
28
+ * mode?: string, allowlist?: string[] | {forks?:string[],tasks?:string[],tags?:string[],tools?:string[]},
29
+ * policyScopes?: {global?:string, forks?:Record<string,string>, tasks?:Record<string,string>},
30
+ * budgetLimits?: Array<{unit:string, limitTokens?:number, limitCost?:number, onExhaust?:string, degradeTo?:string}>,
31
+ * degradeModels?: string[],
32
+ * now?: () => number, maxEntries?: number, maxRecords?: number,
33
+ * }} [options]
34
+ */
35
+ export function createCostSystem(options = {}) {
36
+ const now = typeof options.now === 'function' ? options.now : () => Date.now();
37
+ const audit = createAuditChain({ now, maxRecords: options.maxRecords });
38
+ const policy = createPolicy({ mode: options.mode, allowlist: options.allowlist, scopes: options.policyScopes, audit, now });
39
+ const budget = createBudget({ limits: options.budgetLimits, degradeModels: options.degradeModels, now });
40
+ const ledger = createLedger({
41
+ now,
42
+ maxEntries: options.maxEntries,
43
+ dataDir: options.dataDir,
44
+ persist: options.persist === true,
45
+ logger: options.logger,
46
+ });
47
+ const attribution = createAttribution({ ledger, now });
48
+ const bus = createCostBus({ now, maxEvents: options.maxBusEvents, relays: options.relays });
49
+ /** 宿主会话工具面适配器(可选;默认关闭,不依赖任何第三方插件)。 */
50
+ const hostSurface = createHostSurfaceAdapter({
51
+ ledger,
52
+ bus,
53
+ enabled: options.hostSurface?.enabled === true,
54
+ source: options.hostSurface?.source,
55
+ now,
56
+ });
57
+ /** 模块降级累计(按模块聚合;不参与节省计算)。 */
58
+ const degradedByModule = new Map();
59
+ function recordUsage(input = {}) {
60
+ const out = ledger.append({
61
+ account: input.account ?? 'execution',
62
+ module: input.module,
63
+ source: input.source ?? 'model',
64
+ phase: input.phase,
65
+ surface: input.surface,
66
+ external: input.external,
67
+ taskId: input.taskId,
68
+ forkId: input.forkId,
69
+ sessionId: input.sessionId,
70
+ ticketId: input.ticketId,
71
+ turn: input.turn,
72
+ step: input.step,
73
+ provider: input.provider,
74
+ model: input.model,
75
+ toolName: input.toolName,
76
+ inputTokens: input.inputTokens,
77
+ outputTokens: input.outputTokens,
78
+ cachedTokens: input.cachedTokens,
79
+ totalTokens: input.totalTokens,
80
+ baselineTokens: input.baselineTokens,
81
+ savedTokens: input.savedTokens,
82
+ estimated: input.estimated,
83
+ meta: input.meta,
84
+ });
85
+ if (out.ok) {
86
+ bus.publish('cost:event', { kind: 'ledger',
87
+ id: out.entry.id,
88
+ account: out.entry.account,
89
+ module: out.entry.module,
90
+ phase: out.entry.phase,
91
+ surface: out.entry.surface,
92
+ total: out.entry.total,
93
+ savedTokens: out.entry.savedTokens,
94
+ taskId: out.entry.taskId,
95
+ estimated: out.entry.estimated,
96
+ });
97
+ }
98
+ return out;
99
+ }
100
+ /** 按维度查询账本(task/fork/session/module/account/phase/时间窗)。 */
101
+ function query(filter = {}) {
102
+ return ledger.query(filter);
103
+ }
104
+ /** 从 JSONL 回放(进程重启后恢复报表)。 */
105
+ function replay(direction = 'forward') {
106
+ return ledger.replay(direction);
107
+ }
108
+ /**
109
+ * §8 联动报表:A(单价下降)与 B(跳过调用)分列,governance 摊销后逐任务算净额,
110
+ * 净亏任务显式列出(不隐藏)。
111
+ */
112
+ function savingsReport(input = {}) {
113
+ const from = input.from === undefined ? undefined : Number(input.from);
114
+ const to = input.to === undefined ? undefined : Number(input.to);
115
+ const entries = ledger.query({ limit: Math.max(1, Number(input.limit ?? 5000) || 5000), ...(from === undefined ? {} : { from }), ...(to === undefined ? {} : { to }) }).entries;
116
+ const certificates = audit.substitutions({});
117
+ return buildSavingsReport({ entries, certificates, amortize: input.amortize });
118
+ }
119
+ function authorize(input = {}) {
120
+ const out = budget.authorize(input);
121
+ if (!out.ok) {
122
+ try {
123
+ audit.append({ type: 'budget.denied', actor: 'cost-budget', payload: { ...out } });
124
+ }
125
+ catch { /* ignore */ }
126
+ bus.publish('cost:event', { kind: 'budget.denied', ...out });
127
+ }
128
+ else if (out.degraded) {
129
+ try {
130
+ audit.append({ type: 'budget.degraded', actor: 'cost-budget', payload: { ...out } });
131
+ }
132
+ catch { /* ignore */ }
133
+ bus.publish('cost:event', { kind: 'budget.degraded', ...out });
134
+ }
135
+ else {
136
+ bus.publish('cost:event', { kind: 'budget.authorized', unit: out.unit, key: out.key, consumedTokens: out.consumedTokens, remainingTokens: out.remainingTokens });
137
+ }
138
+ return out;
139
+ }
140
+ /** 归因入口:enforce 写账本;shadow 只累计「本可节省」。 */
141
+ function attribute(input = {}) {
142
+ const out = attribution.attribute(input);
143
+ if (out.ok)
144
+ bus.publish('cost:event', { kind: 'attribution', mode: out.mode, module: out.module, savedTokens: out.savedTokens ?? 0, projectedTokens: out.projectedTokens ?? 0 });
145
+ return out;
146
+ }
147
+ /** §5.4:总额 − 各模块之和,差额进 unattributed(报表单列)。 */
148
+ function attributeTotal(input = {}) {
149
+ const out = attribution.attributeTotal(input);
150
+ if (out.ok)
151
+ bus.publish('cost:event', { kind: 'attribution.total', totalSavedTokens: out.totalSavedTokens, attributedTokens: out.attributedTokens, unattributedTokens: out.unattributedTokens });
152
+ return out;
153
+ }
154
+ /**
155
+ * §5.5:一次真实替换 = 证书 + 账本事件,两者共用 `taskId + step`。
156
+ * 任何一步失败都 fail-closed(不写半套)。
157
+ */
158
+ function substitute(cert = {}) {
159
+ const recorded = audit.appendSubstitution(cert);
160
+ if (!recorded.ok) {
161
+ bus.publish('cost:event', { kind: 'substitution.rejected', code: recorded.code, taskId: cert?.taskId ?? null });
162
+ return recorded;
163
+ }
164
+ let attributed = null;
165
+ if (Number(recorded.cert.savedTokens) > 0) {
166
+ attributed = attribution.attribute({
167
+ taskId: recorded.cert.taskId,
168
+ module: cert.moduleId ?? 'b3-ccs',
169
+ savedTokens: recorded.cert.savedTokens,
170
+ mode: 'enforce',
171
+ forkId: recorded.cert.forkId ?? undefined,
172
+ sessionId: recorded.cert.sessionId ?? undefined,
173
+ step: recorded.cert.step,
174
+ meta: { kind: 'substitution', step: recorded.cert.step, edgeId: recorded.cert.edgeId, replacedCallbackId: recorded.cert.replacedCallbackId },
175
+ });
176
+ if (!attributed.ok)
177
+ return attributed;
178
+ }
179
+ bus.publish('cost:substitution', {
180
+ taskId: recorded.cert.taskId,
181
+ step: recorded.cert.step,
182
+ edgeId: recorded.cert.edgeId,
183
+ toolName: recorded.cert.toolName,
184
+ savedTokens: recorded.cert.savedTokens,
185
+ });
186
+ return { ok: true, cert: recorded.cert, attribution: attributed, hash: recorded.record.hash };
187
+ }
188
+ /**
189
+ * §5.5:回放「这一省是怎么来的」——按 `taskId + step` 取出证书与账本事件。
190
+ */
191
+ function trace({ taskId, step } = {}) {
192
+ if (taskId === undefined || String(taskId).trim() === '') {
193
+ return { ok: false, code: 'task-missing', message: 'trace requires taskId' };
194
+ }
195
+ const filter = { taskId };
196
+ if (step !== undefined)
197
+ filter.step = step;
198
+ const certificates = audit.substitutions(filter);
199
+ const entries = ledger.query({ taskId, limit: 500 }).entries
200
+ .filter((entry) => (step === undefined ? true : entry.step === Number(step)));
201
+ const savedFromCerts = certificates.reduce((sum, cert) => sum + Number(cert.savedTokens ?? 0), 0);
202
+ // 对账只比「替换产生的账本事件」:其它节省(A1 压缩等)不属于证书口径。
203
+ const substitutionEntries = entries.filter((entry) => entry.meta?.kind === 'substitution');
204
+ const savedFromLedger = substitutionEntries.reduce((sum, entry) => sum + entry.savedTokens, 0);
205
+ const savedFromLedgerAll = entries.reduce((sum, entry) => sum + entry.savedTokens, 0);
206
+ return Object.freeze({
207
+ ok: true,
208
+ taskId: String(taskId),
209
+ step: step === undefined ? null : Number(step),
210
+ certificates,
211
+ entries,
212
+ /** 两边对账:证书声明的节省应等于**替换账本**记到的节省。 */
213
+ savedFromCertificates: savedFromCerts,
214
+ savedFromLedger,
215
+ /** 任务全部节省(含 A1 等非替换来源),仅展示,不参与对账。 */
216
+ savedFromLedgerAll,
217
+ reconciled: savedFromCerts === savedFromLedger,
218
+ });
219
+ }
220
+ /**
221
+ * 降级留痕(目标 2.1-5):模块异常 → 审计链 + 事件总线 + 计数。
222
+ * 降级**不计入任何节省**,只用于让报表能区分「真的省了」与「模块挂了」。
223
+ */
224
+ function degraded(input = {}) {
225
+ const module = String(input.module ?? 'unknown');
226
+ const phase = String(input.phase ?? 'transform');
227
+ const message = String(input.message ?? '');
228
+ const count = Math.max(1, Number(input.count ?? 1) || 1);
229
+ const at = now();
230
+ const totals = degradedByModule.get(module) ?? { module, events: 0, occurrences: 0, lastAt: 0, phases: {} };
231
+ totals.events += 1;
232
+ totals.occurrences += count;
233
+ totals.lastAt = at;
234
+ totals.phases[phase] = (totals.phases[phase] ?? 0) + count;
235
+ degradedByModule.set(module, totals);
236
+ try {
237
+ audit.append({ type: 'cost.degraded', actor: 'cost-system', payload: { module, phase, message, count, taskId: input.taskId ?? null } });
238
+ }
239
+ catch { /* ignore */ }
240
+ bus.publish('cost:degraded', { module, phase, message, count, taskId: input.taskId ?? null });
241
+ return { ok: true, module, phase, count, total: { ...totals, phases: { ...totals.phases } }, at };
242
+ }
243
+ /**
244
+ * §10.1 验收指标:七项必须给全;缺项显式报 `complete:false + missing[]`。
245
+ * 传入 `tasks` / `control` / `guard` / `traceCheck` 才能算出对照与回放检查。
246
+ */
247
+ function metrics(input = {}) {
248
+ const entries = ledger.query({ limit: Math.max(1, Number(input.limit ?? 5000) || 5000) }).entries;
249
+ const certificates = audit.substitutions({});
250
+ return buildMetricsReport({
251
+ entries,
252
+ certificates,
253
+ tasks: input.tasks,
254
+ control: input.control,
255
+ guard: input.guard,
256
+ audit: { ok: audit.verify().ok },
257
+ traceCheck: typeof input.traceCheck === 'function' ? input.traceCheck : (taskId) => trace({ taskId }),
258
+ });
259
+ }
260
+ /**
261
+ * M1 交付物:记账报表(估算/实测分列、两本账不合并、外部面单列、unattributed 留位)。
262
+ */
263
+ function ledgerReport(input = {}) {
264
+ const entries = ledger.query({ limit: Math.max(1, Number(input.limit ?? 5000) || 5000), ...(input.from === undefined ? {} : { from: Number(input.from) }), ...(input.to === undefined ? {} : { to: Number(input.to) }) }).entries;
265
+ return buildLedgerReport({ entries, from: input.from, to: input.to, bucketMs: input.bucketMs });
266
+ }
267
+ function subscribe(listener) {
268
+ return bus.subscribe(listener);
269
+ }
270
+ function status() {
271
+ const ledgerSnapshot = ledger.snapshot();
272
+ const auditVerify = audit.verify();
273
+ const attributionSnapshot = attribution.snapshot();
274
+ const busSnapshot = bus.snapshot(1);
275
+ return Object.freeze({
276
+ ok: true,
277
+ mode: policy.describe().mode,
278
+ policy: policy.describe(),
279
+ /** 三档命中统计(observe 也计数;shadow 落证书;enforce 才替换)。 */
280
+ policyStats: policy.stats(),
281
+ ledger: ledgerSnapshot,
282
+ budget: budget.state(),
283
+ audit: { ok: auditVerify.ok, count: auditVerify.count, brokenAt: auditVerify.brokenAt },
284
+ attribution: attributionSnapshot,
285
+ /* §5.4:模块收益与未归因收益**分开列**,未归因永不算模块业绩。 */
286
+ attributionSummary: {
287
+ attributedSavedTotal: attributionSnapshot.attributedSavedTotal,
288
+ unattributedSavedTotal: attributionSnapshot.unattributedSavedTotal,
289
+ attributedProjectedTotal: attributionSnapshot.attributedProjectedTotal,
290
+ unattributedProjectedTotal: attributionSnapshot.unattributedProjectedTotal,
291
+ },
292
+ bus: { count: busSnapshot.count, dropped: busSnapshot.dropped, subscribers: busSnapshot.subscribers, relays: busSnapshot.relays },
293
+ degraded: {
294
+ modules: [...degradedByModule.values()].map((item) => ({ ...item, phases: { ...item.phases } })),
295
+ occurrences: [...degradedByModule.values()].reduce((sum, item) => sum + item.occurrences, 0),
296
+ },
297
+ /* 报表口径:只把本项目 worker 面的 execution.saved 当节省;governance 与外部适配器并列。 */
298
+ savings: {
299
+ executionSaved: ledgerSnapshot.ownExecutionSaved,
300
+ governanceCost: ledgerSnapshot.governance.total,
301
+ netSaved: ledgerSnapshot.netSaved,
302
+ /** 外部(宿主面)插件自报:仅对照,不计入上面的任何数字。 */
303
+ externalSaved: ledgerSnapshot.externalSaved,
304
+ },
305
+ /** 估算 token 与实测 token 分列(不混算)。 */
306
+ tokens: ledgerSnapshot.tokens,
307
+ surfaces: ledgerSnapshot.bySurface,
308
+ hostSurface: hostSurface.status(),
309
+ });
310
+ }
311
+ return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status });
312
+ }
@@ -0,0 +1,193 @@
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
+ * 3.3.0 · M1 记账报表(`ledger-report`)。
6
+ *
7
+ * 交付物「记账报表」的实体:把账本事件整理成**可读、可核对**的报表,
8
+ * 并且遵守前几节定下的口径纪律:
9
+ *
10
+ * * **估算 ≠ 实测**:两列分开给,`estimatedRatio` 显式展示,不混算;
11
+ * * **两本账不合并**:execution 与 governance 分账,`netSaved` 只在能对上时给;
12
+ * * **外部面单列**:宿主面外部插件自报收益永不并入本项目节省;
13
+ * * **unattributed 必须留位**:归因缺口显式展示为 0 或非 0,不允许"消失";
14
+ * * **非法行不入账**:JSONL 里坏行跳过并计数,不静默丢弃也不假装成功。
15
+ */
16
+ import { COST_MODULES, COST_PHASES, COST_SURFACES } from "./types.js";
17
+ const num = (value) => {
18
+ const n = Number(value);
19
+ return Number.isFinite(n) ? n : 0;
20
+ };
21
+ const nonNegative = (value) => Math.max(0, num(value));
22
+ /**
23
+ * 解析落盘的 JSONL 账本(`cost-ledger.jsonl`,逐行追加)。
24
+ * 坏行跳过并计数 —— 报表必须能说明"有多少行没读进来"。
25
+ * @param {string} text
26
+ */
27
+ export function parseLedgerJsonl(text) {
28
+ const entries = [];
29
+ let skipped = 0;
30
+ for (const line of String(text ?? '').split('\n')) {
31
+ const trimmed = line.trim();
32
+ if (trimmed === '')
33
+ continue;
34
+ try {
35
+ const parsed = JSON.parse(trimmed);
36
+ if (parsed && typeof parsed === 'object' && (parsed.account === 'execution' || parsed.account === 'governance'))
37
+ entries.push(parsed);
38
+ else
39
+ skipped += 1;
40
+ }
41
+ catch {
42
+ skipped += 1;
43
+ }
44
+ }
45
+ return { entries, skipped };
46
+ }
47
+ const emptyRow = () => ({ entries: 0, inputTokens: 0, outputTokens: 0, total: 0, savedTokens: 0, estimated: false });
48
+ function addTo(map, key, entry) {
49
+ if (!map.has(key))
50
+ map.set(key, { ...emptyRow(), key });
51
+ const row = map.get(key);
52
+ row.entries += 1;
53
+ row.inputTokens += nonNegative(entry.inputTokens);
54
+ row.outputTokens += nonNegative(entry.outputTokens);
55
+ row.total += nonNegative(entry.total);
56
+ row.savedTokens += nonNegative(entry.savedTokens);
57
+ if (entry.estimated === true)
58
+ row.estimated = true;
59
+ return row;
60
+ }
61
+ /**
62
+ * 记账报表。
63
+ *
64
+ * @param {{
65
+ * entries?: object[],
66
+ * from?: number,
67
+ * to?: number,
68
+ * bucketMs?: number,
69
+ * }} [input]
70
+ */
71
+ export function buildLedgerReport(input = {}) {
72
+ const from = Number.isFinite(Number(input.from)) ? Number(input.from) : null;
73
+ const to = Number.isFinite(Number(input.to)) ? Number(input.to) : null;
74
+ const bucketMs = Math.max(1, Number(input.bucketMs ?? 60 * 60 * 1000) || 60 * 60 * 1000);
75
+ const raw = Array.isArray(input.entries) ? input.entries : [];
76
+ const byPhase = new Map();
77
+ const byModule = new Map();
78
+ const byTask = new Map();
79
+ const bySurface = new Map();
80
+ const accounts = {
81
+ execution: { entries: 0, total: 0, savedTokens: 0 },
82
+ governance: { entries: 0, total: 0, savedTokens: 0 },
83
+ };
84
+ const tokens = { measuredTokens: 0, estimatedTokens: 0, inputTokens: 0, outputTokens: 0 };
85
+ const buckets = new Map();
86
+ let ownExecutionSaved = 0;
87
+ let externalSaved = 0;
88
+ let filtered = 0;
89
+ let invalid = 0;
90
+ for (const entry of raw) {
91
+ if (!entry || (entry.account !== 'execution' && entry.account !== 'governance')) {
92
+ invalid += 1;
93
+ continue;
94
+ }
95
+ const at = num(entry.at);
96
+ if (from !== null && at < from) {
97
+ filtered += 1;
98
+ continue;
99
+ }
100
+ if (to !== null && at > to) {
101
+ filtered += 1;
102
+ continue;
103
+ }
104
+ const account = accounts[entry.account];
105
+ account.entries += 1;
106
+ account.total += nonNegative(entry.total);
107
+ const total = nonNegative(entry.total);
108
+ const saved = nonNegative(entry.savedTokens);
109
+ if (entry.estimated === true)
110
+ tokens.estimatedTokens += total;
111
+ else
112
+ tokens.measuredTokens += total;
113
+ tokens.inputTokens += nonNegative(entry.inputTokens);
114
+ tokens.outputTokens += nonNegative(entry.outputTokens);
115
+ if (entry.account === 'execution' && saved > 0) {
116
+ if (entry.external === true)
117
+ externalSaved += saved;
118
+ else
119
+ ownExecutionSaved += saved;
120
+ }
121
+ account.savedTokens += saved;
122
+ addTo(byPhase, String(entry.phase ?? 'unknown'), entry);
123
+ addTo(byModule, String(entry.module ?? 'unattributed'), entry);
124
+ addTo(byTask, String(entry.taskId ?? 'unattributed'), entry);
125
+ addTo(bySurface, String(entry.surface ?? 'worker'), entry);
126
+ const bucketKey = Math.floor(at / bucketMs) * bucketMs;
127
+ if (!buckets.has(bucketKey))
128
+ buckets.set(bucketKey, { at: bucketKey, entries: 0, total: 0, savedTokens: 0, estimatedTokens: 0 });
129
+ const bucket = buckets.get(bucketKey);
130
+ bucket.entries += 1;
131
+ bucket.total += total;
132
+ bucket.savedTokens += saved;
133
+ if (entry.estimated === true)
134
+ bucket.estimatedTokens += total;
135
+ }
136
+ // 固定列:模块与相位都按枚举铺满,缺的给 0(报表要能一眼看出"这一项是 0"而不是"没这一列")
137
+ const phaseRows = COST_PHASES.map((phase) => byPhase.get(phase) ?? { ...emptyRow(), key: phase });
138
+ const moduleRows = COST_MODULES.map((module) => byModule.get(module) ?? { ...emptyRow(), key: module });
139
+ const surfaceRows = COST_SURFACES.map((surface) => bySurface.get(surface) ?? { ...emptyRow(), key: surface });
140
+ const taskRows = [...byTask.values()].sort((a, b) => b.total - a.total || a.key.localeCompare(b.key));
141
+ const allTokens = tokens.measuredTokens + tokens.estimatedTokens;
142
+ return Object.freeze({
143
+ ok: true,
144
+ window: Object.freeze({ from, to, bucketMs }),
145
+ counts: Object.freeze({ entries: accounts.execution.entries + accounts.governance.entries, invalid, filtered }),
146
+ accounts: Object.freeze({
147
+ execution: Object.freeze({ ...accounts.execution }),
148
+ governance: Object.freeze({ ...accounts.governance }),
149
+ /** 两本账相减只在 execution 为「本项目(非外部)」口径时成立。 */
150
+ netSaved: ownExecutionSaved - accounts.governance.total,
151
+ }),
152
+ tokens: Object.freeze({ ...tokens, estimatedRatio: allTokens > 0 ? tokens.estimatedTokens / allTokens : 0 }),
153
+ savings: Object.freeze({
154
+ ownExecutionSaved,
155
+ /** 外部面收益:只展示,不计入本项目净节省。 */
156
+ externalSaved,
157
+ }),
158
+ byPhase: Object.freeze(phaseRows.map((row) => Object.freeze(row))),
159
+ byModule: Object.freeze(moduleRows.map((row) => Object.freeze(row))),
160
+ bySurface: Object.freeze(surfaceRows.map((row) => Object.freeze(row))),
161
+ byTask: Object.freeze(taskRows.map((row) => Object.freeze(row))),
162
+ timeline: Object.freeze([...buckets.values()].sort((a, b) => a.at - b.at).map((row) => Object.freeze(row))),
163
+ /** 记账口径声明,供报表阅读者核对(写进报表而不是只写在文档里)。 */
164
+ discipline: Object.freeze({
165
+ estimatedAndMeasuredSeparated: true,
166
+ accountsNotMerged: true,
167
+ externalNeverSummed: true,
168
+ unattributedAlwaysListed: true,
169
+ }),
170
+ });
171
+ }
172
+ /** 人读行(CLI / 面板共用,避免两处格式分叉)。 */
173
+ export function formatLedgerReport(report) {
174
+ const lines = [];
175
+ lines.push(`记账报表 窗口 ${report.window.from ?? '-'} → ${report.window.to ?? '-'}`);
176
+ lines.push(`事件 ${report.counts.entries} 条(非法 ${report.counts.invalid},被窗口过滤 ${report.counts.filtered})`);
177
+ lines.push('');
178
+ lines.push('账本 条数 token 节省');
179
+ for (const [name, row] of Object.entries(report.accounts)) {
180
+ if (name === 'netSaved')
181
+ continue;
182
+ lines.push(`${name.padEnd(14)} ${String(row.entries).padEnd(8)} ${String(row.total).padEnd(10)} ${row.savedTokens}`);
183
+ }
184
+ lines.push(`净节省(本项目) ${report.accounts.netSaved}`);
185
+ lines.push(`外部面收益(不并入) ${report.savings.externalSaved}`);
186
+ lines.push('');
187
+ lines.push(`估算占比 ${(report.tokens.estimatedRatio * 100).toFixed(1)}%(实测 ${report.tokens.measuredTokens} / 估算 ${report.tokens.estimatedTokens})`);
188
+ lines.push('');
189
+ lines.push('按模块 token 节省');
190
+ for (const row of report.byModule)
191
+ lines.push(`${row.key.padEnd(16)} ${String(row.total).padEnd(10)} ${row.savedTokens}`);
192
+ return lines.join('\n');
193
+ }