@twinsearth/dsh-master 3.2.9 → 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 (102) hide show
  1. package/README.md +39 -6
  2. package/client.js +51 -1
  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 +38 -11
  25. package/dist-host/plugins/cost-optimizer.js +277 -27
  26. package/dist-host/plugins/superagent-router.js +89 -2
  27. package/dist-host/plugins/superagent-worker.js +1 -1
  28. package/dist-host/superagent/router.js +1 -1
  29. package/dist-host/superagent/types.js +8 -0
  30. package/dist-worker/agent-loop.js +449 -7
  31. package/dist-worker/baseline-fit.js +66 -0
  32. package/dist-worker/budget-guard.js +93 -0
  33. package/dist-worker/ccs.js +99 -0
  34. package/dist-worker/cost-hooks.js +115 -0
  35. package/dist-worker/cost-modules.js +330 -0
  36. package/dist-worker/cost-spi.js +3 -0
  37. package/dist-worker/edg.js +372 -0
  38. package/dist-worker/gdr.js +159 -0
  39. package/dist-worker/index.js +80 -2
  40. package/dist-worker/llm-http.js +20 -0
  41. package/dist-worker/sink-http.js +1 -0
  42. package/dist-worker/spill.js +115 -0
  43. package/dist-worker/tool-batch.js +66 -0
  44. package/dist-worker/tool-compact.js +275 -0
  45. package/dist-worker/tool-disclosure.js +90 -0
  46. package/dist-worker/tools.js +16 -0
  47. package/docs/ARCHITECTURE.md +32 -11
  48. package/docs/CHANGELOG.md +60 -0
  49. package/docs/DESIGN-v3.3.0-cost-optimization.md +1053 -0
  50. package/docs/EXPERIMENT-M5-real-ab.md +82 -0
  51. package/docs/RELEASE-v3.2.10.md +32 -0
  52. package/docs/ROADMAP.md +3 -3
  53. package/docs/SECURITY.md +9 -9
  54. package/docs/SUPERAGENT.md +5 -5
  55. package/docs/THIRD-PARTY.md +87 -0
  56. package/docs/WORKER-AGENT-LOOP.md +1 -1
  57. package/package.json +15 -3
  58. package/src/cost/attribution.ts +186 -0
  59. package/src/cost/audit.ts +170 -0
  60. package/src/cost/budget.ts +226 -0
  61. package/src/cost/bus.ts +97 -0
  62. package/src/cost/cert-chain.ts +102 -0
  63. package/src/cost/experiment.ts +136 -0
  64. package/src/cost/gate.ts +191 -0
  65. package/src/cost/host-surface.ts +97 -0
  66. package/src/cost/index.ts +316 -0
  67. package/src/cost/ledger-report.ts +186 -0
  68. package/src/cost/ledger.ts +385 -0
  69. package/src/cost/metrics.ts +193 -0
  70. package/src/cost/milestones.ts +170 -0
  71. package/src/cost/modules/index.ts +13 -0
  72. package/src/cost/modules/manifests.ts +129 -0
  73. package/src/cost/policy.ts +276 -0
  74. package/src/cost/report.ts +158 -0
  75. package/src/cost/reuse-report.ts +131 -0
  76. package/src/cost/rsi.ts +364 -0
  77. package/src/cost/toolface.ts +151 -0
  78. package/src/cost/types.ts +298 -0
  79. package/src/kernel.ts +37 -11
  80. package/src/plugins/cost-optimizer.ts +271 -26
  81. package/src/plugins/superagent-router.ts +83 -2
  82. package/src/plugins/superagent-worker.ts +1 -1
  83. package/src/superagent/router.ts +1 -1
  84. package/src/superagent/types.ts +70 -0
  85. package/worker/agent-loop.ts +444 -7
  86. package/worker/baseline-fit.ts +80 -0
  87. package/worker/budget-guard.ts +156 -0
  88. package/worker/ccs.ts +202 -0
  89. package/worker/cost-hooks.ts +133 -0
  90. package/worker/cost-modules.ts +356 -0
  91. package/worker/cost-spi.ts +283 -0
  92. package/worker/edg.ts +424 -0
  93. package/worker/gdr.ts +233 -0
  94. package/worker/index.ts +84 -3
  95. package/worker/llm-http.ts +18 -0
  96. package/worker/sink-http.ts +5 -2
  97. package/worker/spill.ts +133 -0
  98. package/worker/tool-batch.ts +124 -0
  99. package/worker/tool-compact.ts +317 -0
  100. package/worker/tool-disclosure.ts +131 -0
  101. package/worker/tools.ts +553 -531
  102. package/worker/types.ts +191 -0
