@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,209 @@
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 · milestones(里程碑与依赖顺序,设计 §14)。
6
+ *
7
+ * 规格原文:
8
+ *
9
+ * | 阶段 | 内容 | 产出 |
10
+ * |---|---|---|
11
+ * | M1 | 系统层:ledger/budget/policy/audit + 4 切点接入(observe) | 记账报表 + 预算闸测试 |
12
+ * | M2 | 模块层 A:A1 紧凑输出、A2 渐进纰漏(shadow→enforce 白名单) | 工具面 token 下降 + 归因 |
13
+ * | M3 | 模块层 B:B1 EDG、B2 GDR(shadow 计算证书,不替换) | 命中率与「本可节省」报表 |
14
+ * | M4 | B3 CCS enforce(白名单)+ 自动回退 | 真实替换 + 证书链 + 成功率对照 |
15
+ * | M5 | A3 批量(若宿主支持)、B4 RSI 离线流程、与瓮城回流/信息栏对接 | 离线候选冻结清单 + 面板 |
16
+ *
17
+ * 依赖顺序:**M1 必须先行**(没有账本后续收益不可信);**M4 必须等 M3 的守卫准确率数据**。
18
+ *
19
+ * 本文件把这张表变成机器可读的里程碑状态:每个阶段分「实现」与「效果证据」两件事判定,
20
+ * 缺数据只能说 `effect-unmeasured`,**不许把"实现了"说成"达标了"**。
21
+ */
22
+ export const MILESTONE_IDS = Object.freeze(['M1', 'M2', 'M3', 'M4', 'M5']);
23
+ /** M4 放行前,M3 需要给出的守卫准确率最小样本量(与 policy.enforce 门禁一致)。 */
24
+ export const M3_MIN_SAMPLES = 20;
25
+ /** M4 放行前要求的最小守卫准确率(与 ENFORCE_GATE.minAccuracy 一致)。 */
26
+ export const M3_MIN_ACCURACY = 0.98;
27
+ /**
28
+ * M5 回流放行条件:至少 3 条事件拿到对端回执,且对端自报身份在白名单内。
29
+ * 自环(同一进程里服务端+客户端)与未知 peer 一律不算达标。
30
+ */
31
+ export const M5_MIN_RELAY_EVENTS = 3;
32
+ export const M5_RELAY_PEERS = Object.freeze(['dsh-fang-bundle', 'contract-server']);
33
+ export const MILESTONES = Object.freeze([
34
+ Object.freeze({
35
+ id: 'M1',
36
+ title: '系统层:ledger/budget/policy/audit + 4 切点接入(observe)',
37
+ deliverable: '记账报表 + 预算闸测试',
38
+ dependsOn: [],
39
+ /** 实现落点(文件存在即视为实现完成;行为由测试保证)。 */
40
+ implementation: ['src/cost/ledger.ts', 'src/cost/budget.ts', 'src/cost/policy.ts', 'src/cost/audit.ts', 'worker/cost-hooks.ts'],
41
+ tests: ['test/cost-ledger.test.mjs', 'test/cost-budget.test.mjs', 'test/cost-policy.test.mjs', 'test/cost-audit.test.mjs', 'test/worker-cost-hooks.test.ts', 'test/worker-budget-guard.test.ts'],
42
+ }),
43
+ Object.freeze({
44
+ id: 'M2',
45
+ title: '模块层 A:A1 紧凑输出、A2 渐进纰漏(shadow→enforce 白名单)',
46
+ deliverable: '工具面 token 下降 + 归因',
47
+ dependsOn: ['M1'],
48
+ implementation: ['worker/tool-compact.ts', 'worker/tool-disclosure.ts', 'src/cost/attribution.ts'],
49
+ tests: ['test/worker-tool-compact.test.ts', 'test/worker-tool-disclosure.test.ts', 'test/cost-attribution.test.mjs', 'test/cost-disclosure.test.mjs'],
50
+ }),
51
+ Object.freeze({
52
+ id: 'M3',
53
+ title: '模块层 B:B1 EDG、B2 GDR(shadow 计算证书,不替换)',
54
+ deliverable: '命中率与「本可节省」报表',
55
+ dependsOn: ['M1'],
56
+ implementation: ['worker/edg.ts', 'worker/gdr.ts'],
57
+ tests: ['test/worker-edg.test.ts', 'test/worker-gdr.test.ts', 'test/worker-decision-spi.test.ts'],
58
+ }),
59
+ Object.freeze({
60
+ id: 'M4',
61
+ title: 'B3 CCS enforce(白名单)+ 自动回退',
62
+ deliverable: '真实替换 + 证书链 + 成功率对照',
63
+ dependsOn: ['M3'],
64
+ implementation: ['worker/ccs.ts'],
65
+ tests: ['test/worker-ccs.test.ts', 'test/cost-substitution.test.mjs', 'test/invariants.test.ts'],
66
+ }),
67
+ Object.freeze({
68
+ id: 'M5',
69
+ title: 'A3 批量(若宿主支持)、B4 RSI 离线流程、与瓮城回流/信息栏对接',
70
+ deliverable: '离线候选冻结清单 + 面板',
71
+ dependsOn: ['M1'],
72
+ implementation: ['worker/tool-batch.ts', 'src/cost/rsi.ts', 'scripts/rsi-offline.mjs', 'client.js'],
73
+ tests: ['test/worker-tool-batch.test.ts', 'test/cost-rsi.test.mjs'],
74
+ }),
75
+ ]);
76
+ /**
77
+ * 评估里程碑。
78
+ *
79
+ * @param {{
80
+ * files?: string[], // 仓库里实际存在的文件(相对路径)
81
+ * guard?: {samples?: number, accuracy?: number}, // M3 的守卫准确率数据(来自 shadow 报告)
82
+ * forkExperiment?: {ok?: boolean}, // M4 的成功率对照(来自 gate 的 G5)
83
+ * fangRelay?: {available?: boolean, peer?: string, events?: number,
84
+ * sameProcess?: boolean, transport?: string, contractVersion?: string,
85
+ * reason?: string}, // M5 的回流对端证据(独立进程实测)
86
+ * }} evidence
87
+ */
88
+ export function evaluateMilestones(evidence = {}) {
89
+ const files = new Set(Array.isArray(evidence.files) ? evidence.files : []);
90
+ const rows = [];
91
+ const done = new Set();
92
+ let relayEvidence = null;
93
+ for (const milestone of MILESTONES) {
94
+ const missingImplementation = milestone.implementation.filter((file) => !files.has(file));
95
+ const missingTests = milestone.tests.filter((file) => !files.has(file));
96
+ const dependenciesMet = milestone.dependsOn.every((id) => done.has(id));
97
+ const implemented = missingImplementation.length === 0 && missingTests.length === 0;
98
+ let effect = 'unmeasured';
99
+ const reasons = [];
100
+ if (!implemented) {
101
+ reasons.push(`missing:${[...missingImplementation, ...missingTests].join('|')}`);
102
+ }
103
+ if (!dependenciesMet) {
104
+ reasons.push(`dependency-unmet:${milestone.dependsOn.filter((id) => !done.has(id)).join('|')}`);
105
+ }
106
+ // 各阶段的效果证据要求
107
+ if (implemented && dependenciesMet) {
108
+ if (milestone.id === 'M1') {
109
+ effect = 'verified'; // 系统层效果由测试直接证明(记账、预算闸)
110
+ }
111
+ else if (milestone.id === 'M2') {
112
+ effect = evidence.attributionObserved === true ? 'verified' : 'unmeasured';
113
+ if (effect === 'unmeasured')
114
+ reasons.push('effect-needs-real-token-drop');
115
+ }
116
+ else if (milestone.id === 'M3') {
117
+ const samples = Math.max(0, Number(evidence?.guard?.samples ?? 0) || 0);
118
+ const accuracy = Number(evidence?.guard?.accuracy ?? 0) || 0;
119
+ if (samples >= M3_MIN_SAMPLES && accuracy >= M3_MIN_ACCURACY)
120
+ effect = 'verified';
121
+ else
122
+ reasons.push(`effect-needs-guard-accuracy(samples=${samples}, accuracy=${accuracy})`);
123
+ }
124
+ else if (milestone.id === 'M4') {
125
+ if (evidence?.forkExperiment?.ok === true)
126
+ effect = 'verified';
127
+ else
128
+ reasons.push('effect-needs-fork-success-comparison');
129
+ }
130
+ else if (milestone.id === 'M5') {
131
+ // 回流必须有**对端回执**才算达标:可用 + 非自环 + 对端自报身份在白名单 + 至少 3 条事件。
132
+ const relay = evidence?.fangRelay && typeof evidence.fangRelay === 'object' ? evidence.fangRelay : null;
133
+ const available = relay?.available === true;
134
+ const peer = String(relay?.peer ?? '');
135
+ const events = Math.max(0, Number(relay?.events ?? 0) || 0);
136
+ if (!available) {
137
+ reasons.push(`fang-relay-unavailable:${String(relay?.reason ?? 'not-provided')}`);
138
+ }
139
+ else if (relay?.sameProcess === true) {
140
+ reasons.push('fang-relay-self-loop');
141
+ }
142
+ else if (!M5_RELAY_PEERS.includes(peer)) {
143
+ reasons.push(`fang-relay-peer-invalid:${peer || 'missing'}`);
144
+ }
145
+ else if (events < M5_MIN_RELAY_EVENTS) {
146
+ reasons.push(`fang-relay-no-receipts(events=${events})`);
147
+ }
148
+ else {
149
+ effect = 'verified';
150
+ }
151
+ relayEvidence = Object.freeze({
152
+ available,
153
+ peer: peer || null,
154
+ events,
155
+ sameProcess: relay?.sameProcess === true,
156
+ transport: relay?.transport ?? null,
157
+ contractVersion: relay?.contractVersion ?? relay?.peerContractVersion ?? null,
158
+ });
159
+ }
160
+ }
161
+ const status = !implemented
162
+ ? 'not-implemented'
163
+ : !dependenciesMet
164
+ ? 'blocked-by-dependency'
165
+ : effect === 'verified' ? 'done' : 'implemented-effect-unmeasured';
166
+ rows.push(Object.freeze({
167
+ id: milestone.id,
168
+ title: milestone.title,
169
+ deliverable: milestone.deliverable,
170
+ dependsOn: milestone.dependsOn,
171
+ implemented,
172
+ dependenciesMet,
173
+ effect,
174
+ status,
175
+ reasons,
176
+ /** M5:回流对端证据(peer 自报 + 事件数 + 是否自环);其它阶段为 null。 */
177
+ relay: relayEvidence,
178
+ /** 剩余缺口说明(不隐藏)。 */
179
+ notes: milestone.id === 'M5' && relayEvidence?.peer === 'contract-server'
180
+ ? ['peer=contract-server:契约对端独立进程实测,dsh-fang-bundle 采用 contract 1.1 后应变为 peer=dsh-fang-bundle']
181
+ : [],
182
+ }));
183
+ if (status === 'done')
184
+ done.add(milestone.id);
185
+ }
186
+ // 依赖顺序的静态校验:M1 必须最先;M4 只依赖 M3。
187
+ const orderProblems = [];
188
+ const m1 = MILESTONES.find((item) => item.id === 'M1');
189
+ if (!m1 || m1.dependsOn.length !== 0)
190
+ orderProblems.push('M1-must-be-first');
191
+ const m4 = MILESTONES.find((item) => item.id === 'M4');
192
+ if (!m4 || !m4.dependsOn.includes('M3'))
193
+ orderProblems.push('M4-must-depend-on-M3');
194
+ const allImplemented = rows.every((row) => row.implemented);
195
+ const allDone = rows.every((row) => row.status === 'done');
196
+ return Object.freeze({
197
+ ok: orderProblems.length === 0 && allDone,
198
+ implementedAll: allImplemented,
199
+ doneAll: allDone,
200
+ orderProblems,
201
+ rows: Object.freeze(rows),
202
+ /** 摘要在面板/报表用:实现了多少、真达标多少。 */
203
+ summary: Object.freeze({
204
+ implemented: rows.filter((row) => row.implemented).length,
205
+ verified: rows.filter((row) => row.status === 'done').length,
206
+ total: rows.length,
207
+ }),
208
+ });
209
+ }
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+ /** 3.3.0 §11:`src/cost/modules/` 入口(模块清单;可执行实现在 worker/,见 manifests.ts 说明)。 */
5
+ export { MODULE_KIND, MODULE_LAYER, MODULE_MODES, MODULE_MANIFESTS, MODULE_IDS, manifestOf, validateManifests, } from "./manifests.js";
@@ -0,0 +1,130 @@
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 §11:模块清单(`src/cost/modules/`)。
6
+ *
7
+ * **进程边界说明(重要)**:A/B 类模块的可执行实现运行在 **fork worker 进程**里,
8
+ * 因此实体代码在 `worker/`(`tool-compact.ts`、`tool-disclosure.ts`、`tool-batch.ts`、
9
+ * `edg.ts`、`gdr.ts`、`ccs.ts`)。本目录提供的是**宿主侧清单**:模块 id、层级、默认档、
10
+ * 开关环境变量、实现文件与文档锚点 —— 供编排器、面板与门禁统一引用,
11
+ * 避免"宿主侧再抄一份实现"导致两套语义。
12
+ */
13
+ export const MODULE_KIND = Object.freeze({ tool: 'tool', decision: 'decision' });
14
+ export const MODULE_LAYER = Object.freeze({ a: 'a', b: 'b' });
15
+ export const MODULE_MODES = Object.freeze(['off', 'shadow', 'enforce']);
16
+ /** 7 个模块(A1–A3 / B1–B4)的宿主侧清单。 */
17
+ export const MODULE_MANIFESTS = Object.freeze([
18
+ Object.freeze({
19
+ id: 'a1-compact',
20
+ kind: 'tool',
21
+ layer: 'a',
22
+ name: '工具输出紧凑化',
23
+ implementation: 'worker/tool-compact.ts',
24
+ defaultMode: 'shadow',
25
+ envFlag: 'DSH_MASTER_COMPACT_MODE',
26
+ surfaces: ['worker'],
27
+ changesObservableSemantics: false,
28
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#a1-tool-compact工具输出紧凑化',
29
+ }),
30
+ Object.freeze({
31
+ id: 'a2-disclosure',
32
+ kind: 'tool',
33
+ layer: 'a',
34
+ name: '渐进式纰漏',
35
+ implementation: 'worker/tool-disclosure.ts',
36
+ defaultMode: 'off',
37
+ envFlag: 'DSH_MASTER_DISCLOSURE',
38
+ surfaces: ['worker'],
39
+ changesObservableSemantics: false,
40
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#a2-tool-disclosure渐进式纰漏',
41
+ }),
42
+ Object.freeze({
43
+ id: 'a3-batch',
44
+ kind: 'tool',
45
+ layer: 'a',
46
+ name: '批量调用引导',
47
+ implementation: 'worker/tool-batch.ts',
48
+ defaultMode: 'shadow',
49
+ envFlag: 'DSH_MASTER_BATCH',
50
+ surfaces: ['worker'],
51
+ changesObservableSemantics: false,
52
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#a3-tool-batch批量调用',
53
+ }),
54
+ Object.freeze({
55
+ id: 'b1-edg',
56
+ kind: 'decision',
57
+ layer: 'b',
58
+ name: '可执行决策图',
59
+ implementation: 'worker/edg.ts',
60
+ defaultMode: 'off',
61
+ envFlag: 'DSH_MASTER_EDG',
62
+ surfaces: ['worker'],
63
+ changesObservableSemantics: false,
64
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#b1-edg可执行决策图',
65
+ }),
66
+ Object.freeze({
67
+ id: 'b2-gdr',
68
+ kind: 'decision',
69
+ layer: 'b',
70
+ name: '回执守卫',
71
+ implementation: 'worker/gdr.ts',
72
+ defaultMode: 'off',
73
+ envFlag: 'DSH_MASTER_GDR',
74
+ surfaces: ['worker'],
75
+ changesObservableSemantics: false,
76
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#b2-gdr回执守卫',
77
+ }),
78
+ Object.freeze({
79
+ id: 'b3-ccs',
80
+ kind: 'decision',
81
+ layer: 'b',
82
+ name: '认证回调替换',
83
+ implementation: 'worker/ccs.ts',
84
+ defaultMode: 'off',
85
+ envFlag: 'DSH_MASTER_CCS',
86
+ surfaces: ['worker'],
87
+ /** 替换会**跳过模型推理**:只在 enforce + 白名单下发生。 */
88
+ changesObservableSemantics: false,
89
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#b3-ccs认证回调替换',
90
+ }),
91
+ Object.freeze({
92
+ id: 'b4-rsi',
93
+ kind: 'decision',
94
+ layer: 'b',
95
+ name: '离线自改进',
96
+ implementation: 'src/cost/rsi.ts',
97
+ defaultMode: 'off',
98
+ envFlag: null,
99
+ surfaces: ['host'],
100
+ changesObservableSemantics: false,
101
+ docs: 'docs/DESIGN-v3.3.0-cost-optimization.md#b4-rsi离线自改进',
102
+ }),
103
+ ]);
104
+ export const MODULE_IDS = Object.freeze(MODULE_MANIFESTS.map((manifest) => manifest.id));
105
+ export function manifestOf(id) {
106
+ return MODULE_MANIFESTS.find((manifest) => manifest.id === String(id)) ?? null;
107
+ }
108
+ /** 校验清单自身:7 个模块、id 唯一、默认档合法。 */
109
+ export function validateManifests(manifests = MODULE_MANIFESTS) {
110
+ const problems = [];
111
+ const ids = new Set();
112
+ for (const manifest of manifests) {
113
+ if (!manifest?.id)
114
+ problems.push('missing-id');
115
+ if (ids.has(manifest?.id))
116
+ problems.push(`duplicate-id:${manifest.id}`);
117
+ ids.add(manifest?.id);
118
+ if (!MODULE_MODES.includes(manifest?.defaultMode))
119
+ problems.push(`bad-default-mode:${manifest?.id}`);
120
+ if (!MODULE_KIND[manifest?.kind])
121
+ problems.push(`bad-kind:${manifest?.id}`);
122
+ if (!MODULE_LAYER[manifest?.layer])
123
+ problems.push(`bad-layer:${manifest?.id}`);
124
+ if (manifest?.changesObservableSemantics !== false)
125
+ problems.push(`observable-semantics:${manifest?.id}`);
126
+ }
127
+ if (manifests.length !== 7)
128
+ problems.push(`module-count:${manifests.length}!=7`);
129
+ return { ok: problems.length === 0, problems };
130
+ }
@@ -0,0 +1,276 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
4
+ /**
5
+ * 3.3.0 系统层 · policy(灰度与回滚,设计 §5.3)。
6
+ *
7
+ * 三档语义:
8
+ * * `observe` —— 只计量、只计算命中,**绝不替换**(3.2.x 现状,默认档);
9
+ * * `shadow` —— 决策层真实计算守卫结果并落证书,但**仍调用慢模型**(可对比「本可节省」);
10
+ * * `enforce` —— 守卫命中才真正替换,且受白名单(forkId / task 标签 / 工具集)限制。
11
+ *
12
+ * 粒度:`global → forkId → taskId`,取**最严格**档(observe 最严格,enforce 最宽松)。
13
+ * 默认 observe;enforce 只能由显式配置或用户指令开启(过门禁,不自动升级)。
14
+ * 任何时刻都可降级(回滚优先)。
15
+ */
16
+ import { ENFORCE_GATE, POLICY_MODES } from "./types.js";
17
+ /** 越靠前越严格(限制越少自动化)。 */
18
+ const STRICTNESS = Object.freeze(['observe', 'shadow', 'enforce']);
19
+ function mostRestrictive(modes) {
20
+ const valid = modes.filter((mode) => POLICY_MODES.includes(mode));
21
+ if (valid.length === 0)
22
+ return 'observe';
23
+ return valid.reduce((strict, current) => (STRICTNESS.indexOf(current) < STRICTNESS.indexOf(strict) ? current : strict));
24
+ }
25
+ function normalizeList(input) {
26
+ const list = Array.isArray(input) ? input : [];
27
+ return Object.freeze([...new Set(list.map((item) => String(item ?? '').trim()).filter(Boolean))]);
28
+ }
29
+ /**
30
+ * 归一化白名单:兼容 3.3.0 早期的扁平数组(等价于「forkId 或标签命中」),
31
+ * 也接受显式四维对象 `{forks, tasks, tags, tools}`。空维度 = 该维度不限制。
32
+ */
33
+ function normalizeAllowlist(input) {
34
+ if (Array.isArray(input)) {
35
+ const flat = normalizeList(input);
36
+ return Object.freeze({ legacy: true, forks: flat, tasks: Object.freeze([]), tags: flat, tools: Object.freeze([]) });
37
+ }
38
+ const source = input && typeof input === 'object' ? input : {};
39
+ return Object.freeze({
40
+ legacy: false,
41
+ forks: normalizeList(source.forks),
42
+ tasks: normalizeList(source.tasks),
43
+ tags: normalizeList(source.tags),
44
+ tools: normalizeList(source.tools),
45
+ });
46
+ }
47
+ /**
48
+ * @param {{
49
+ * mode?: string,
50
+ * scopes?: { global?: string, forks?: Record<string,string>, tasks?: Record<string,string> },
51
+ * allowlist?: string[] | { forks?: string[], tasks?: string[], tags?: string[], tools?: string[] },
52
+ * audit?: { append?: Function },
53
+ * now?: () => number,
54
+ * }} [options]
55
+ */
56
+ export function createPolicy(options = {}) {
57
+ const now = typeof options.now === 'function' ? options.now : () => Date.now();
58
+ const audit = options.audit && typeof options.audit.append === 'function' ? options.audit : null;
59
+ const allowlist = normalizeAllowlist(options.allowlist);
60
+ const requestedMode = String(options.mode ?? 'observe');
61
+ let mode = POLICY_MODES.includes(requestedMode) ? requestedMode : 'observe';
62
+ let lastRefusal = null;
63
+ const scopes = options.scopes && typeof options.scopes === 'object' ? options.scopes : {};
64
+ const forkScopes = scopes.forks && typeof scopes.forks === 'object' ? { ...scopes.forks } : {};
65
+ const taskScopes = scopes.tasks && typeof scopes.tasks === 'object' ? { ...scopes.tasks } : {};
66
+ /** 命中计数(observe 也要算,不然「灰度」无从判断)。 */
67
+ const hits = new Map();
68
+ const CERTIFICATE_LIMIT = 200;
69
+ const certificates = [];
70
+ const record = (type, payload) => {
71
+ try {
72
+ audit?.append({ type, actor: 'cost-policy', payload });
73
+ }
74
+ catch { /* 审计失败不改变策略行为 */ }
75
+ };
76
+ /** 解析当前上下文的生效档:global → fork → task,取最严格。 */
77
+ function resolve(context = {}) {
78
+ const forkId = String(context.forkId ?? '');
79
+ const taskId = String(context.taskId ?? '');
80
+ const layers = [{ scope: 'global', mode }];
81
+ if (forkId && POLICY_MODES.includes(String(forkScopes[forkId]))) {
82
+ layers.push({ scope: 'fork', mode: String(forkScopes[forkId]) });
83
+ }
84
+ if (taskId && POLICY_MODES.includes(String(taskScopes[taskId]))) {
85
+ layers.push({ scope: 'task', mode: String(taskScopes[taskId]) });
86
+ }
87
+ const effective = mostRestrictive(layers.map((layer) => layer.mode));
88
+ return Object.freeze({
89
+ mode: effective,
90
+ decidedBy: layers.find((layer) => layer.mode === effective)?.scope ?? 'global',
91
+ layers,
92
+ });
93
+ }
94
+ /** enforce 是否被白名单放行(forkId / taskId / tags / toolName 四个维度,非空维度必须命中)。 */
95
+ function allowlisted(context = {}) {
96
+ const forkId = String(context.forkId ?? '');
97
+ const taskId = String(context.taskId ?? '');
98
+ const tags = Array.isArray(context.tags) ? context.tags.map((tag) => String(tag)) : [];
99
+ const toolName = String(context.toolName ?? '');
100
+ if (allowlist.legacy) {
101
+ if (allowlist.forks.length === 0)
102
+ return { ok: false, reason: 'allowlist-empty' };
103
+ const matched = (forkId !== '' && allowlist.forks.includes(forkId)) || tags.some((tag) => allowlist.tags.includes(tag));
104
+ return matched ? { ok: true, reason: null } : { ok: false, reason: 'allowlist-miss' };
105
+ }
106
+ if (allowlist.forks.length === 0 && allowlist.tasks.length === 0 && allowlist.tags.length === 0 && allowlist.tools.length === 0) {
107
+ return { ok: false, reason: 'allowlist-empty' };
108
+ }
109
+ if (allowlist.forks.length > 0 && !(forkId !== '' && allowlist.forks.includes(forkId)))
110
+ return { ok: false, reason: 'fork-not-allowlisted' };
111
+ if (allowlist.tasks.length > 0 && !(taskId !== '' && allowlist.tasks.includes(taskId)))
112
+ return { ok: false, reason: 'task-not-allowlisted' };
113
+ if (allowlist.tags.length > 0 && !tags.some((tag) => allowlist.tags.includes(tag)))
114
+ return { ok: false, reason: 'tag-not-allowlisted' };
115
+ if (allowlist.tools.length > 0 && !(toolName !== '' && allowlist.tools.includes(toolName)))
116
+ return { ok: false, reason: 'tool-not-allowlisted' };
117
+ return { ok: true, reason: null };
118
+ }
119
+ /**
120
+ * 决策复用模块在守卫「命中」时调用(B1/B2/B3 都会经过这里)。
121
+ * observe —— 只计数;
122
+ * shadow —— 计数 + 落证书(记录本可节省),**不替换**;
123
+ * enforce —— 计数 + 落证书 + 允许替换(还需白名单命中)。
124
+ */
125
+ function recordHit(input = {}) {
126
+ const context = { forkId: input.forkId, taskId: input.taskId, tags: input.tags, toolName: input.toolName };
127
+ const resolved = resolve(context);
128
+ const key = `${resolved.mode}:${String(input.module ?? 'unattributed')}`;
129
+ const row = hits.get(key) ?? { mode: resolved.mode, module: String(input.module ?? 'unattributed'), hits: 0, wouldSaveTokens: 0, substituted: 0 };
130
+ row.hits += 1;
131
+ row.wouldSaveTokens += Math.max(0, Number(input.wouldSaveTokens ?? 0) || 0);
132
+ hits.set(key, row);
133
+ let certificate = null;
134
+ if (resolved.mode === 'observe') {
135
+ return Object.freeze({ ok: true, mode: 'observe', substituted: false, counted: true, certificate: null, resolved });
136
+ }
137
+ const allowed = resolved.mode === 'enforce' ? allowlisted(context) : { ok: false, reason: 'shadow-does-not-substitute' };
138
+ const substituted = resolved.mode === 'enforce' && allowed.ok;
139
+ if (substituted)
140
+ row.substituted += 1;
141
+ certificate = Object.freeze({
142
+ at: now(),
143
+ module: String(input.module ?? 'unattributed'),
144
+ guardId: input.guardId === undefined ? null : String(input.guardId),
145
+ edgeId: input.edgeId === undefined ? null : String(input.edgeId),
146
+ receiptHash: input.receiptHash === undefined ? null : String(input.receiptHash),
147
+ mode: resolved.mode,
148
+ substituted,
149
+ wouldSaveTokens: Math.max(0, Number(input.wouldSaveTokens ?? 0) || 0),
150
+ allowlist: allowed.ok ? 'matched' : allowed.reason,
151
+ });
152
+ certificates.push(certificate);
153
+ if (certificates.length > CERTIFICATE_LIMIT)
154
+ certificates.shift();
155
+ record('policy.hit', { ...certificate });
156
+ return Object.freeze({
157
+ ok: true,
158
+ mode: resolved.mode,
159
+ substituted,
160
+ counted: true,
161
+ certificate,
162
+ resolved,
163
+ allowlist: allowed,
164
+ });
165
+ }
166
+ function describe() {
167
+ return Object.freeze({
168
+ mode,
169
+ requestedMode,
170
+ scopes: Object.freeze({ global: mode, forks: Object.freeze({ ...forkScopes }), tasks: Object.freeze({ ...taskScopes }) }),
171
+ allowlist: Object.freeze({
172
+ legacy: allowlist.legacy,
173
+ forks: allowlist.forks.slice(),
174
+ tasks: allowlist.tasks.slice(),
175
+ tags: allowlist.tags.slice(),
176
+ tools: allowlist.tools.slice(),
177
+ }),
178
+ gate: { minAccuracy: ENFORCE_GATE.minAccuracy, minSamples: ENFORCE_GATE.minSamples, allowlistRequired: true },
179
+ lastRefusal,
180
+ });
181
+ }
182
+ function canEnforce(context = {}) {
183
+ if (resolve(context).mode !== 'enforce')
184
+ return false;
185
+ return allowlisted(context).ok;
186
+ }
187
+ function requestMode(next, evidence = {}) {
188
+ const target = String(next);
189
+ if (!POLICY_MODES.includes(target)) {
190
+ return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` };
191
+ }
192
+ if (target === mode)
193
+ return { ok: true, mode, unchanged: true, at: now() };
194
+ const from = mode;
195
+ if (target === 'enforce') {
196
+ const reasons = [];
197
+ if (from === 'observe' && evidence.force !== true)
198
+ reasons.push('shadow-required');
199
+ const accuracy = Number(evidence.accuracy);
200
+ if (!Number.isFinite(accuracy) || accuracy < ENFORCE_GATE.minAccuracy)
201
+ reasons.push('accuracy-below-threshold');
202
+ const samples = Number(evidence.samples);
203
+ if (!Number.isFinite(samples) || samples < ENFORCE_GATE.minSamples)
204
+ reasons.push('samples-below-threshold');
205
+ const destructiveHits = Number(evidence.destructiveHits ?? 0);
206
+ if (!Number.isFinite(destructiveHits) || destructiveHits > 0)
207
+ reasons.push('destructive-hits');
208
+ const allowlistEmpty = allowlist.legacy
209
+ ? allowlist.forks.length === 0
210
+ : allowlist.forks.length === 0 && allowlist.tasks.length === 0 && allowlist.tags.length === 0 && allowlist.tools.length === 0;
211
+ if (allowlistEmpty)
212
+ reasons.push('allowlist-empty');
213
+ if (reasons.length > 0) {
214
+ lastRefusal = Object.freeze({ at: now(), next: target, reasons });
215
+ record('policy.refused', { from, to: target, reasons });
216
+ return { ok: false, code: 'enforce-gate-failed', reasons, mode, at: lastRefusal.at };
217
+ }
218
+ }
219
+ mode = target;
220
+ lastRefusal = null;
221
+ record('policy.mode', { from, to: target, evidence: { ...evidence } });
222
+ return { ok: true, mode, from, at: now() };
223
+ }
224
+ function setMode(next) {
225
+ const target = String(next);
226
+ if (!POLICY_MODES.includes(target)) {
227
+ return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` };
228
+ }
229
+ if (target === mode)
230
+ return { ok: true, mode, unchanged: true };
231
+ const from = mode;
232
+ mode = target;
233
+ record('policy.rollback', { from, to: target });
234
+ return { ok: true, mode, from };
235
+ }
236
+ /** 设置某个 fork / task 的档位(只能更严格或经 enforce 门禁前先下沉)。 */
237
+ function setScope(scope, id, next) {
238
+ const target = String(next);
239
+ if (!POLICY_MODES.includes(target)) {
240
+ return { ok: false, code: 'mode-invalid', message: `mode must be one of ${POLICY_MODES.join('/')}` };
241
+ }
242
+ const key = String(id ?? '').trim();
243
+ if (!key)
244
+ return { ok: false, code: 'scope-id-invalid', message: 'scope id must be a non-empty string' };
245
+ if (scope === 'fork')
246
+ forkScopes[key] = target;
247
+ else if (scope === 'task')
248
+ taskScopes[key] = target;
249
+ else
250
+ return { ok: false, code: 'scope-invalid', message: 'scope must be fork or task' };
251
+ record('policy.scope', { scope, id: key, to: target });
252
+ return { ok: true, scope, id: key, mode: target };
253
+ }
254
+ function stats() {
255
+ return Object.freeze({
256
+ ok: true,
257
+ rows: [...hits.values()].map((row) => Object.freeze({ ...row })),
258
+ totalHits: [...hits.values()].reduce((sum, row) => sum + row.hits, 0),
259
+ totalWouldSaveTokens: [...hits.values()].reduce((sum, row) => sum + row.wouldSaveTokens, 0),
260
+ totalSubstituted: [...hits.values()].reduce((sum, row) => sum + row.substituted, 0),
261
+ certificates: certificates.slice(-50),
262
+ });
263
+ }
264
+ return Object.freeze({
265
+ describe,
266
+ resolve,
267
+ allowlisted,
268
+ recordHit,
269
+ stats,
270
+ canEnforce,
271
+ requestMode,
272
+ setMode,
273
+ setScope,
274
+ isEnforce: (context = {}) => resolve(context).mode === 'enforce',
275
+ });
276
+ }