@twinsearth/dsh-master 3.3.0 → 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.
package/src/cost/index.ts CHANGED
@@ -17,6 +17,7 @@ import { createAuditChain } from './audit.ts'
17
17
  import { createAttribution } from './attribution.ts'
18
18
  import { createCostBus } from './bus.ts'
19
19
  import { createHostSurfaceAdapter } from './host-surface.ts'
20
+ import { createFangRelay } from './fang-relay.ts'
20
21
  import { buildSavingsReport, classifyEntry } from './report.ts'
21
22
  import { buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP } from './metrics.ts'
22
23
  import { MODULE_MANIFESTS, MODULE_IDS, validateManifests } from './modules/manifests.ts'
@@ -24,8 +25,9 @@ import { buildLedgerReport, formatLedgerReport, parseLedgerJsonl } from './ledge
24
25
  import { buildToolFaceReport, planToolFace } from './toolface.ts'
25
26
  import { buildReuseReport, formatReuseReport } from './reuse-report.ts'
26
27
 
27
- export { createLedger, createBudget, createPolicy, createAuditChain, createAttribution, createCostBus, createHostSurfaceAdapter, buildSavingsReport, classifyEntry, buildMetricsReport, METRIC_KEYS, SUCCESS_REGRESSION_PP, MODULE_MANIFESTS, MODULE_IDS, validateManifests, buildLedgerReport, formatLedgerReport, parseLedgerJsonl, buildToolFaceReport, planToolFace, buildReuseReport, formatReuseReport }
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 }
28
29
  export * from './types.ts'
30
+ export { RELAY_PEERS, RELAY_DEFAULTS, createFangRelayFromContext, createRelayClient } from './fang-relay.ts'
29
31
 
30
32
  /**
31
33
  * @param {{
@@ -34,6 +36,7 @@ export * from './types.ts'
34
36
  * budgetLimits?: Array<{unit:string, limitTokens?:number, limitCost?:number, onExhaust?:string, degradeTo?:string}>,
35
37
  * degradeModels?: string[],
36
38
  * now?: () => number, maxEntries?: number, maxRecords?: number,
39
+ * relay?: object, relayClient?: object|null, relayOptions?: object, relayAutoAttach?: boolean,
37
40
  * }} [options]
38
41
  */
39
42
  export function createCostSystem(options = {}) {
@@ -50,6 +53,18 @@ export function createCostSystem(options = {}) {
50
53
  })
51
54
  const attribution = createAttribution({ ledger, now })
52
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
+ }
53
68
  /** 宿主会话工具面适配器(可选;默认关闭,不依赖任何第三方插件)。 */
54
69
  const hostSurface = createHostSurfaceAdapter({
55
70
  ledger,
@@ -270,6 +285,21 @@ export function createCostSystem(options = {}) {
270
285
  return bus.subscribe(listener)
271
286
  }
272
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
+
273
303
  function status() {
274
304
  const ledgerSnapshot = ledger.snapshot()
275
305
  const auditVerify = audit.verify()
@@ -293,6 +323,8 @@ export function createCostSystem(options = {}) {
293
323
  unattributedProjectedTotal: attributionSnapshot.unattributedProjectedTotal,
294
324
  },
295
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(),
296
328
  degraded: {
297
329
  modules: [...degradedByModule.values()].map((item) => ({ ...item, phases: { ...item.phases } })),
298
330
  occurrences: [...degradedByModule.values()].reduce((sum, item) => sum + item.occurrences, 0),
@@ -312,5 +344,5 @@ export function createCostSystem(options = {}) {
312
344
  })
313
345
  }
314
346
 
315
- return Object.freeze({ ledger, budget, policy, audit, attribution, bus, hostSurface, recordUsage, authorize, attribute, attributeTotal, substitute, trace, degraded, query, replay, savingsReport, metrics, ledgerReport, subscribe, status })
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 })
316
348
  }
@@ -28,6 +28,13 @@ export const M3_MIN_SAMPLES = 20
28
28
  /** M4 放行前要求的最小守卫准确率(与 ENFORCE_GATE.minAccuracy 一致)。 */
29
29
  export const M3_MIN_ACCURACY = 0.98
30
30
 