@@ -0,0 +1,136 @@
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
+ * M5 实验台:**A/B 对照执行器**(control = ccs off / enforce = ccs enforce)。
7
+ *
8
+ * 设计原则(决定它能不能当证据):
9
+ * * 走**真实 Worker 代码路径**(工具注册表、A1 紧凑、A3 批量、B1/B2/B3 全都在);
10
+ * * 每组 ≥10 任务,**同一 corpus 同一顺序**,control 与 enforce 只差一个开关;
11
+ * * 输出**同表**:tokens/task 与成功率并列,绝不只报实验组;
12
+ * * **自报口径**:`harness:'local-dry-run'` 与 `runner` 字段写进结果 ——
13
+ * 用脚本化 LLM 跑的 dry run 不能冒充「真实 fork 实机」证据(G5 会因此继续被挡住)。
14
+ */
15
+
16
+ export const EXPERIMENT_HARNESS = Object.freeze({
17
+ dryRun: 'local-dry-run',
18
+ /** 真实 provider + 真实 Worker 代码路径(票据可以是合成的,但模型与工具是真的)。 */
19
+ realProvider: 'real-provider-real-worker',
20
+ realFork: 'real-fork',
21
+ })
22
+
23
+ const num = (value) => {
24
+ const n = Number(value)
25
+ return Number.isFinite(n) ? n : 0
26
+ }
27
+
28
+ /**
29
+ * 单组执行:把 corpus 逐个跑完,收集指标。
30
+ *
31
+ * @param {{
32
+ * tasks: Array<{taskId: string, run: (task: object) => Promise<object>}>,
33
+ * group: 'control' | 'enforce',
34
+ * }} input
35
+ */
36
+ export async function runGroup(input = {}) {
37
+ const tasks = Array.isArray(input.tasks) ? input.tasks : []
38
+ const rows = []
39
+ for (const task of tasks) {
40
+ const startedAt = Date.now()
41
+ let result
42
+ try {
43
+ result = await task.run(task)
44
+ } catch (error) {
45
+ result = { ok: false, error: { code: 'harness-error', message: error instanceof Error ? error.message : String(error) } }
46
+ }
47
+ const elapsedMs = Date.now() - startedAt
48
+ const cost = result?.cost ?? {}
49
+ rows.push({
50
+ taskId: String(task.taskId),
51
+ ok: result?.ok === true,
52
+ tokens: num(result?.tokenUsage),
53
+ elapsedMs: num(result?.elapsedMs) || elapsedMs,
54
+ steps: Array.isArray(result?.steps) ? result.steps.length : 0,
55
+ a1: cost.compaction
56
+ ? {
57
+ mode: cost.compaction.mode,
58
+ applied: cost.compaction.applied === true,
59
+ savedChars: num(cost.compaction.savedChars),
60
+ estimatedTokens: num(cost.compaction.estimatedTokens),
61
+ }
62
+ : null,
63
+ a3: cost.batch ? { taught: cost.batch.taught === true, corrections: num(cost.batch.corrections) } : null,
64
+ reuse: cost.ccs
65
+ ? {
66
+ mode: cost.ccs.mode,
67
+ attempts: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.length : 0,
68
+ hits: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.filter((item) => item.hit === true).length : 0,
69
+ wouldSaveTokens: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.reduce((sum, item) => sum + num(item.wouldSaveTokens), 0) : 0,
70
+ skippedModelCalls: num(cost.ccs.skippedModelCalls),
71
+ substitutions: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.length : 0,
72
+ dispatched: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.filter((cert) => cert.dispatched === true).length : 0,
73
+ failures: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.filter((cert) => cert.dispatched === false).length : 0,
74
+ shadowCertificates: Array.isArray(cost.ccs.shadowCertificates) ? cost.ccs.shadowCertificates.length : 0,
75
+ }
76
+ : null,
77
+ })
78
+ }
79
+ return { group: String(input.group ?? 'unknown'), tasks: rows }
80
+ }
81
+
82
+ /** 两组同表对比(成功率差值按 §10.1 的 2pp 门禁)。 */
83
+ export function compareGroups({ control, enforce, harness = EXPERIMENT_HARNESS.dryRun, runner = 'scripted-llm' } = {}) {
84
+ const avg = (rows) => (rows.length > 0 ? rows.reduce((sum, row) => sum + num(row.tokens), 0) / rows.length : null)
85
+ const rate = (rows) => (rows.length > 0 ? rows.filter((row) => row.ok === true).length / rows.length : null)
86
+ const ctl = Array.isArray(control?.tasks) ? control.tasks : []
87
+ const trt = Array.isArray(enforce?.tasks) ? enforce.tasks : []
88
+ const controlTokens = avg(ctl)
89
+ const enforceTokens = avg(trt)
90
+ const controlSuccess = rate(ctl)
91
+ const enforceSuccess = rate(trt)
92
+ const tokenDeltaPct = controlTokens !== null && enforceTokens !== null && controlTokens > 0
93
+ ? (enforceTokens - controlTokens) / controlTokens
94
+ : null
95
+ const successDelta = controlSuccess !== null && enforceSuccess !== null ? enforceSuccess - controlSuccess : null
96
+ const reuse = trt.map((row) => row.reuse).filter(Boolean)
97
+ const a1 = trt.map((row) => row.a1).filter(Boolean)
98
+ return Object.freeze({
99
+ harness,
100
+ runner,
101
+ /** 这条是给 G5 用的:dry-run 永远不足以当证据;真实 provider + 真实 worker 才算测量有效。 */
102
+ qualifiesForGate: harness === EXPERIMENT_HARNESS.realFork || harness === EXPERIMENT_HARNESS.realProvider,
103
+ table: [
104
+ { group: 'control', samples: ctl.length, tokensPerTask: controlTokens, successRate: controlSuccess, elapsedMsMedian: median(ctl.map((row) => num(row.elapsedMs))) },
105
+ { group: 'enforce', samples: trt.length, tokensPerTask: enforceTokens, successRate: enforceSuccess, elapsedMsMedian: median(trt.map((row) => num(row.elapsedMs))) },
106
+ ],
107
+ delta: { tokensPerTask: controlTokens !== null && enforceTokens !== null ? enforceTokens - controlTokens : null, tokensPerTaskPct: tokenDeltaPct, successRate: successDelta },
108
+ a1: {
109
+ tasks: a1.length,
110
+ /** pplied 只反映**最后一次**压缩事件;真正判断贡献要看是否产生了估算节省。 */
111
+ tasksWithSavings: a1.filter((row) => num(row.estimatedTokens) > 0).length,
112
+ applied: a1.filter((row) => row.applied === true).length,
113
+ estimatedTokens: a1.reduce((sum, row) => sum + num(row.estimatedTokens), 0),
114
+ },
115
+ reuse: {
116
+ attempts: reuse.reduce((sum, row) => sum + num(row.attempts), 0),
117
+ hits: reuse.reduce((sum, row) => sum + num(row.hits), 0),
118
+ wouldSaveTokens: reuse.reduce((sum, row) => sum + num(row.wouldSaveTokens), 0),
119
+ skippedModelCalls: reuse.reduce((sum, row) => sum + num(row.skippedModelCalls), 0),
120
+ dispatched: reuse.reduce((sum, row) => sum + num(row.dispatched), 0),
121
+ failures: reuse.reduce((sum, row) => sum + num(row.failures), 0),
122
+ shadowCertificates: reuse.reduce((sum, row) => sum + num(row.shadowCertificates), 0),
123
+ },
124
+ /** 与 gate G5 同形,便于直接喂 `evaluateForkExperiment`。 */
125
+ gateShape: Object.freeze({
126
+ control: { tasks: ctl.map((row) => ({ taskId: row.taskId, tokens: row.tokens, success: row.ok })) },
127
+ enforce: { tasks: trt.map((row) => ({ taskId: row.taskId, tokens: row.tokens, success: row.ok })) },
128
+ }),
129
+ })
130
+ }
131
+
132
+ function median(values) {
133
+ const sorted = [...values].sort((a, b) => a - b)
134
+ if (sorted.length === 0) return null
135
+ return sorted[Math.min(sorted.length - 1, Math.ceil(0.5 * sorted.length) - 1)]
136
+ }
@@ -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
+ }