@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,95 @@
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 系统层 · bus(事件总线与回流出口,设计 §5.6)。
6
+ *
7
+ * 对外只暴露三种内部事件(冒号命名,与 Router 事件通道一致):
8
+ * * `cost:event` —— 账本/归因/预算等一般成本事件(payload.kind 区分);
9
+ * * `cost:degraded` —— 模块降级(不伪装成节省);
10
+ * * `cost:substitution` —— 替换证书(与审计链同源)。
11
+ *
12
+ * 可选对外出口(3.3.0 不阻塞主线):
13
+ * * `web` —— Worker→Router 既有事件通道(Router 落库后供状态页/分身信息栏读取);
14
+ * * `fang` —— 瓮城「产物回流」;未就绪时状态里必须显式 `available: false` + 原因,
15
+ * **不得**静默丢弃或假装成功。
16
+ */
17
+ export const COST_EVENT_TYPES = Object.freeze(['cost:event', 'cost:degraded', 'cost:substitution']);
18
+ /**
19
+ * @param {{maxEvents?: number, now?: () => number, relays?: Array<{name:string, publish?:Function, available?:boolean, reason?:string}>}} [options]
20
+ */
21
+ export function createCostBus(options = {}) {
22
+ const now = typeof options.now === 'function' ? options.now : () => Date.now();
23
+ const maxEvents = Math.max(1, Number(options.maxEvents ?? 200) || 200);
24
+ const events = [];
25
+ const subscribers = new Set();
26
+ /** 外部出口(web / fang);available=false 的出口只记录状态,不投递。 */
27
+ const relays = (Array.isArray(options.relays) ? options.relays : [])
28
+ .filter((relay) => relay && typeof relay === 'object' && relay.name)
29
+ .map((relay) => ({
30
+ name: String(relay.name),
31
+ publish: typeof relay.publish === 'function' ? relay.publish : null,
32
+ available: relay.available === true && typeof relay.publish === 'function',
33
+ reason: String(relay.reason ?? ''),
34
+ delivered: 0,
35
+ failed: 0,
36
+ }));
37
+ let dropped = 0;
38
+ function publish(type, payload = {}) {
39
+ const event = Object.freeze({ type: String(type ?? 'unknown'), at: now(), payload: Object.freeze({ ...payload }) });
40
+ events.push(event);
41
+ if (events.length > maxEvents) {
42
+ events.shift();
43
+ dropped += 1;
44
+ }
45
+ for (const listener of [...subscribers]) {
46
+ try {
47
+ listener(event);
48
+ }
49
+ catch { /* 订阅者异常不影响成本链路 */ }
50
+ }
51
+ for (const relay of relays) {
52
+ if (!relay.available || !relay.publish)
53
+ continue;
54
+ try {
55
+ relay.publish(event);
56
+ relay.delivered += 1;
57
+ }
58
+ catch {
59
+ relay.failed += 1;
60
+ // 出口失败不得影响主流程,但必须留痕(failed 计数),不静默吞掉。
61
+ }
62
+ }
63
+ return event;
64
+ }
65
+ function subscribe(listener) {
66
+ if (typeof listener !== 'function')
67
+ return () => { };
68
+ subscribers.add(listener);
69
+ return () => { subscribers.delete(listener); };
70
+ }
71
+ function snapshot(limit = 50) {
72
+ const n = Math.max(1, Number(limit) || 50);
73
+ return Object.freeze({
74
+ ok: true,
75
+ count: events.length,
76
+ dropped,
77
+ subscribers: subscribers.size,
78
+ events: events.slice(-n),
79
+ /** §5.6 出口状态:不可用必须显式说明原因,不假装成功。 */
80
+ relays: relays.map((relay) => Object.freeze({
81
+ name: relay.name,
82
+ available: relay.available,
83
+ reason: relay.reason,
84
+ delivered: relay.delivered,
85
+ failed: relay.failed,
86
+ })),
87
+ });
88
+ }
89
+ function reset() {
90
+ events.length = 0;
91
+ dropped = 0;
92
+ return { ok: true };
93
+ }
94
+ return Object.freeze({ publish, subscribe, snapshot, reset });
95
+ }
@@ -0,0 +1,100 @@
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 · M4 交付物:**证书链核对报告**(真实替换 → 证书 → 审计链 → 账本)。
6
+ *
7
+ * 一张证书要说清楚三件事,缺一件就不算"可回放":
8
+ *
9
+ * 1. **派发证据**:`viaNativeDispatcher === true` 且 `dispatched === true`(真的走了原生派发器);
10
+ * 2. **审计落链**:能在审计链里按 `taskId + step + edgeId` 找到对应记录,且 `verify()` 不断链;
11
+ * 3. **账本对账**:同一 `taskId + step` 的替换账本事件金额与证书金额一致。
12
+ *
13
+ * 任何一项不成立 ⇒ 该证书 `ok:false` 并给出 `issue`,报表整体 `ok:false`。
14
+ * 失败替换(`dispatched:false`)单列:它们是**自动回退的证据**,不是节省。
15
+ */
16
+ const num = (value) => {
17
+ const n = Number(value);
18
+ return Number.isFinite(n) && n >= 0 ? n : 0;
19
+ };
20
+ /**
21
+ * @param {{
22
+ * certificates?: Array<object>,
23
+ * entries?: Array<object>,
24
+ * audit?: {ok?: boolean, count?: number, records?: Array<object>},
25
+ * }} input
26
+ */
27
+ export function buildCertChainReport(input = {}) {
28
+ const certificates = Array.isArray(input.certificates) ? input.certificates : [];
29
+ const entries = Array.isArray(input.entries) ? input.entries : [];
30
+ const auditOk = input?.audit?.ok !== false;
31
+ const auditRecords = Array.isArray(input?.audit?.records) ? input.audit.records : [];
32
+ // 替换账本事件:按 taskId+step 索引(同一步可能有多条,取合计)
33
+ const ledgerByKey = new Map();
34
+ for (const entry of entries) {
35
+ if (entry?.meta?.kind !== 'substitution')
36
+ continue;
37
+ const key = `${String(entry.taskId)}|${Number(entry.step)}`;
38
+ ledgerByKey.set(key, (ledgerByKey.get(key) ?? 0) + num(entry.savedTokens));
39
+ }
40
+ const rows = certificates.map((cert) => {
41
+ const taskId = String(cert?.taskId ?? '');
42
+ const step = Number(cert?.step ?? -1);
43
+ const key = `${taskId}|${step}`;
44
+ // 审计链只接受已派发的证书;因此 dispatched 缺省视为 true,显式 false 才是失败替换。
45
+ const dispatched = cert?.dispatched !== false;
46
+ const native = cert?.viaNativeDispatcher === true;
47
+ const ledgerSaved = ledgerByKey.get(key) ?? null;
48
+ const certSaved = num(cert?.savedTokens);
49
+ const audited = auditRecords.some((record) => String(record?.payload?.edgeId ?? '') === String(cert?.edgeId ?? '')
50
+ && String(record?.payload?.taskId ?? '') === taskId
51
+ && Number(record?.payload?.step ?? -1) === step);
52
+ const issues = [];
53
+ if (!native)
54
+ issues.push('not-native-dispatcher');
55
+ if (dispatched && ledgerSaved === null)
56
+ issues.push('missing-ledger-entry');
57
+ if (dispatched && ledgerSaved !== null && ledgerSaved !== certSaved)
58
+ issues.push('ledger-mismatch');
59
+ if (dispatched && !audited && auditRecords.length > 0)
60
+ issues.push('missing-audit-record');
61
+ if (dispatched && !auditOk)
62
+ issues.push('audit-chain-broken');
63
+ return Object.freeze({
64
+ edgeId: String(cert?.edgeId ?? ''),
65
+ toolName: String(cert?.toolName ?? ''),
66
+ replacedCallbackId: String(cert?.replacedCallbackId ?? ''),
67
+ taskId,
68
+ step,
69
+ dispatched,
70
+ viaNativeDispatcher: native,
71
+ certificateSavedTokens: certSaved,
72
+ ledgerSavedTokens: ledgerSaved,
73
+ audited,
74
+ issue: issues.length > 0 ? issues.join(',') : null,
75
+ /** 失败替换是「自动回退」的证据,不是缺陷也不是节省。 */
76
+ fallback: !dispatched,
77
+ ok: issues.length === 0,
78
+ });
79
+ });
80
+ const dispatchedRows = rows.filter((row) => row.dispatched);
81
+ const fallbackRows = rows.filter((row) => row.fallback);
82
+ return Object.freeze({
83
+ ok: rows.every((row) => row.ok),
84
+ certificates: rows.length,
85
+ dispatched: dispatchedRows.length,
86
+ fallbacks: fallbackRows.length,
87
+ /** 证书声明的节省总额(只算派发成功的)。 */
88
+ savedTokens: dispatchedRows.reduce((sum, row) => sum + num(row.certificateSavedTokens), 0),
89
+ audit: Object.freeze({ ok: auditOk, records: Number(input?.audit?.count ?? auditRecords.length) }),
90
+ /** 自动回退证据:每条失败替换的原因。 */
91
+ fallbackReasons: Object.freeze(fallbackRows.map((row) => Object.freeze({ edgeId: row.edgeId, reason: String(row.issue ?? 'not-dispatched') }))),
92
+ rows: Object.freeze(rows),
93
+ discipline: Object.freeze({
94
+ /** 失败替换不计节省、不算替换成功。 */
95
+ fallbacksNeverCountAsSavings: fallbackRows.every((row) => num(row.certificateSavedTokens) === 0 || row.dispatched === false),
96
+ everyDispatchHasLedgerEntry: dispatchedRows.every((row) => row.ledgerSavedTokens !== null),
97
+ nativeDispatcherOnly: rows.every((row) => row.viaNativeDispatcher === true),
98
+ }),
99
+ });
100
+ }
@@ -0,0 +1,132 @@
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
+ * M5 实验台:**A/B 对照执行器**(control = ccs off / enforce = ccs enforce)。
6
+ *
7
+ * 设计原则(决定它能不能当证据):
8
+ * * 走**真实 Worker 代码路径**(工具注册表、A1 紧凑、A3 批量、B1/B2/B3 全都在);
9
+ * * 每组 ≥10 任务,**同一 corpus 同一顺序**,control 与 enforce 只差一个开关;
10
+ * * 输出**同表**:tokens/task 与成功率并列,绝不只报实验组;
11
+ * * **自报口径**:`harness:'local-dry-run'` 与 `runner` 字段写进结果 ——
12
+ * 用脚本化 LLM 跑的 dry run 不能冒充「真实 fork 实机」证据(G5 会因此继续被挡住)。
13
+ */
14
+ export const EXPERIMENT_HARNESS = Object.freeze({
15
+ dryRun: 'local-dry-run',
16
+ /** 真实 provider + 真实 Worker 代码路径(票据可以是合成的,但模型与工具是真的)。 */
17
+ realProvider: 'real-provider-real-worker',
18
+ realFork: 'real-fork',
19
+ });
20
+ const num = (value) => {
21
+ const n = Number(value);
22
+ return Number.isFinite(n) ? n : 0;
23
+ };
24
+ /**
25
+ * 单组执行:把 corpus 逐个跑完,收集指标。
26
+ *
27
+ * @param {{
28
+ * tasks: Array<{taskId: string, run: (task: object) => Promise<object>}>,
29
+ * group: 'control' | 'enforce',
30
+ * }} input
31
+ */
32
+ export async function runGroup(input = {}) {
33
+ const tasks = Array.isArray(input.tasks) ? input.tasks : [];
34
+ const rows = [];
35
+ for (const task of tasks) {
36
+ const startedAt = Date.now();
37
+ let result;
38
+ try {
39
+ result = await task.run(task);
40
+ }
41
+ catch (error) {
42
+ result = { ok: false, error: { code: 'harness-error', message: error instanceof Error ? error.message : String(error) } };
43
+ }
44
+ const elapsedMs = Date.now() - startedAt;
45
+ const cost = result?.cost ?? {};
46
+ rows.push({
47
+ taskId: String(task.taskId),
48
+ ok: result?.ok === true,
49
+ tokens: num(result?.tokenUsage),
50
+ elapsedMs: num(result?.elapsedMs) || elapsedMs,
51
+ steps: Array.isArray(result?.steps) ? result.steps.length : 0,
52
+ a1: cost.compaction
53
+ ? {
54
+ mode: cost.compaction.mode,
55
+ applied: cost.compaction.applied === true,
56
+ savedChars: num(cost.compaction.savedChars),
57
+ estimatedTokens: num(cost.compaction.estimatedTokens),
58
+ }
59
+ : null,
60
+ a3: cost.batch ? { taught: cost.batch.taught === true, corrections: num(cost.batch.corrections) } : null,
61
+ reuse: cost.ccs
62
+ ? {
63
+ mode: cost.ccs.mode,
64
+ attempts: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.length : 0,
65
+ hits: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.filter((item) => item.hit === true).length : 0,
66
+ wouldSaveTokens: Array.isArray(cost.ccs.attempts) ? cost.ccs.attempts.reduce((sum, item) => sum + num(item.wouldSaveTokens), 0) : 0,
67
+ skippedModelCalls: num(cost.ccs.skippedModelCalls),
68
+ substitutions: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.length : 0,
69
+ dispatched: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.filter((cert) => cert.dispatched === true).length : 0,
70
+ failures: Array.isArray(cost.ccs.substitutions) ? cost.ccs.substitutions.filter((cert) => cert.dispatched === false).length : 0,
71
+ shadowCertificates: Array.isArray(cost.ccs.shadowCertificates) ? cost.ccs.shadowCertificates.length : 0,
72
+ }
73
+ : null,
74
+ });
75
+ }
76
+ return { group: String(input.group ?? 'unknown'), tasks: rows };
77
+ }
78
+ /** 两组同表对比(成功率差值按 §10.1 的 2pp 门禁)。 */
79
+ export function compareGroups({ control, enforce, harness = EXPERIMENT_HARNESS.dryRun, runner = 'scripted-llm' } = {}) {
80
+ const avg = (rows) => (rows.length > 0 ? rows.reduce((sum, row) => sum + num(row.tokens), 0) / rows.length : null);
81
+ const rate = (rows) => (rows.length > 0 ? rows.filter((row) => row.ok === true).length / rows.length : null);
82
+ const ctl = Array.isArray(control?.tasks) ? control.tasks : [];
83
+ const trt = Array.isArray(enforce?.tasks) ? enforce.tasks : [];
84
+ const controlTokens = avg(ctl);
85
+ const enforceTokens = avg(trt);
86
+ const controlSuccess = rate(ctl);
87
+ const enforceSuccess = rate(trt);
88
+ const tokenDeltaPct = controlTokens !== null && enforceTokens !== null && controlTokens > 0
89
+ ? (enforceTokens - controlTokens) / controlTokens
90
+ : null;
91
+ const successDelta = controlSuccess !== null && enforceSuccess !== null ? enforceSuccess - controlSuccess : null;
92
+ const reuse = trt.map((row) => row.reuse).filter(Boolean);
93
+ const a1 = trt.map((row) => row.a1).filter(Boolean);
94
+ return Object.freeze({
95
+ harness,
96
+ runner,
97
+ /** 这条是给 G5 用的:dry-run 永远不足以当证据;真实 provider + 真实 worker 才算测量有效。 */
98
+ qualifiesForGate: harness === EXPERIMENT_HARNESS.realFork || harness === EXPERIMENT_HARNESS.realProvider,
99
+ table: [
100
+ { group: 'control', samples: ctl.length, tokensPerTask: controlTokens, successRate: controlSuccess, elapsedMsMedian: median(ctl.map((row) => num(row.elapsedMs))) },
101
+ { group: 'enforce', samples: trt.length, tokensPerTask: enforceTokens, successRate: enforceSuccess, elapsedMsMedian: median(trt.map((row) => num(row.elapsedMs))) },
102
+ ],
103
+ delta: { tokensPerTask: controlTokens !== null && enforceTokens !== null ? enforceTokens - controlTokens : null, tokensPerTaskPct: tokenDeltaPct, successRate: successDelta },
104
+ a1: {
105
+ tasks: a1.length,
106
+ /** pplied 只反映**最后一次**压缩事件;真正判断贡献要看是否产生了估算节省。 */
107
+ tasksWithSavings: a1.filter((row) => num(row.estimatedTokens) > 0).length,
108
+ applied: a1.filter((row) => row.applied === true).length,
109
+ estimatedTokens: a1.reduce((sum, row) => sum + num(row.estimatedTokens), 0),
110
+ },
111
+ reuse: {
112
+ attempts: reuse.reduce((sum, row) => sum + num(row.attempts), 0),
113
+ hits: reuse.reduce((sum, row) => sum + num(row.hits), 0),
114
+ wouldSaveTokens: reuse.reduce((sum, row) => sum + num(row.wouldSaveTokens), 0),
115
+ skippedModelCalls: reuse.reduce((sum, row) => sum + num(row.skippedModelCalls), 0),
116
+ dispatched: reuse.reduce((sum, row) => sum + num(row.dispatched), 0),
117
+ failures: reuse.reduce((sum, row) => sum + num(row.failures), 0),
118
+ shadowCertificates: reuse.reduce((sum, row) => sum + num(row.shadowCertificates), 0),
119
+ },
120
+ /** 与 gate G5 同形,便于直接喂 `evaluateForkExperiment`。 */
121
+ gateShape: Object.freeze({
122
+ control: { tasks: ctl.map((row) => ({ taskId: row.taskId, tokens: row.tokens, success: row.ok })) },
123
+ enforce: { tasks: trt.map((row) => ({ taskId: row.taskId, tokens: row.tokens, success: row.ok })) },
124
+ }),
125
+ });
126
+ }
127
+ function median(values) {
128
+ const sorted = [...values].sort((a, b) => a - b);
129
+ if (sorted.length === 0)
130
+ return null;
131
+ return sorted[Math.min(sorted.length - 1, Math.ceil(0.5 * sorted.length) - 1)];
132
+ }
@@ -0,0 +1,189 @@
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 · gate(发布门禁,设计 §10.2)。
6
+ *
7
+ * **3.3.0 不通过不发布**。六道门:
8
+ *
9
+ * | # | 门禁 | 判定 |
10
+ * |---|---|---|
11
+ * | G1 | `npm run check` 0 error | 退出码 |
12
+ * | G2 | `npm run build` 0 error | 退出码 |
13
+ * | G3 | `npm test` 全绿(含新增模块测试) | 退出码 + 用例数 |
14
+ * | G4 | 新增测试至少覆盖:I1–I8 各一条、守卫命中/未命中、schema 漂移、证书回放、归因不重复计 | 覆盖清单逐项核验 |
15
+ * | G5 | 真实 fork 实机:≥10 任务的对照组 vs enforce 组,tokens/task 与成功率同表 | 样本量 + 同表 + 成功率不退化 |
16
+ * | G6 | 零残留:临时 profile / 工作区清理证据 | 路径扫描 |
17
+ * | G7 | 发布需用户明确授权 | 默认 **未授权**(设计不含发布动作) |
18
+ *
19
+ * 门禁的价值在于**敢说不**:任一门不满足即 `ok:false` 并列出 `blockers`,
20
+ * 不允许"部分通过"。
21
+ */
22
+ /** 必须被测试覆盖的能力(G4),每项给出识别标记。 */
23
+ export const REQUIRED_COVERAGE = Object.freeze([
24
+ { id: 'I1', label: '预算不足不启动模型调用', patterns: [/I1:.*budget/u] },
25
+ { id: 'I2', label: 'CCS 只经原生派发器 / 无派发器拒绝', patterns: [/I2:.*native dispatcher/u, /I2:.*non-native/u] },
26
+ { id: 'I3', label: '破坏性工具默认不入池', patterns: [/I3:.*destructive/u] },
27
+ { id: 'I4', label: 'schema 漂移整图作废', patterns: [/I4:.*schema drift/u] },
28
+ { id: 'I5', label: '守卫不匹配回落慢模型', patterns: [/I5:.*guard miss/u] },
29
+ { id: 'I6', label: '模块异常 degraded 且任务继续', patterns: [/I6:.*throwing module/u] },
30
+ { id: 'I7', label: '压缩回执可回读原文(spill)', patterns: [/I7:.*read back/u] },
31
+ { id: 'I8', label: '取消信号阻断替换', patterns: [/I8:.*aborted signal/u] },
32
+ { id: 'guard-hit', label: '守卫命中', patterns: [/all five gates must pass/u] },
33
+ { id: 'guard-miss', label: '守卫未命中', patterns: [/each gate has its own miss reason/u] },
34
+ { id: 'schema-drift', label: 'schema 漂移(B2 层)', patterns: [/schema drift archives the whole graph/u, /schemaHash/u] },
35
+ { id: 'cert-replay', label: '证书回放', patterns: [/100% replayable/u] },
36
+ { id: 'no-double-count', label: '归因不重复计', patterns: [/doubleCountGuard/u, /mutually exclusive/u] },
37
+ ]);
38
+ /** G5:对照组 vs enforce 组的最小样本量。 */
39
+ export const MIN_FORK_TASKS = 10;
40
+ /** G5:成功率允许的最大下降(与 §10.1 的 2pp 门禁一致)。 */
41
+ export const MAX_SUCCESS_DROP_PP = 0.02;
42
+ const num = (value) => {
43
+ const n = Number(value);
44
+ return Number.isFinite(n) ? n : 0;
45
+ };
46
+ /**
47
+ * G4:扫描测试源码,逐项确认覆盖(找不到标记即报缺)。
48
+ * @param {Record<string, string>} sources 文件名 → 源码
49
+ */
50
+ export function checkCoverage(sources = {}) {
51
+ const haystack = Object.values(sources).join('\n');
52
+ const missing = [];
53
+ const covered = [];
54
+ for (const item of REQUIRED_COVERAGE) {
55
+ const found = item.patterns.some((pattern) => pattern.test(haystack));
56
+ if (found)
57
+ covered.push(item.id);
58
+ else
59
+ missing.push({ id: item.id, label: item.label });
60
+ }
61
+ return { ok: missing.length === 0, covered, missing };
62
+ }
63
+ /**
64
+ * G5:评估真实 fork 实机对照实验。
65
+ * @param {{control?: {tasks: Array<{taskId: string, tokens: number, success: boolean}>}, enforce?: {tasks: Array<{taskId: string, tokens: number, success: boolean}>}}} input
66
+ */
67
+ export function evaluateForkExperiment(input = {}) {
68
+ const control = Array.isArray(input?.control?.tasks) ? input.control.tasks : [];
69
+ const enforce = Array.isArray(input?.enforce?.tasks) ? input.enforce.tasks : [];
70
+ const problems = [];
71
+ // 证据来源必须真实:dry-run(合成语料 + 脚本化模型)不能冒充 G5。
72
+ const harness = input?.harness === undefined ? 'real-fork' : String(input.harness);
73
+ if (harness === 'local-dry-run')
74
+ problems.push('harness-not-real');
75
+ if (control.length < MIN_FORK_TASKS)
76
+ problems.push(`control-tasks<${MIN_FORK_TASKS}`);
77
+ if (enforce.length < MIN_FORK_TASKS)
78
+ problems.push(`enforce-tasks<${MIN_FORK_TASKS}`);
79
+ const avg = (rows) => (rows.length > 0 ? rows.reduce((sum, row) => sum + num(row?.tokens), 0) / rows.length : null);
80
+ const rate = (rows) => (rows.length > 0 ? rows.filter((row) => row?.success === true).length / rows.length : null);
81
+ const controlTokensPerTask = avg(control);
82
+ const enforceTokensPerTask = avg(enforce);
83
+ const controlSuccess = rate(control);
84
+ const enforceSuccess = rate(enforce);
85
+ const tokenDelta = controlTokensPerTask !== null && enforceTokensPerTask !== null
86
+ ? Math.round((enforceTokensPerTask - controlTokensPerTask) * 100) / 100
87
+ : null;
88
+ const successDelta = controlSuccess !== null && enforceSuccess !== null
89
+ ? Math.round((enforceSuccess - controlSuccess) * 10000) / 10000
90
+ : null;
91
+ if (successDelta !== null && successDelta <= -MAX_SUCCESS_DROP_PP)
92
+ problems.push('success-regression>=2pp');
93
+ // 同表:两组必须都在同一份报告里给出 tokens/task 与成功率
94
+ const sameTable = controlTokensPerTask !== null && enforceTokensPerTask !== null && controlSuccess !== null && enforceSuccess !== null;
95
+ if (!sameTable)
96
+ problems.push('groups-not-in-same-table');
97
+ return {
98
+ ok: problems.length === 0,
99
+ problems,
100
+ samples: { control: control.length, enforce: enforce.length },
101
+ table: [
102
+ { group: 'control', samples: control.length, tokensPerTask: controlTokensPerTask, successRate: controlSuccess },
103
+ { group: 'enforce', samples: enforce.length, tokensPerTask: enforceTokensPerTask, successRate: enforceSuccess },
104
+ ],
105
+ delta: { tokensPerTask: tokenDelta, successRate: successDelta },
106
+ /** token 变好且成功率不退化,才算"通过且有效"。 */
107
+ improvement: tokenDelta !== null && tokenDelta < 0 && (successDelta === null || successDelta > -MAX_SUCCESS_DROP_PP),
108
+ };
109
+ }
110
+ /**
111
+ * G6:零残留——给出的路径里不允许还有临时 profile / 工作区。
112
+ * @param {Array<{path: string, exists: boolean, kind?: string}>} checks
113
+ */
114
+ export function checkResidue(checks = []) {
115
+ const leftovers = checks.filter((entry) => entry?.exists === true).map((entry) => ({ path: String(entry.path), kind: String(entry.kind ?? 'unknown') }));
116
+ return {
117
+ ok: leftovers.length === 0,
118
+ checked: checks.length,
119
+ leftovers,
120
+ /** 没有检查项等于没有证据,不算通过。 */
121
+ evidence: checks.length > 0 ? '<results>' : null,
122
+ };
123
+ }
124
+ /**
125
+ * 汇总门禁。
126
+ * @param {{
127
+ * commands?: Array<{id: string, exitCode: number, note?: string}>,
128
+ * coverage?: ReturnType<typeof checkCoverage>,
129
+ * forkExperiment?: ReturnType<typeof evaluateForkExperiment>,
130
+ * residue?: ReturnType<typeof checkResidue>,
131
+ * releaseAuthorized?: boolean,
132
+ * }} input
133
+ */
134
+ export function evaluateGate(input = {}) {
135
+ const items = [];
136
+ const blockers = [];
137
+ for (const command of Array.isArray(input.commands) ? input.commands : []) {
138
+ const id = String(command?.id ?? 'unknown');
139
+ const passed = num(command?.exitCode) === 0;
140
+ items.push({ gate: id, ok: passed, detail: `exit=${num(command?.exitCode)}${command?.note ? ` ${command.note}` : ''}` });
141
+ if (!passed)
142
+ blockers.push(`${id}:exit-code`);
143
+ }
144
+ const coverage = input.coverage;
145
+ if (!coverage) {
146
+ blockers.push('G4:coverage-not-checked');
147
+ items.push({ gate: 'G4', ok: false, detail: '未执行覆盖核验' });
148
+ }
149
+ else {
150
+ items.push({ gate: 'G4', ok: coverage.ok === true, detail: coverage.ok ? `${coverage.covered.length} 项全中` : `缺 ${coverage.missing.map((item) => item.id).join(',')}` });
151
+ if (coverage.ok !== true)
152
+ blockers.push(`G4:missing:${coverage.missing.map((item) => item.id).join(',')}`);
153
+ }
154
+ const fork = input.forkExperiment;
155
+ if (!fork) {
156
+ blockers.push('G5:fork-experiment-missing');
157
+ items.push({ gate: 'G5', ok: false, detail: '尚无真实 fork 对照数据(≥10 任务/组)' });
158
+ }
159
+ else {
160
+ 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(',') });
161
+ if (fork.ok !== true)
162
+ blockers.push(`G5:${fork.problems.join(',')}`);
163
+ }
164
+ const residue = input.residue;
165
+ if (!residue) {
166
+ blockers.push('G6:residue-not-checked');
167
+ items.push({ gate: 'G6', ok: false, detail: '未提供清理证据' });
168
+ }
169
+ else {
170
+ items.push({ gate: 'G6', ok: residue.ok === true, detail: residue.ok ? `${residue.checked} 个路径均无残留` : `残留: ${residue.leftovers.map((item) => item.path).join(',')}` });
171
+ if (residue.ok !== true)
172
+ blockers.push('G6:residue');
173
+ }
174
+ // G7:默认未授权。设计本身不含发布动作;授权是人工显式行为。
175
+ const authorized = input.releaseAuthorized === true;
176
+ items.push({ gate: 'G7', ok: authorized, detail: authorized ? '用户已明确授权' : '未授权(发布需用户明确授权)' });
177
+ if (!authorized)
178
+ blockers.push('G7:release-not-authorized');
179
+ const hardGates = ['G1', 'G2', 'G3', 'G4', 'G5', 'G6'];
180
+ const hardOk = items.filter((item) => hardGates.includes(item.gate)).every((item) => item.ok);
181
+ return {
182
+ ok: hardOk && authorized,
183
+ /** 不发布 ≠ 代码不可用:G1–G4 + G6 全绿时,代码质量门禁是过了的。 */
184
+ bootstrapOk: hardOk,
185
+ blockers,
186
+ items,
187
+ releaseAuthorized: authorized,
188
+ };
189
+ }
@@ -0,0 +1,80 @@
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
+ const SOURCES = new Set(['dsh-ops', 'other']);
5
+ function finiteNonNegative(value) {
6
+ const n = Number(value);
7
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
8
+ }
9
+ /**
10
+ * @param {{
11
+ * ledger: {append: Function},
12
+ * enabled?: boolean,
13
+ * source?: string,
14
+ * bus?: ReturnType<typeof createCostBus>,
15
+ * now?: () => number,
16
+ * }} options
17
+ */
18
+ export function createHostSurfaceAdapter(options = {}) {
19
+ const ledger = options.ledger;
20
+ if (!ledger || typeof ledger.append !== 'function')
21
+ throw new Error('host-surface adapter requires a ledger');
22
+ const enabled = options.enabled === true;
23
+ const source = SOURCES.has(String(options.source ?? 'dsh-ops')) ? String(options.source ?? 'dsh-ops') : 'other';
24
+ const bus = options.bus && typeof options.bus.publish === 'function' ? options.bus : null;
25
+ const now = typeof options.now === 'function' ? options.now : () => Date.now();
26
+ let recorded = 0;
27
+ let tokensClaimed = 0;
28
+ /**
29
+ * 登记一次宿主面外部收益(只展示,不计入本项目节省)。
30
+ * @param {{taskId?: string, savedTokens: number, baselineTokens?: number, meta?: object}} input
31
+ */
32
+ function record(input = {}) {
33
+ if (!enabled)
34
+ return { ok: false, code: 'host-adapter-disabled', message: 'host surface adapter is opt-in; enable it in config' };
35
+ const saved = finiteNonNegative(input.savedTokens);
36
+ if (saved === null)
37
+ return { ok: false, code: 'saved-tokens-invalid', message: 'savedTokens must be a non-negative number' };
38
+ const appended = ledger.append({
39
+ account: 'execution',
40
+ surface: 'host',
41
+ external: true,
42
+ module: 'a1-compact',
43
+ source: 'tool',
44
+ phase: 'tool',
45
+ taskId: input.taskId,
46
+ totalTokens: 0,
47
+ savedTokens: saved,
48
+ meta: {
49
+ ...(input.meta && typeof input.meta === 'object' ? input.meta : {}),
50
+ adapter: source,
51
+ external: true,
52
+ note: 'third-party host-surface saving; shown for comparison only',
53
+ },
54
+ });
55
+ if (!appended.ok)
56
+ return { ok: false, code: appended.code, message: appended.message };
57
+ recorded += 1;
58
+ tokensClaimed += saved;
59
+ try {
60
+ bus?.publish?.('cost.host-surface', { adapter: source, savedTokens: saved, taskId: input.taskId ?? null });
61
+ }
62
+ catch { /* ignore */ }
63
+ return { ok: true, adapter: source, external: true, savedTokens: saved, ledger: { id: appended.entry.id }, at: now() };
64
+ }
65
+ function status() {
66
+ return Object.freeze({
67
+ ok: true,
68
+ enabled,
69
+ adapter: source,
70
+ /** 明确声明:本适配器不产生本项目收益。 */
71
+ contributesToOwnSavings: false,
72
+ recorded,
73
+ savedTokens: tokensClaimed,
74
+ note: enabled
75
+ ? '外部宿主面收益只做对照展示,不计入 executionSaved / netSaved'
76
+ : '未启用:本设计不依赖任何第三方宿主插件',
77
+ });
78
+ }
79
+ return Object.freeze({ record, status });
80
+ }