@twinsearth/dsh-master 3.2.10 → 3.3.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.
Files changed (104) hide show
  1. package/README.md +4 -0
  2. package/client.js +50 -0
  3. package/dist-host/contract/client.js +7 -0
  4. package/dist-host/contract/constants.js +16 -2
  5. package/dist-host/contract/index.js +1 -0
  6. package/dist-host/contract/relay.js +239 -0
  7. package/dist-host/cost/attribution.js +183 -0
  8. package/dist-host/cost/audit.js +163 -0
  9. package/dist-host/cost/budget.js +211 -0
  10. package/dist-host/cost/bus.js +95 -0
  11. package/dist-host/cost/cert-chain.js +100 -0
  12. package/dist-host/cost/experiment.js +132 -0
  13. package/dist-host/cost/fang-relay.js +329 -0
  14. package/dist-host/cost/gate.js +189 -0
  15. package/dist-host/cost/host-surface.js +80 -0
  16. package/dist-host/cost/index.js +345 -0
  17. package/dist-host/cost/ledger-report.js +193 -0
  18. package/dist-host/cost/ledger.js +401 -0
  19. package/dist-host/cost/metrics.js +198 -0
  20. package/dist-host/cost/milestones.js +209 -0
  21. package/dist-host/cost/modules/index.js +5 -0
  22. package/dist-host/cost/modules/manifests.js +130 -0
  23. package/dist-host/cost/policy.js +276 -0
  24. package/dist-host/cost/report.js +156 -0
  25. package/dist-host/cost/reuse-report.js +128 -0
  26. package/dist-host/cost/rsi.js +367 -0
  27. package/dist-host/cost/toolface.js +147 -0
  28. package/dist-host/cost/types.js +31 -0
  29. package/dist-host/kernel.js +28 -2
  30. package/dist-host/plugins/cost-optimizer.js +329 -27
  31. package/dist-host/plugins/superagent-router.js +104 -1
  32. package/dist-host/superagent/types.js +8 -0
  33. package/dist-worker/agent-loop.js +449 -7
  34. package/dist-worker/baseline-fit.js +66 -0
  35. package/dist-worker/budget-guard.js +93 -0
  36. package/dist-worker/ccs.js +99 -0
  37. package/dist-worker/cost-hooks.js +115 -0
  38. package/dist-worker/cost-modules.js +330 -0
  39. package/dist-worker/cost-spi.js +3 -0
  40. package/dist-worker/edg.js +372 -0
  41. package/dist-worker/gdr.js +159 -0
  42. package/dist-worker/index.js +80 -2
  43. package/dist-worker/llm-http.js +20 -0
  44. package/dist-worker/sink-http.js +1 -0
  45. package/dist-worker/spill.js +115 -0
  46. package/dist-worker/tool-batch.js +66 -0
  47. package/dist-worker/tool-compact.js +275 -0
  48. package/dist-worker/tool-disclosure.js +90 -0
  49. package/dist-worker/tools.js +16 -0
  50. package/docs/ARCHITECTURE.md +24 -0
  51. package/docs/CHANGELOG.md +66 -0
  52. package/docs/DESIGN-v3.3.0-cost-optimization.md +1058 -0
  53. package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
  54. package/docs/EXPERIMENT-M5-real-ab.md +87 -0
  55. package/docs/THIRD-PARTY.md +87 -0
  56. package/package.json +2 -2
  57. package/src/contract/client.js +7 -0
  58. package/src/contract/constants.js +17 -2
  59. package/src/contract/index.js +1 -0
  60. package/src/contract/relay.js +223 -0
  61. package/src/cost/attribution.ts +186 -0
  62. package/src/cost/audit.ts +170 -0
  63. package/src/cost/budget.ts +226 -0
  64. package/src/cost/bus.ts +97 -0
  65. package/src/cost/cert-chain.ts +102 -0
  66. package/src/cost/experiment.ts +136 -0
  67. package/src/cost/fang-relay.ts +318 -0
  68. package/src/cost/gate.ts +191 -0
  69. package/src/cost/host-surface.ts +97 -0
  70. package/src/cost/index.ts +348 -0
  71. package/src/cost/ledger-report.ts +186 -0
  72. package/src/cost/ledger.ts +385 -0
  73. package/src/cost/metrics.ts +193 -0
  74. package/src/cost/milestones.ts +206 -0
  75. package/src/cost/modules/index.ts +13 -0
  76. package/src/cost/modules/manifests.ts +129 -0
  77. package/src/cost/policy.ts +276 -0
  78. package/src/cost/report.ts +158 -0
  79. package/src/cost/reuse-report.ts +131 -0
  80. package/src/cost/rsi.ts +364 -0
  81. package/src/cost/toolface.ts +151 -0
  82. package/src/cost/types.ts +298 -0
  83. package/src/kernel.ts +27 -2
  84. package/src/plugins/cost-optimizer.ts +313 -26
  85. package/src/plugins/superagent-router.ts +96 -1
  86. package/src/superagent/types.ts +70 -0
  87. package/worker/agent-loop.ts +444 -7
  88. package/worker/baseline-fit.ts +80 -0
  89. package/worker/budget-guard.ts +156 -0
  90. package/worker/ccs.ts +202 -0
  91. package/worker/cost-hooks.ts +133 -0
  92. package/worker/cost-modules.ts +356 -0
  93. package/worker/cost-spi.ts +283 -0
  94. package/worker/edg.ts +424 -0
  95. package/worker/gdr.ts +233 -0
  96. package/worker/index.ts +84 -3
  97. package/worker/llm-http.ts +18 -0
  98. package/worker/sink-http.ts +5 -2
  99. package/worker/spill.ts +133 -0
  100. package/worker/tool-batch.ts +124 -0
  101. package/worker/tool-compact.ts +317 -0
  102. package/worker/tool-disclosure.ts +131 -0
  103. package/worker/tools.ts +553 -531
  104. package/worker/types.ts +191 -0