31
+ /**
32
+ * M5 回流放行条件:至少 3 条事件拿到对端回执,且对端自报身份在白名单内。
33
+ * 自环(同一进程里服务端+客户端)与未知 peer 一律不算达标。
34
+ */
35
+ export const M5_MIN_RELAY_EVENTS = 3
36
+ export const M5_RELAY_PEERS = Object.freeze(['dsh-fang-bundle', 'contract-server'])
37
+
31
38
  export const MILESTONES = Object.freeze([
32
39
  Object.freeze({
33
40
  id: 'M1',
@@ -79,13 +86,16 @@ export const MILESTONES = Object.freeze([
79
86
  * files?: string[], // 仓库里实际存在的文件(相对路径)
80
87
  * guard?: {samples?: number, accuracy?: number}, // M3 的守卫准确率数据(来自 shadow 报告)
81
88
  * forkExperiment?: {ok?: boolean}, // M4 的成功率对照(来自 gate 的 G5)
82
- * fangRelay?: {available?: boolean}, // 瓮城回流通道(本会话范围内恒为不可用)
89
+ * fangRelay?: {available?: boolean, peer?: string, events?: number,
90
+ * sameProcess?: boolean, transport?: string, contractVersion?: string,
91
+ * reason?: string}, // M5 的回流对端证据(独立进程实测)
83
92
  * }} evidence
84
93
  */
85
94
  export function evaluateMilestones(evidence = {}) {
86
95
  const files = new Set(Array.isArray(evidence.files) ? evidence.files : [])
87
96
  const rows = []
88
97
  const done = new Set()
98
+ let relayEvidence = null
89
99
 
90
100
  for (const milestone of MILESTONES) {
91
101
  const missingImplementation = milestone.implementation.filter((file) => !files.has(file))
@@ -118,10 +128,30 @@ export function evaluateMilestones(evidence = {}) {
118
128
  if (evidence?.forkExperiment?.ok === true) effect = 'verified'
119
129
  else reasons.push('effect-needs-fork-success-comparison')
120
130
  } else if (milestone.id === 'M5') {
121
- // 面板 + 冻结清单可测;瓮城回流在本会话范围内明确不可用(不伪装)
122
- const relayOk = evidence?.fangRelay?.available === true
123
- effect = relayOk ? 'verified' : 'unmeasured'
124
- if (!relayOk) reasons.push('fang-relay-out-of-scope')
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
+ } else if (relay?.sameProcess === true) {
139
+ reasons.push('fang-relay-self-loop')
140
+ } else if (!M5_RELAY_PEERS.includes(peer)) {
141
+ reasons.push(`fang-relay-peer-invalid:${peer || 'missing'}`)
142
+ } else if (events < M5_MIN_RELAY_EVENTS) {
143
+ reasons.push(`fang-relay-no-receipts(events=${events})`)
144
+ } else {
145
+ effect = 'verified'
146
+ }
147
+ relayEvidence = Object.freeze({
148
+ available,
149
+ peer: peer || null,
150
+ events,
151
+ sameProcess: relay?.sameProcess === true,
152
+ transport: relay?.transport ?? null,
153
+ contractVersion: relay?.contractVersion ?? relay?.peerContractVersion ?? null,
154
+ })
125
155
  }
126
156
  }
127
157
 
@@ -141,6 +171,12 @@ export function evaluateMilestones(evidence = {}) {
141
171
  effect,
142
172
  status,
143
173
  reasons,
174
+ /** M5:回流对端证据(peer 自报 + 事件数 + 是否自环);其它阶段为 null。 */
175
+ relay: relayEvidence,
176
+ /** 剩余缺口说明(不隐藏)。 */
177
+ notes: milestone.id === 'M5' && relayEvidence?.peer === 'contract-server'
178
+ ? ['peer=contract-server:契约对端独立进程实测,dsh-fang-bundle 采用 contract 1.1 后应变为 peer=dsh-fang-bundle']
179
+ : [],
144
180
  }))
145
181
  if (status === 'done') done.add(milestone.id)
146
182
  }
package/src/kernel.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import crypto from 'node:crypto'
12
12
 
13
- export const MASTER_VERSION = '3.3.0'
13
+ export const MASTER_VERSION = '3.3.1'
14
14
  export const FANG_CONTRACT_MIN = '3.1.14'
15
15
 
16
16
  export const SERVICE = Object.freeze({
@@ -28,7 +28,7 @@ import {
28
28
  roleAllowed,
29
29
  safeGet,
30
30
  } from '../kernel.ts'
31
- import { createCostSystem, DEFAULT_BUDGET_LIMITS } from '../cost/index.ts'
31
+ import { createCostSystem, createFangRelayFromContext, DEFAULT_BUDGET_LIMITS } from '../cost/index.ts'
32
32
 
33
33
  export const name = 'dsh-master-cost-optimizer'
34
34
  export const inject = ['dshMaster']
@@ -53,6 +53,10 @@ function finiteNonNegative(value) {
53
53
  * enabled?: boolean, mode?: string, targetSavingsRatio?: number,
54
54
  * budget?: Array<{unit: string, limitTokens: number, onExhaust?: string}>,
55
55
  * enforceAllowlist?: string[],
56
+ * relay?: {enabled?: boolean, httpBaseUrl?: string, httpToken?: string, fileDir?: string,
57
+ * timeoutMs?: number, maxBatch?: number, maxQueue?: number, probeTtlMs?: number,
58
+ * autoFlushMs?: number},
59
+ * dispatch?: {motherUrl?: string, motherToken?: string, contractFileDir?: string},
56
60
  * }} [config]
57
61
  */
58
62
  export function apply(ctx, config = {}) {
@@ -70,6 +74,12 @@ export function apply(ctx, config = {}) {
70
74
  }
71
75
 
72
76
  const targetSavingsRatio = finiteNonNegative(config.targetSavingsRatio)
77
+ /**
78
+ * §5.6 / M5:瓮城回流出口。
79
+ * 传输按「插件配置 relay → dispatch 配置 → 环境变量 DSH_FANG_*」装配;
80
+ * 一个都没有时不造客户端,出口状态显式 `relay-client-absent`。
81
+ */
82
+ const relayWiring = createFangRelayFromContext(ctx, config, { logger: ctx?.logger })
73
83
  const system = createCostSystem({
74
84
  mode: config.mode,
75
85
  allowlist: config.enforceAllowlist ?? config.allowlist,
@@ -80,9 +90,21 @@ export function apply(ctx, config = {}) {
80
90
  hostSurface: config.hostSurface && typeof config.hostSurface === 'object'
81
91
  ? { enabled: config.hostSurface.enabled === true, source: config.hostSurface.source }
82
92
  : undefined,
93
+ relay: relayWiring.relay,
94
+ logger: ctx?.logger,
83
95
  })
84
96
  const policy = system.policy.describe()
85
97
 
98
+ // M5:加载后异步探测一次回流对端(不阻塞 apply;没有客户端时立即返回 absent)。
99
+ try { void Promise.resolve(system.relayProbe()).catch(() => {}) } catch { /* 探测失败不影响主流程 */ }
100
+ /** M5:回流自动投递;没有客户端或 autoFlushMs=0 时不开定时器。 */
101
+ const autoFlushMs = Math.max(0, Number(config.relay?.autoFlushMs ?? 2_000) || 0)
102
+ let relayTimer = null
103
+ if (relayWiring.client && autoFlushMs > 0) {
104
+ relayTimer = setInterval(() => { try { void system.relayFlush().catch(() => {}) } catch { /* ignore */ } }, autoFlushMs)
105
+ try { relayTimer.unref?.() } catch { /* ignore */ }
106
+ }
107
+
86
108
  // 3.2.x 兼容计数器(任务级实测汇总;系统层另有账本,两者口径不同、互不覆盖)。
87
109
  const metrics = {
88
110
  tasks: 0,
@@ -95,7 +117,7 @@ export function apply(ctx, config = {}) {
95
117
 
96
118
  const service = {
97
119
  id: SERVICE.cost,
98
- version: '3.3.0',
120
+ version: '3.3.1',
99
121
  role,
100
122
  get mode() {
101
123
  return system.policy.describe().mode
@@ -315,12 +337,24 @@ export function apply(ctx, config = {}) {
315
337
  substitutions(filter = {}) {
316
338
  return system.audit.substitutions(filter)
317
339
  },
340
+ /** M5:瓮城回流出口状态(只读;不可用时必须带具体原因)。 */
341
+ relayStatus() {
342
+ return system.relayStatus()
343
+ },
344
+ /** M5:立即探测回流对端(hello 协商 + relay 状态),返回最新状态。 */
345
+ async relayProbe(input = {}) {
346
+ return await system.relayProbe(input)
347
+ },
348
+ /** M5:立即投递队列中的回流事件;回执里的数字才是送达证据。 */
349
+ async relayFlush() {
350
+ return await system.relayFlush()
351
+ },
318
352
  status() {
319
353
  const current = system.policy.describe()
320
354
  return {
321
355
  ok: true,
322
356
  id: 'cost-optimizer',
323
- version: '3.3.0',
357
+ version: '3.3.1',
324
358
  role,
325
359
  mode: current.mode,
326
360
  requestedMode: current.requestedMode,
@@ -340,8 +374,10 @@ export function apply(ctx, config = {}) {
340
374
  attribution: system.attribution.snapshot(),
341
375
  attributionSummary: system.status().attributionSummary,
342
376
  bus: system.bus.snapshot(1),
343
- /** §5.6:内部事件类型(冒号命名)与对外出口状态(fang 未就绪 ⇒ unavailable)。 */
344
- events: { types: ['cost:event', 'cost:degraded', 'cost:substitution'], relays: system.bus.snapshot(1).relays },
377
+ /** §5.6 / M5:瓮城回流出口(可达 ⇒ available:true + 对端自报 peer;否则带原因)。 */
378
+ relay: system.relayStatus(),
379
+ /** §5.6:内部事件类型(冒号命名)与对外出口状态。 */
380
+ events: { types: ['cost:event', 'cost:degraded', 'cost:substitution'], relays: system.bus.snapshot(1).relays },
345
381
  degraded: system.status().degraded,
346
382
  surfaces: system.status().surfaces,
347
383
  hostSurface: system.hostSurface.status(),
@@ -360,12 +396,18 @@ export function apply(ctx, config = {}) {
360
396
  doubleCountGuard: report.doubleCountGuard,
361
397
  }
362
398
  })(),
363
- note: '3.3.0 M1+M2:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow);工具面 A2–A3 与决策复用 B1–B4 在 M3–M4 接入。',
399
+ note: '3.3.1:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1 真实投递);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认)。',
364
400
  }
365
401
  },
366
402
  }
367
403
 
368
404
  try { ctx.effect(() => ctx.provide(SERVICE.cost, service)) } catch { /* fake ctx */ }
405
+ try {
406
+ ctx.effect(() => () => {
407
+ if (relayTimer) clearInterval(relayTimer)
408
+ relayTimer = null
409
+ })
410
+ } catch { /* fake ctx */ }
369
411
  // 3.3.0 §11:可选子服务(主服务形态不变;消费方必须探测,不能假定存在)。
370
412
  const optionalFaces = {
371
413
  [OPTIONAL_SERVICE.costLedger]: Object.freeze({
@@ -312,6 +312,20 @@ export function apply(ctx, config = {}) {
312
312
  } catch { return null }
313
313
  })()
314
314
  const tickets = id === null ? [...workerEvents.keys()] : [id]
315
+ /**
316
+ * §5.6 / M5:回流出口状态来自成本服务(3.3.1 起是真实探测,不再是硬编码)。
317
+ * 没有成本服务时如实给 `cost-service-unavailable`,不编造“未就绪”原因。
318
+ */
319
+ const relayBlock = (() => {
320
+ try {
321
+ const cost = safeGet(ctx, SERVICE.cost)
322
+ if (cost && typeof cost.relayStatus === 'function') {
323
+ const relay = cost.relayStatus()
324
+ if (relay && typeof relay === 'object') return relay
325
+ }
326
+ } catch { /* 回流状态读取失败不阻塞成本摘要 */ }
327
+ return { target: 'fang', available: false, reason: 'cost-service-unavailable' }
328
+ })()
315
329
  const rows = []
316
330
  const totals = { events: 0, degraded: 0, substitutions: 0, estimated: 0, savedTokens: 0 }
317
331
  for (const key of tickets) {
@@ -345,8 +359,8 @@ export function apply(ctx, config = {}) {
345
359
  totals,
346
360
  /* §11:只读成本块(A 单价项 / B 跳过调用项分列 + 命中率),面板只读消费。 */
347
361
  service: serviceBlock,
348
- /* §5.6:瓮城「产物回流」尚未就绪 —— 显式 unavailable,不假装成功、不阻塞主线。 */
349
- relay: { target: 'fang', available: false, reason: 'fang-channel-not-ready' },
362
+ /* §5.6 / M5:瓮城回流出口真实状态(不可用必须显式带原因)。 */
363
+ relay: relayBlock,
350
364
  }
351
365
  }
352
366