@@ -0,0 +1,191 @@
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 · gate(发布门禁,设计 §10.2)。
7
+ *
8
+ * **3.3.0 不通过不发布**。六道门:
9
+ *
10
+ * | # | 门禁 | 判定 |
11
+ * |---|---|---|
12
+ * | G1 | `npm run check` 0 error | 退出码 |
13
+ * | G2 | `npm run build` 0 error | 退出码 |
14
+ * | G3 | `npm test` 全绿(含新增模块测试) | 退出码 + 用例数 |
15
+ * | G4 | 新增测试至少覆盖:I1–I8 各一条、守卫命中/未命中、schema 漂移、证书回放、归因不重复计 | 覆盖清单逐项核验 |
16
+ * | G5 | 真实 fork 实机:≥10 任务的对照组 vs enforce 组,tokens/task 与成功率同表 | 样本量 + 同表 + 成功率不退化 |
17
+ * | G6 | 零残留:临时 profile / 工作区清理证据 | 路径扫描 |
18
+ * | G7 | 发布需用户明确授权 | 默认 **未授权**(设计不含发布动作) |
19
+ *
20
+ * 门禁的价值在于**敢说不**:任一门不满足即 `ok:false` 并列出 `blockers`,
21
+ * 不允许"部分通过"。
22
+ */
23
+
24
+ /** 必须被测试覆盖的能力(G4),每项给出识别标记。 */
25
+ export const REQUIRED_COVERAGE = Object.freeze([
26
+ { id: 'I1', label: '预算不足不启动模型调用', patterns: [/I1:.*budget/u] },
27
+ { id: 'I2', label: 'CCS 只经原生派发器 / 无派发器拒绝', patterns: [/I2:.*native dispatcher/u, /I2:.*non-native/u] },
28
+ { id: 'I3', label: '破坏性工具默认不入池', patterns: [/I3:.*destructive/u] },
29
+ { id: 'I4', label: 'schema 漂移整图作废', patterns: [/I4:.*schema drift/u] },
30
+ { id: 'I5', label: '守卫不匹配回落慢模型', patterns: [/I5:.*guard miss/u] },
31
+ { id: 'I6', label: '模块异常 degraded 且任务继续', patterns: [/I6:.*throwing module/u] },
32
+ { id: 'I7', label: '压缩回执可回读原文(spill)', patterns: [/I7:.*read back/u] },
33
+ { id: 'I8', label: '取消信号阻断替换', patterns: [/I8:.*aborted signal/u] },
34
+ { id: 'guard-hit', label: '守卫命中', patterns: [/all five gates must pass/u] },
35
+ { id: 'guard-miss', label: '守卫未命中', patterns: [/each gate has its own miss reason/u] },
36
+ { id: 'schema-drift', label: 'schema 漂移(B2 层)', patterns: [/schema drift archives the whole graph/u, /schemaHash/u] },
37
+ { id: 'cert-replay', label: '证书回放', patterns: [/100% replayable/u] },
38
+ { id: 'no-double-count', label: '归因不重复计', patterns: [/doubleCountGuard/u, /mutually exclusive/u] },
39
+ ])
40
+
41
+ /** G5:对照组 vs enforce 组的最小样本量。 */
42
+ export const MIN_FORK_TASKS = 10
43
+ /** G5:成功率允许的最大下降(与 §10.1 的 2pp 门禁一致)。 */
44
+ export const MAX_SUCCESS_DROP_PP = 0.02
45
+
46
+ const num = (value) => {
47
+ const n = Number(value)
48
+ return Number.isFinite(n) ? n : 0
49
+ }
50
+
51
+ /**
52
+ * G4:扫描测试源码,逐项确认覆盖(找不到标记即报缺)。
53
+ * @param {Record<string, string>} sources 文件名 → 源码
54
+ */
55
+ export function checkCoverage(sources = {}) {
56
+ const haystack = Object.values(sources).join('\n')
57
+ const missing = []
58
+ const covered = []
59
+ for (const item of REQUIRED_COVERAGE) {
60
+ const found = item.patterns.some((pattern) => pattern.test(haystack))
61
+ if (found) covered.push(item.id)
62
+ else missing.push({ id: item.id, label: item.label })
63
+ }
64
+ return { ok: missing.length === 0, covered, missing }
65
+ }
66
+
67
+ /**
68
+ * G5:评估真实 fork 实机对照实验。
69
+ * @param {{control?: {tasks: Array<{taskId: string, tokens: number, success: boolean}>}, enforce?: {tasks: Array<{taskId: string, tokens: number, success: boolean}>}}} input
70
+ */
71
+ export function evaluateForkExperiment(input = {}) {
72
+ const control = Array.isArray(input?.control?.tasks) ? input.control.tasks : []
73
+ const enforce = Array.isArray(input?.enforce?.tasks) ? input.enforce.tasks : []
74
+ const problems = []
75
+ // 证据来源必须真实:dry-run(合成语料 + 脚本化模型)不能冒充 G5。
76
+ const harness = input?.harness === undefined ? 'real-fork' : String(input.harness)
77
+ if (harness === 'local-dry-run') problems.push('harness-not-real')
78
+ if (control.length < MIN_FORK_TASKS) problems.push(`control-tasks<${MIN_FORK_TASKS}`)
79
+ if (enforce.length < MIN_FORK_TASKS) problems.push(`enforce-tasks<${MIN_FORK_TASKS}`)
80
+
81
+ const avg = (rows) => (rows.length > 0 ? rows.reduce((sum, row) => sum + num(row?.tokens), 0) / rows.length : null)
82
+ const rate = (rows) => (rows.length > 0 ? rows.filter((row) => row?.success === true).length / rows.length : null)
83
+
84
+ const controlTokensPerTask = avg(control)
85
+ const enforceTokensPerTask = avg(enforce)
86
+ const controlSuccess = rate(control)
87
+ const enforceSuccess = rate(enforce)
88
+ const tokenDelta = controlTokensPerTask !== null && enforceTokensPerTask !== null
89
+ ? Math.round((enforceTokensPerTask - controlTokensPerTask) * 100) / 100
90
+ : null
91
+ const successDelta = controlSuccess !== null && enforceSuccess !== null
92
+ ? Math.round((enforceSuccess - controlSuccess) * 10000) / 10000
93
+ : null
94
+ if (successDelta !== null && successDelta <= -MAX_SUCCESS_DROP_PP) problems.push('success-regression>=2pp')
95
+ // 同表:两组必须都在同一份报告里给出 tokens/task 与成功率
96
+ const sameTable = controlTokensPerTask !== null && enforceTokensPerTask !== null && controlSuccess !== null && enforceSuccess !== null
97
+ if (!sameTable) problems.push('groups-not-in-same-table')
98
+
99
+ return {
100
+ ok: problems.length === 0,
101
+ problems,
102
+ samples: { control: control.length, enforce: enforce.length },
103
+ table: [
104
+ { group: 'control', samples: control.length, tokensPerTask: controlTokensPerTask, successRate: controlSuccess },
105
+ { group: 'enforce', samples: enforce.length, tokensPerTask: enforceTokensPerTask, successRate: enforceSuccess },
106
+ ],
107
+ delta: { tokensPerTask: tokenDelta, successRate: successDelta },
108
+ /** token 变好且成功率不退化,才算"通过且有效"。 */
109
+ improvement: tokenDelta !== null && tokenDelta < 0 && (successDelta === null || successDelta > -MAX_SUCCESS_DROP_PP),
110
+ }
111
+ }
112
+
113
+ /**
114
+ * G6:零残留——给出的路径里不允许还有临时 profile / 工作区。
115
+ * @param {Array<{path: string, exists: boolean, kind?: string}>} checks
116
+ */
117
+ export function checkResidue(checks = []) {
118
+ const leftovers = checks.filter((entry) => entry?.exists === true).map((entry) => ({ path: String(entry.path), kind: String(entry.kind ?? 'unknown') }))
119
+ return {
120
+ ok: leftovers.length === 0,
121
+ checked: checks.length,
122
+ leftovers,
123
+ /** 没有检查项等于没有证据,不算通过。 */
124
+ evidence: checks.length > 0 ? '<results>' : null,
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 汇总门禁。
130
+ * @param {{
131
+ * commands?: Array<{id: string, exitCode: number, note?: string}>,
132
+ * coverage?: ReturnType<typeof checkCoverage>,
133
+ * forkExperiment?: ReturnType<typeof evaluateForkExperiment>,
134
+ * residue?: ReturnType<typeof checkResidue>,
135
+ * releaseAuthorized?: boolean,
136
+ * }} input
137
+ */
138
+ export function evaluateGate(input = {}) {
139
+ const items = []
140
+ const blockers = []
141
+
142
+ for (const command of Array.isArray(input.commands) ? input.commands : []) {
143
+ const id = String(command?.id ?? 'unknown')
144
+ const passed = num(command?.exitCode) === 0
145
+ items.push({ gate: id, ok: passed, detail: `exit=${num(command?.exitCode)}${command?.note ? ` ${command.note}` : ''}` })
146
+ if (!passed) blockers.push(`${id}:exit-code`)
147
+ }
148
+
149
+ const coverage = input.coverage
150
+ if (!coverage) {
151
+ blockers.push('G4:coverage-not-checked')
152
+ items.push({ gate: 'G4', ok: false, detail: '未执行覆盖核验' })
153
+ } else {
154
+ items.push({ gate: 'G4', ok: coverage.ok === true, detail: coverage.ok ? `${coverage.covered.length} 项全中` : `缺 ${coverage.missing.map((item) => item.id).join(',')}` })
155
+ if (coverage.ok !== true) blockers.push(`G4:missing:${coverage.missing.map((item) => item.id).join(',')}`)
156
+ }
157
+
158
+ const fork = input.forkExperiment
159
+ if (!fork) {
160
+ blockers.push('G5:fork-experiment-missing')
161
+ items.push({ gate: 'G5', ok: false, detail: '尚无真实 fork 对照数据(≥10 任务/组)' })
162
+ } else {
163
+ items.push({ gate: 'G5', ok: fork.ok === true, detail: fork.ok ? `control ${fork.table[0].tokensPerTask} → enforce ${fork.table[1].tokensPerTask} tokens/task` : fork.problems.join(',') })
164
+ if (fork.ok !== true) blockers.push(`G5:${fork.problems.join(',')}`)
165
+ }
166
+
167
+ const residue = input.residue
168
+ if (!residue) {
169
+ blockers.push('G6:residue-not-checked')
170
+ items.push({ gate: 'G6', ok: false, detail: '未提供清理证据' })
171
+ } else {
172
+ items.push({ gate: 'G6', ok: residue.ok === true, detail: residue.ok ? `${residue.checked} 个路径均无残留` : `残留: ${residue.leftovers.map((item) => item.path).join(',')}` })
173
+ if (residue.ok !== true) blockers.push('G6:residue')
174
+ }
175
+
176
+ // G7:默认未授权。设计本身不含发布动作;授权是人工显式行为。
177
+ const authorized = input.releaseAuthorized === true
178
+ items.push({ gate: 'G7', ok: authorized, detail: authorized ? '用户已明确授权' : '未授权(发布需用户明确授权)' })
179
+ if (!authorized) blockers.push('G7:release-not-authorized')
180
+
181
+ const hardGates = ['G1', 'G2', 'G3', 'G4', 'G5', 'G6']
182
+ const hardOk = items.filter((item) => hardGates.includes(item.gate)).every((item) => item.ok)
183
+ return {
184
+ ok: hardOk && authorized,
185
+ /** 不发布 ≠ 代码不可用:G1–G4 + G6 全绿时,代码质量门禁是过了的。 */
186
+ bootstrapOk: hardOk,
187
+ blockers,
188
+ items,
189
+ releaseAuthorized: authorized,
190
+ }
191
+ }
@@ -0,0 +1,97 @@
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 · 宿主会话工具面适配器(可选)。
7
+ *
8
+ * 设计 §3.3:`dsh-ops` 这类宿主插件作用于**宿主会话工具面**(DSH 原生工具准入/
9
+ * 执行器接口),而本项目 A1–A3 作用于**master Worker 自有工具面**(`worker/tools.ts`)。
10
+ * 两者作用面不同,不能合并统计,也不能互相替代。
11
+ *
12
+ * 因此本文件**不重写、不依赖、不冒领**任何宿主插件:
13
+ * * 它只提供一个「外部收益登记」入口,由宿主侧(或用户脚本)把某个宿主插件的
14
+ * 自报收益按 `surface: 'host', external: true` 写入账本;
15
+ * * 这些条目永远不进入 `ownExecutionSaved` / `netSaved`,只作为对照展示;
16
+ * * 未安装宿主插件时,本适配器完全空转(无网络、无副作用、零依赖)。
17
+ *
18
+ * 这是「双管齐下」的第二半:**协议等价实现(Worker 面自研)+ 可选适配器(宿主面归因)**。
19
+ */
20
+
21
+ import { createCostBus } from './bus.ts'
22
+
23
+ const SOURCES = new Set(['dsh-ops', 'other'])
24
+
25
+ function finiteNonNegative(value) {
26
+ const n = Number(value)
27
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null
28
+ }
29
+
30
+ /**
31
+ * @param {{
32
+ * ledger: {append: Function},
33
+ * enabled?: boolean,
34
+ * source?: string,
35
+ * bus?: ReturnType<typeof createCostBus>,
36
+ * now?: () => number,
37
+ * }} options
38
+ */
39
+ export function createHostSurfaceAdapter(options = {}) {
40
+ const ledger = options.ledger
41
+ if (!ledger || typeof ledger.append !== 'function') throw new Error('host-surface adapter requires a ledger')
42
+ const enabled = options.enabled === true
43
+ const source = SOURCES.has(String(options.source ?? 'dsh-ops')) ? String(options.source ?? 'dsh-ops') : 'other'
44
+ const bus = options.bus && typeof options.bus.publish === 'function' ? options.bus : null
45
+ const now = typeof options.now === 'function' ? options.now : () => Date.now()
46
+ let recorded = 0
47
+ let tokensClaimed = 0
48
+
49
+ /**
50
+ * 登记一次宿主面外部收益(只展示,不计入本项目节省)。
51
+ * @param {{taskId?: string, savedTokens: number, baselineTokens?: number, meta?: object}} input
52
+ */
53
+ function record(input = {}) {
54
+ if (!enabled) return { ok: false, code: 'host-adapter-disabled', message: 'host surface adapter is opt-in; enable it in config' }
55
+ const saved = finiteNonNegative(input.savedTokens)
56
+ if (saved === null) return { ok: false, code: 'saved-tokens-invalid', message: 'savedTokens must be a non-negative number' }
57
+ const appended = ledger.append({
58
+ account: 'execution',
59
+ surface: 'host',
60
+ external: true,
61
+ module: 'a1-compact',
62
+ source: 'tool',
63
+ phase: 'tool',
64
+ taskId: input.taskId,
65
+ totalTokens: 0,
66
+ savedTokens: saved,
67
+ meta: {
68
+ ...(input.meta && typeof input.meta === 'object' ? input.meta : {}),
69
+ adapter: source,
70
+ external: true,
71
+ note: 'third-party host-surface saving; shown for comparison only',
72
+ },
73
+ })
74
+ if (!appended.ok) return { ok: false, code: appended.code, message: appended.message }
75
+ recorded += 1
76
+ tokensClaimed += saved
77
+ try { bus?.publish?.('cost.host-surface', { adapter: source, savedTokens: saved, taskId: input.taskId ?? null }) } catch { /* ignore */ }
78
+ return { ok: true, adapter: source, external: true, savedTokens: saved, ledger: { id: appended.entry.id }, at: now() }
79
+ }
80
+
81
+ function status() {
82
+ return Object.freeze({
83
+ ok: true,
84
+ enabled,
85
+ adapter: source,
86
+ /** 明确声明:本适配器不产生本项目收益。 */
87
+ contributesToOwnSavings: false,
88
+ recorded,
89
+ savedTokens: tokensClaimed,
90
+ note: enabled
91
+ ? '外部宿主面收益只做对照展示,不计入 executionSaved / netSaved'
92
+ : '未启用:本设计不依赖任何第三方宿主插件',
93
+ })
94
+ }
95
+
96
+ return Object.freeze({ record, status })
97
+ }
@@ -0,0 +1,348 @@
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 系统层入口:把 ledger / budget / policy / audit 组装成一个系统。
7
+ *
8
+ * `cost-optimizer` 插件通过它拿到统一的记账、预算、灰度与审计口径;
9
+ * M1 只做 observe(不接入任何替换路径),所以这些组件只被读取与记账,
10
+ * 不改变 Worker/模型调用行为。
11
+ */
12
+
13
+ import { createLedger } from './ledger.ts'
14
+ import { createBudget } from './budget.ts'
15
+ import { createPolicy } from './policy.ts'
16
+ import { createAuditChain } from './audit.ts'
17
+ import { createAttribution } from './attribution.ts'
18
+ import { createCostBus } from './bus.ts'
19
+ import { createHostSurfaceAdapter } from './host-surface.ts'
20
+ import { createFangRelay } from './fang-relay.ts'
21
+ import { buildSavingsReport, classifyEntry } from './report.ts'
22
+ import { buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP } from './metrics.ts'
23
+ import { MODULE_MANIFESTS, MODULE_IDS, validateManifests } from './modules/manifests.ts'
24
+ import { buildLedgerReport, formatLedgerReport, parseLedgerJsonl } from './ledger-report.ts'
25
+ import { buildToolFaceReport, planToolFace } from './toolface.ts'
26
+ import { buildReuseReport, formatReuseReport } from './reuse-report.ts'
27
+
28
+ export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, createFangRelay, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport }
29
+ export * from './types.ts'
30
+ export { RELAY_PEERS, RELAY_DEFAULTS, createFangRelayFromContext, createRelayClient } from './fang-relay.ts'
31
+
32
+ /**
33
+ * @param {{
34
+ * mode?: string, allowlist?: string[] | {forks?:string[],tasks?:string[],tags?:string[],tools?:string[]},
35
+ * policyScopes?: {global?:string, forks?:Record<string,string>, tasks?:Record<string,string>},
36
+ * budgetLimits?: Array<{unit:string, limitTokens?:number, limitCost?:number, onExhaust?:string, degradeTo?:string}>,
37
+ * degradeModels?: string[],
38
+ * now?: () => number, maxEntries?: number, maxRecords?: number,
39
+ * relay?: object, relayClient?: object|null, relayOptions?: object, relayAutoAttach?: boolean,
40
+ * }} [options]
41
+ */
42
+ export function createCostSystem(options = {}) {
43
+ const now = typeof options.now === 'function' ? options.now : () => Date.now()
44
+ const audit = createAuditChain({ now, maxRecords: options.maxRecords })
45
+ const policy = createPolicy({ mode: options.mode, allowlist: options.allowlist, scopes: options.policyScopes, audit, now })
46
+ const budget = createBudget({ limits: options.budgetLimits, degradeModels: options.degradeModels, now })
47
+ const ledger = createLedger({
48
+ now,
49
+ maxEntries: options.maxEntries,
50
+ dataDir: options.dataDir,
51
+ persist: options.persist === true,
52
+ logger: options.logger,
53
+ })
54
+ const attribution = createAttribution({ ledger, now })
55
+ const bus = createCostBus({ now, maxEvents: options.maxBusEvents, relays: options.relays })
56
+ /**
57
+ * §5.6 / M5:瓮城回流出口。
58
+ * 传入 `relay`(已有适配器)则复用;否则用 `relayClient` 现造一个。
59
+ * 没有客户端时出口状态是 `relay-client-absent` —— 显式不可用,不是“探测中”。
60
+ */
61
+ const relay = options.relay && typeof options.relay.status === 'function'
62
+ ? options.relay
63
+ : createFangRelay({ client: options.relayClient ?? null, now, ...(options.relayOptions && typeof options.relayOptions === 'object' ? options.relayOptions : {}), logger: options.logger })
64
+ // 默认把出口挂到总线;测试可关掉自动挂载,手动投递。
65
+ if (options.relayAutoAttach !== false && typeof relay.attach === 'function') {
66
+ try { relay.attach(bus) } catch { /* 回流挂载失败不影响记账 */ }
67
+ }
68
+ /** 宿主会话工具面适配器(可选;默认关闭,不依赖任何第三方插件)。 */
69
+ const hostSurface = createHostSurfaceAdapter({
70
+ ledger,
71
+ bus,
72
+ enabled: options.hostSurface?.enabled === true,
73
+ source: options.hostSurface?.source,
74
+ now,
75
+ })
76
+ /** 模块降级累计(按模块聚合;不参与节省计算)。 */
77
+ const degradedByModule = new Map()
78
+
79
+ function recordUsage(input = {}) {
80
+ const out = ledger.append({
81
+ account: input.account ?? 'execution',
82
+ module: input.module,
83
+ source: input.source ?? 'model',
84
+ phase: input.phase,
85
+ surface: input.surface,
86
+ external: input.external,
87
+ taskId: input.taskId,
88
+ forkId: input.forkId,
89
+ sessionId: input.sessionId,
90
+ ticketId: input.ticketId,
91
+ turn: input.turn,
92
+ step: input.step,
93
+ provider: input.provider,
94
+ model: input.model,
95
+ toolName: input.toolName,
96
+ inputTokens: input.inputTokens,
97
+ outputTokens: input.outputTokens,
98
+ cachedTokens: input.cachedTokens,
99
+ totalTokens: input.totalTokens,
100
+ baselineTokens: input.baselineTokens,
101
+ savedTokens: input.savedTokens,
102
+ estimated: input.estimated,
103
+ meta: input.meta,
104
+ })
105
+ if (out.ok) {
106
+ bus.publish('cost:event', { kind: 'ledger',
107
+ id: out.entry.id,
108
+ account: out.entry.account,
109
+ module: out.entry.module,
110
+ phase: out.entry.phase,
111
+ surface: out.entry.surface,
112
+ total: out.entry.total,
113
+ savedTokens: out.entry.savedTokens,
114
+ taskId: out.entry.taskId,
115
+ estimated: out.entry.estimated,
116
+ })
117
+ }
118
+ return out
119
+ }
120
+
121
+ /** 按维度查询账本(task/fork/session/module/account/phase/时间窗)。 */
122
+ function query(filter = {}) {
123
+ return ledger.query(filter)
124
+ }
125
+
126
+ /** 从 JSONL 回放(进程重启后恢复报表)。 */
127
+ function replay(direction = 'forward') {
128
+ return ledger.replay(direction)
129
+ }
130
+
131
+ /**
132
+ * §8 联动报表:A(单价下降)与 B(跳过调用)分列,governance 摊销后逐任务算净额,
133
+ * 净亏任务显式列出(不隐藏)。
134
+ */
135
+ function savingsReport(input = {}) {
136
+ const from = input.from === undefined ? undefined : Number(input.from)
137
+ const to = input.to === undefined ? undefined : Number(input.to)
138
+ const entries = ledger.query({ limit: Math.max(1, Number(input.limit ?? 5000) || 5000), ...(from === undefined ? {} : { from }), ...(to === undefined ? {} : { to }) }).entries
139
+ const certificates = audit.substitutions({})
140
+ return buildSavingsReport({ entries, certificates, amortize: input.amortize })
141
+ }
142
+
143
+ function authorize(input = {}) {
144
+ const out = budget.authorize(input)
145
+ if (!out.ok) {
146
+ try { audit.append({ type: 'budget.denied', actor: 'cost-budget', payload: { ...out } }) } catch { /* ignore */ }
147
+ bus.publish('cost:event', { kind: 'budget.denied', ...out })
148
+ } else if (out.degraded) {
149
+ try { audit.append({ type: 'budget.degraded', actor: 'cost-budget', payload: { ...out } }) } catch { /* ignore */ }
150
+ bus.publish('cost:event', { kind: 'budget.degraded', ...out })
151
+ } else {
152
+ bus.publish('cost:event', { kind: 'budget.authorized', unit: out.unit, key: out.key, consumedTokens: out.consumedTokens, remainingTokens: out.remainingTokens })
153
+ }
154
+ return out
155
+ }
156
+
157
+ /** 归因入口:enforce 写账本;shadow 只累计「本可节省」。 */
158
+ function attribute(input = {}) {
159
+ const out = attribution.attribute(input)
160
+ if (out.ok) bus.publish('cost:event', { kind: 'attribution', mode: out.mode, module: out.module, savedTokens: out.savedTokens ?? 0, projectedTokens: out.projectedTokens ?? 0 })
161
+ return out
162
+ }
163
+
164
+ /** §5.4:总额 − 各模块之和,差额进 unattributed(报表单列)。 */
165
+ function attributeTotal(input = {}) {
166
+ const out = attribution.attributeTotal(input)
167
+ if (out.ok) bus.publish('cost:event', { kind: 'attribution.total', totalSavedTokens: out.totalSavedTokens, attributedTokens: out.attributedTokens, unattributedTokens: out.unattributedTokens })
168
+ return out
169
+ }
170
+
171
+ /**
172
+ * §5.5:一次真实替换 = 证书 + 账本事件,两者共用 `taskId + step`。
173
+ * 任何一步失败都 fail-closed(不写半套)。
174
+ */
175
+ function substitute(cert = {}) {
176
+ const recorded = audit.appendSubstitution(cert)
177
+ if (!recorded.ok) {
178
+ bus.publish('cost:event', { kind: 'substitution.rejected', code: recorded.code, taskId: cert?.taskId ?? null })
179
+ return recorded
180
+ }
181
+ let attributed = null
182
+ if (Number(recorded.cert.savedTokens) > 0) {
183
+ attributed = attribution.attribute({
184
+ taskId: recorded.cert.taskId,
185
+ module: cert.moduleId ?? 'b3-ccs',
186
+ savedTokens: recorded.cert.savedTokens,
187
+ mode: 'enforce',
188
+ forkId: recorded.cert.forkId ?? undefined,
189
+ sessionId: recorded.cert.sessionId ?? undefined,
190
+ step: recorded.cert.step,
191
+ meta: { kind: 'substitution', step: recorded.cert.step, edgeId: recorded.cert.edgeId, replacedCallbackId: recorded.cert.replacedCallbackId },
192
+ })
193
+ if (!attributed.ok) return attributed
194
+ }
195
+ bus.publish('cost:substitution', {
196
+ taskId: recorded.cert.taskId,
197
+ step: recorded.cert.step,
198
+ edgeId: recorded.cert.edgeId,
199
+ toolName: recorded.cert.toolName,
200
+ savedTokens: recorded.cert.savedTokens,
201
+ })
202
+ return { ok: true, cert: recorded.cert, attribution: attributed, hash: recorded.record.hash }
203
+ }
204
+
205
+ /**
206
+ * §5.5:回放「这一省是怎么来的」——按 `taskId + step` 取出证书与账本事件。
207
+ */
208
+ function trace({ taskId, step } = {}) {
209
+ if (taskId === undefined || String(taskId).trim() === '') {
210
+ return { ok: false, code: 'task-missing', message: 'trace requires taskId' }
211
+ }
212
+ const filter = { taskId }
213
+ if (step !== undefined) filter.step = step
214
+ const certificates = audit.substitutions(filter)
215
+ const entries = ledger.query({ taskId, limit: 500 }).entries
216
+ .filter((entry) => (step === undefined ? true : entry.step === Number(step)))
217
+ const savedFromCerts = certificates.reduce((sum, cert) => sum + Number(cert.savedTokens ?? 0), 0)
218
+ // 对账只比「替换产生的账本事件」:其它节省(A1 压缩等)不属于证书口径。
219
+ const substitutionEntries = entries.filter((entry) => entry.meta?.kind === 'substitution')
220
+ const savedFromLedger = substitutionEntries.reduce((sum, entry) => sum + entry.savedTokens, 0)
221
+ const savedFromLedgerAll = entries.reduce((sum, entry) => sum + entry.savedTokens, 0)
222
+ return Object.freeze({
223
+ ok: true,
224
+ taskId: String(taskId),
225
+ step: step === undefined ? null : Number(step),
226
+ certificates,
227
+ entries,
228
+ /** 两边对账:证书声明的节省应等于**替换账本**记到的节省。 */
229
+ savedFromCertificates: savedFromCerts,
230
+ savedFromLedger,
231
+ /** 任务全部节省(含 A1 等非替换来源),仅展示,不参与对账。 */
232
+ savedFromLedgerAll,
233
+ reconciled: savedFromCerts === savedFromLedger,
234
+ })
235
+ }
236
+
237
+ /**
238
+ * 降级留痕(目标 2.1-5):模块异常 → 审计链 + 事件总线 + 计数。
239
+ * 降级**不计入任何节省**,只用于让报表能区分「真的省了」与「模块挂了」。
240
+ */
241
+ function degraded(input = {}) {
242
+ const module = String(input.module ?? 'unknown')
243
+ const phase = String(input.phase ?? 'transform')
244
+ const message = String(input.message ?? '')
245
+ const count = Math.max(1, Number(input.count ?? 1) || 1)
246
+ const at = now()
247
+ const totals = degradedByModule.get(module) ?? { module, events: 0, occurrences: 0, lastAt: 0, phases: {} }
248
+ totals.events += 1
249
+ totals.occurrences += count
250
+ totals.lastAt = at
251
+ totals.phases[phase] = (totals.phases[phase] ?? 0) + count
252
+ degradedByModule.set(module, totals)
253
+ try { audit.append({ type: 'cost.degraded', actor: 'cost-system', payload: { module, phase, message, count, taskId: input.taskId ?? null } }) } catch { /* ignore */ }
254
+ bus.publish('cost:degraded', { module, phase, message, count, taskId: input.taskId ?? null })
255
+ return { ok: true, module, phase, count, total: { ...totals, phases: { ...totals.phases } }, at }
256
+ }
257
+
258
+ /**
259
+ * §10.1 验收指标:七项必须给全;缺项显式报 `complete:false + missing[]`。
260
+ * 传入 `tasks` / `control` / `guard` / `traceCheck` 才能算出对照与回放检查。
261
+ */
262
+ function metrics(input = {}) {
263
+ const entries = ledger.query({ limit: Math.max(1, Number(input.limit ?? 5000) || 5000) }).entries
264
+ const certificates = audit.substitutions({})
265
+ return buildMetricsReport({
266
+ entries,
267
+ certificates,
268
+ tasks: input.tasks,
269
+ control: input.control,
270
+ guard: input.guard,
271
+ audit: { ok: audit.verify().ok },
272
+ traceCheck: typeof input.traceCheck === 'function' ? input.traceCheck : (taskId) => trace({ taskId }),
273
+ })
274
+ }
275
+
276
+ /**
277
+ * M1 交付物:记账报表(估算/实测分列、两本账不合并、外部面单列、unattributed 留位)。
278
+ */
279
+ function ledgerReport(input = {}) {
280
+ 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
281
+ return buildLedgerReport({ entries, from: input.from, to: input.to, bucketMs: input.bucketMs })
282
+ }
283
+
284
+ function subscribe(listener) {
285
+ return bus.subscribe(listener)
286
+ }
287
+
288
+ /** §5.6:探测回流对端(hello 协商 + relay 状态),结果并入 `status().relay`。 */
289
+ async function relayProbe(input = {}) {
290
+ return await relay.probe({ force: input.force === true })
291
+ }
292
+
293
+ /** §5.6:投递当前队列里的回流事件;返回回执摘要(不吞错,不谎报送达)。 */
294
+ async function relayFlush() {
295
+ if (typeof relay.flush !== 'function') return { ok: false, code: 'relay-flush-unsupported' }
296
+ return await relay.flush()
297
+ }
298
+
299
+ function relayStatus() {
300
+ return relay.status()
301
+ }
302
+
303
+ function status() {
304
+ const ledgerSnapshot = ledger.snapshot()
305
+ const auditVerify = audit.verify()
306
+ const attributionSnapshot = attribution.snapshot()
307
+ const busSnapshot = bus.snapshot(1)
308
+ return Object.freeze({
309
+ ok: true,
310
+ mode: policy.describe().mode,
311
+ policy: policy.describe(),
312
+ /** 三档命中统计(observe 也计数;shadow 落证书;enforce 才替换)。 */
313
+ policyStats: policy.stats(),
314
+ ledger: ledgerSnapshot,
315
+ budget: budget.state(),
316
+ audit: { ok: auditVerify.ok, count: auditVerify.count, brokenAt: auditVerify.brokenAt },
317
+ attribution: attributionSnapshot,
318
+ /* §5.4:模块收益与未归因收益**分开列**,未归因永不算模块业绩。 */
319
+ attributionSummary: {
320
+ attributedSavedTotal: attributionSnapshot.attributedSavedTotal,
321
+ unattributedSavedTotal: attributionSnapshot.unattributedSavedTotal,
322
+ attributedProjectedTotal: attributionSnapshot.attributedProjectedTotal,
323
+ unattributedProjectedTotal: attributionSnapshot.unattributedProjectedTotal,
324
+ },
325
+ bus: { count: busSnapshot.count, dropped: busSnapshot.dropped, subscribers: busSnapshot.subscribers, relays: busSnapshot.relays },
326
+ /** §5.6 / M5:瓮城回流出口的真实状态(可达则 available:true + 对端自报 peer)。 */
327
+ relay: relay.status(),
328
+ degraded: {
329
+ modules: [...degradedByModule.values()].map((item) => ({ ...item, phases: { ...item.phases } })),
330
+ occurrences: [...degradedByModule.values()].reduce((sum, item) => sum + item.occurrences, 0),
331
+ },
332
+ /* 报表口径:只把本项目 worker 面的 execution.saved 当节省;governance 与外部适配器并列。 */
333
+ savings: {
334
+ executionSaved: ledgerSnapshot.ownExecutionSaved,
335
+ governanceCost: ledgerSnapshot.governance.total,
336
+ netSaved: ledgerSnapshot.netSaved,
337
+ /** 外部(宿主面)插件自报:仅对照,不计入上面的任何数字。 */
338
+ externalSaved: ledgerSnapshot.externalSaved,
339
+ },
340
+ /** 估算 token 与实测 token 分列(不混算)。 */
341
+ tokens: ledgerSnapshot.tokens,
342
+ surfaces: ledgerSnapshot.bySurface,
343
+ hostSurface: hostSurface.status(),
344
+ })
345
+ }
346
+
347
+ return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, relay, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status, relayProbe, relayFlush, relayStatus })
348
+ }