@steve02081504/fount-p2p 0.0.30 → 0.0.32
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/AGENTS.md +79 -0
- package/docs/evfs.md +32 -0
- package/docs/infra.md +25 -0
- package/docs/mesh.md +67 -0
- package/docs/reputation.md +22 -0
- package/docs/runtime.md +62 -0
- package/docs/signaling.md +23 -0
- package/docs/transports.md +92 -0
- package/docs/wire.md +21 -0
- package/link/providers/webrtc.mjs +5 -2
- package/link/rtc/ice_local_hostname.mjs +15 -47
- package/link/rtc/polyfill.mjs +3 -15
- package/package.json +2 -9
- package/sim/AGENTS.md +29 -0
- package/sim/admission.mjs +56 -0
- package/sim/anomaly.mjs +39 -0
- package/sim/apply.mjs +77 -0
- package/sim/attack_space.mjs +113 -0
- package/sim/attacks.mjs +614 -0
- package/sim/behavior.mjs +78 -0
- package/sim/cli.mjs +282 -0
- package/sim/coevolution.mjs +245 -0
- package/sim/cold_start.md +13 -0
- package/sim/constants.mjs +44 -0
- package/sim/discovery.mjs +259 -0
- package/sim/duration.mjs +64 -0
- package/sim/graph_adj.mjs +28 -0
- package/sim/integrity.mjs +58 -0
- package/sim/metrics.mjs +423 -0
- package/sim/model.mjs +1176 -0
- package/sim/optimizer.mjs +250 -0
- package/sim/propagation.mjs +57 -0
- package/sim/report.mjs +176 -0
- package/sim/reputation_social.tunables.json +6 -0
- package/sim/rng.mjs +62 -0
- package/sim/scenarios.mjs +345 -0
- package/sim/sim_pool.mjs +169 -0
- package/sim/sim_worker.mjs +31 -0
- package/sim/social_reputation.mjs +30 -0
- package/sim/space.mjs +312 -0
- package/sim/test/admission.test.mjs +25 -0
- package/sim/test/attack_space.test.mjs +39 -0
- package/sim/test/behavior_vector.test.mjs +37 -0
- package/sim/test/coevolution.test.mjs +32 -0
- package/sim/test/cold_start.test.mjs +46 -0
- package/sim/test/defense.test.mjs +320 -0
- package/sim/test/discovery.test.mjs +68 -0
- package/sim/test/duration.test.mjs +20 -0
- package/sim/test/fidelity.test.mjs +151 -0
- package/sim/test/integrity.test.mjs +56 -0
- package/sim/test/optimizer.test.mjs +24 -0
- package/sim/test/propagation.test.mjs +29 -0
- package/sim/test/sim_model_regressions.test.mjs +110 -0
- package/sim/test/slow_drip.test.mjs +28 -0
- package/sim/test/smoke.test.mjs +52 -0
- package/sim/test/transport.test.mjs +55 -0
- package/sim/test/tunables_apply.test.mjs +64 -0
- package/sim/test/tunables_param_space.test.mjs +74 -0
- package/sim/test/vulnerability.test.mjs +127 -0
- package/sim/transport.mjs +135 -0
- package/sim/tunables_bundle.mjs +71 -0
- package/sim/vulnerability.mjs +532 -0
- package/deno.json +0 -7
- package/link/rtc/w3c_bridge.mjs +0 -152
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 参数搜索(随机 + 进化)。
|
|
3
|
+
*/
|
|
4
|
+
import { pastDeadline } from './duration.mjs'
|
|
5
|
+
import { evaluateTunables, minPanelFitness } from './metrics.mjs'
|
|
6
|
+
import { runSimulation } from './model.mjs'
|
|
7
|
+
import { mutateCandidate, randomCandidate } from './space.mjs'
|
|
8
|
+
import { loadDefaultTunables } from './tunables_bundle.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* tunables: import('./tunables_bundle.mjs').TunablesBundle,
|
|
13
|
+
* result: Awaited<ReturnType<typeof evaluateTunables>>,
|
|
14
|
+
* generation: number,
|
|
15
|
+
* }} CandidateRecord
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* generation: number,
|
|
21
|
+
* generationsRun: number,
|
|
22
|
+
* bestFitness: number,
|
|
23
|
+
* meanFitness: number,
|
|
24
|
+
* elapsedMs: number,
|
|
25
|
+
* durationMs: number | null,
|
|
26
|
+
* generations: number,
|
|
27
|
+
* percent: number,
|
|
28
|
+
* }} OptimizerProgress
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* baseline: CandidateRecord,
|
|
34
|
+
* best: CandidateRecord,
|
|
35
|
+
* history: Array<{ generation: number, bestFitness: number, meanFitness: number }>,
|
|
36
|
+
* stoppedBy: 'generations' | 'duration',
|
|
37
|
+
* generationsRun: number,
|
|
38
|
+
* durationMs: number | null,
|
|
39
|
+
* elapsedMs: number,
|
|
40
|
+
* }} OptimizerResult
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {number | null} durationMs 时长上限
|
|
45
|
+
* @param {number} generations 代数上限
|
|
46
|
+
* @param {number} generation 当前代
|
|
47
|
+
* @param {number} elapsedMs 已用毫秒
|
|
48
|
+
* @param {boolean} timed 是否按时间停止
|
|
49
|
+
* @returns {number} 0..100 进度百分比
|
|
50
|
+
*/
|
|
51
|
+
export function computeProgressPercent(durationMs, generations, generation, elapsedMs, timed) {
|
|
52
|
+
let raw
|
|
53
|
+
if (timed && durationMs != null && durationMs > 0)
|
|
54
|
+
raw = (elapsedMs / durationMs) * 100
|
|
55
|
+
else
|
|
56
|
+
raw = generations > 0 ? (generation / generations) * 100 : 0
|
|
57
|
+
return Math.max(0, Math.min(100, raw))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {CandidateRecord[]} pool 当前种群
|
|
62
|
+
* @param {CandidateRecord} best 当前最优
|
|
63
|
+
* @returns {{ pool: CandidateRecord[], best: CandidateRecord, meanFitness: number }} 本代统计
|
|
64
|
+
*/
|
|
65
|
+
function recordGeneration(pool, best) {
|
|
66
|
+
const meanFitness = pool.reduce((s, c) => s + c.result.fitness, 0) / pool.length
|
|
67
|
+
return { pool, best, meanFitness }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {object} options 选项
|
|
72
|
+
* @param {import('./scenarios.mjs').SimScenario[]} options.scenarios 场景
|
|
73
|
+
* @param {number} [options.generations=20] 代数上限(未设 duration 时生效)
|
|
74
|
+
* @param {number} [options.population=12] 种群
|
|
75
|
+
* @param {number[]} [options.seeds] 种子
|
|
76
|
+
* @param {number} [options.seedBase=42] 搜索种子基
|
|
77
|
+
* @param {import('./metrics.mjs').MetricWeights} [options.weights] 权重
|
|
78
|
+
* @param {number | null} [options.durationMs] 墙钟时长上限(毫秒);设则按时间停止
|
|
79
|
+
* @param {import('./metrics.mjs').EvalOptions} [options.evalOptions] 评估并行选项
|
|
80
|
+
* @param {(info: OptimizerProgress) => void} [options.onProgress] 进度回调
|
|
81
|
+
* @returns {Promise<OptimizerResult>} 搜索结论
|
|
82
|
+
*/
|
|
83
|
+
export async function runOptimizer(options) {
|
|
84
|
+
const {
|
|
85
|
+
scenarios,
|
|
86
|
+
generations = 20,
|
|
87
|
+
population = 12,
|
|
88
|
+
seeds = [1, 2, 3],
|
|
89
|
+
seedBase = 42,
|
|
90
|
+
weights,
|
|
91
|
+
durationMs = null,
|
|
92
|
+
evalOptions,
|
|
93
|
+
onProgress,
|
|
94
|
+
} = options
|
|
95
|
+
|
|
96
|
+
const startedAt = Date.now()
|
|
97
|
+
const deadline = durationMs != null && durationMs > 0 ? startedAt + durationMs : null
|
|
98
|
+
const timed = deadline != null
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {number} generation 代数
|
|
102
|
+
* @param {number} generationsRun 已完成代数
|
|
103
|
+
* @param {number} bestFitness 最优适应度
|
|
104
|
+
* @param {number} meanFitness 平均适应度
|
|
105
|
+
*/
|
|
106
|
+
function emitProgress(generation, generationsRun, bestFitness, meanFitness) {
|
|
107
|
+
if (!onProgress) return
|
|
108
|
+
const elapsedMs = Date.now() - startedAt
|
|
109
|
+
onProgress({
|
|
110
|
+
generation,
|
|
111
|
+
generationsRun,
|
|
112
|
+
bestFitness,
|
|
113
|
+
meanFitness,
|
|
114
|
+
elapsedMs,
|
|
115
|
+
durationMs,
|
|
116
|
+
generations,
|
|
117
|
+
percent: computeProgressPercent(durationMs, generations, generation, elapsedMs, timed),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const baselineTunables = loadDefaultTunables()
|
|
122
|
+
const baselineResult = await evaluateTunables(scenarios, seeds, baselineTunables, runSimulation, weights, evalOptions)
|
|
123
|
+
const baseline = { tunables: baselineTunables, result: baselineResult, generation: 0 }
|
|
124
|
+
|
|
125
|
+
/** @type {CandidateRecord} */
|
|
126
|
+
let best = { ...baseline }
|
|
127
|
+
/** @type {Array<{ generation: number, bestFitness: number, meanFitness: number }>} */
|
|
128
|
+
const history = []
|
|
129
|
+
|
|
130
|
+
/** @type {CandidateRecord[]} */
|
|
131
|
+
let pool = [{ ...baseline }]
|
|
132
|
+
for (let i = 1; i < population; i++) {
|
|
133
|
+
const tunables = randomCandidate(seedBase + i)
|
|
134
|
+
const result = await evaluateTunables(scenarios, seeds, tunables, runSimulation, weights, evalOptions)
|
|
135
|
+
const rec = { tunables, result, generation: 0 }
|
|
136
|
+
pool.push(rec)
|
|
137
|
+
if (result.fitness > best.result.fitness) best = rec
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
const snap = recordGeneration(pool, best)
|
|
142
|
+
history.push({
|
|
143
|
+
generation: 0,
|
|
144
|
+
bestFitness: best.result.fitness,
|
|
145
|
+
meanFitness: snap.meanFitness,
|
|
146
|
+
})
|
|
147
|
+
emitProgress(0, 0, best.result.fitness, snap.meanFitness)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @type {'generations' | 'duration'} */
|
|
151
|
+
let stoppedBy = timed ? 'duration' : 'generations'
|
|
152
|
+
let generationsRun = 0
|
|
153
|
+
|
|
154
|
+
if (timed && pastDeadline(deadline))
|
|
155
|
+
return {
|
|
156
|
+
baseline,
|
|
157
|
+
best,
|
|
158
|
+
history,
|
|
159
|
+
stoppedBy: 'duration',
|
|
160
|
+
generationsRun: 0,
|
|
161
|
+
durationMs,
|
|
162
|
+
elapsedMs: Date.now() - startedAt,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let gen = 1
|
|
166
|
+
while (true) {
|
|
167
|
+
if (!timed && gen > generations) break
|
|
168
|
+
|
|
169
|
+
pool.sort((a, b) => b.result.fitness - a.result.fitness)
|
|
170
|
+
const elites = pool.slice(0, Math.max(2, Math.floor(population / 4)))
|
|
171
|
+
/** @type {CandidateRecord[]} */
|
|
172
|
+
const next = [...elites]
|
|
173
|
+
|
|
174
|
+
while (next.length < population) {
|
|
175
|
+
const parent = elites[next.length % elites.length]
|
|
176
|
+
const tunables = mutateCandidate(parent.tunables, seedBase + gen * 1000 + next.length)
|
|
177
|
+
const result = await evaluateTunables(scenarios, seeds, tunables, runSimulation, weights, evalOptions)
|
|
178
|
+
const rec = { tunables, result, generation: gen }
|
|
179
|
+
next.push(rec)
|
|
180
|
+
if (result.fitness > best.result.fitness) best = rec
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pool = next
|
|
184
|
+
generationsRun = gen
|
|
185
|
+
const snap = recordGeneration(pool, best)
|
|
186
|
+
history.push({
|
|
187
|
+
generation: gen,
|
|
188
|
+
bestFitness: best.result.fitness,
|
|
189
|
+
meanFitness: snap.meanFitness,
|
|
190
|
+
})
|
|
191
|
+
emitProgress(gen, generationsRun, best.result.fitness, snap.meanFitness)
|
|
192
|
+
|
|
193
|
+
if (timed && pastDeadline(deadline)) {
|
|
194
|
+
stoppedBy = 'duration'
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!timed && gen >= generations) {
|
|
199
|
+
stoppedBy = 'generations'
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
gen++
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
baseline,
|
|
208
|
+
best,
|
|
209
|
+
history,
|
|
210
|
+
stoppedBy,
|
|
211
|
+
generationsRun,
|
|
212
|
+
durationMs,
|
|
213
|
+
elapsedMs: Date.now() - startedAt,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 写回门槛(防单场景过拟合):必须在**全场景**总适应度上超过基线 + margin,
|
|
219
|
+
* 且不得让任何单一场景相对基线明显回退。
|
|
220
|
+
* @param {Awaited<ReturnType<typeof evaluateTunables>>} baselineEval 全场景基线评估
|
|
221
|
+
* @param {Awaited<ReturnType<typeof evaluateTunables>>} candidateEval 全场景候选评估
|
|
222
|
+
* @param {object} [options] 选项
|
|
223
|
+
* @param {number} [options.margin=0.02] 全场景最小提升
|
|
224
|
+
* @param {number} [options.regressTol=0.01] 单场景允许的最大回退
|
|
225
|
+
* @returns {{ ok: boolean, reason: string }} 是否应写回与原因
|
|
226
|
+
*/
|
|
227
|
+
export function shouldApplyResult(baselineEval, candidateEval, options = {}) {
|
|
228
|
+
const { margin = 0.02, regressTol = 0.01 } = options
|
|
229
|
+
if (candidateEval.fitness < baselineEval.fitness + margin)
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: `全场景 fitness ${candidateEval.fitness.toFixed(4)} 未超过基线 ${baselineEval.fitness.toFixed(4)} + margin ${margin}`,
|
|
233
|
+
}
|
|
234
|
+
for (const [id, agg] of Object.entries(candidateEval.byScenario)) {
|
|
235
|
+
const base = baselineEval.byScenario[id]
|
|
236
|
+
if (base && agg.fitness < base.fitness - regressTol)
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
reason: `场景 ${id} 回退(${agg.fitness.toFixed(4)} < 基线 ${base.fitness.toFixed(4)} - ${regressTol})`,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const baseMin = minPanelFitness(baselineEval.byScenario)
|
|
243
|
+
const candMin = minPanelFitness(candidateEval.byScenario)
|
|
244
|
+
if (candMin < baseMin - regressTol)
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: `面板最差 min ${candMin.toFixed(4)} 低于基线 ${baseMin.toFixed(4)} - ${regressTol}`,
|
|
248
|
+
}
|
|
249
|
+
return { ok: true, reason: 'ok' }
|
|
250
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 信誉信号异步传播(去全知 slash)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ targetId: string, senderId: string, claim: number, verified: boolean, birthRound: number, spread: number }} PropagatedSlash
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @returns {{ queue: PropagatedSlash[], delivered: Set<string> }} 传播状态
|
|
11
|
+
*/
|
|
12
|
+
export function createPropagationState() {
|
|
13
|
+
return { queue: [], delivered: new Set() }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ queue: PropagatedSlash[] }} state 传播状态
|
|
18
|
+
* @param {PropagatedSlash} alert slash 警报
|
|
19
|
+
* @returns {void}
|
|
20
|
+
*/
|
|
21
|
+
export function enqueueSlash(state, alert) {
|
|
22
|
+
state.queue.push(alert)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} state 传播状态
|
|
27
|
+
* @param {number} round 当前回合
|
|
28
|
+
* @param {(target: string, sender: string, claim: number, verified: boolean) => void} apply 应用到观察者
|
|
29
|
+
* @param {(senderId: string) => number} trustOf 发送者信任
|
|
30
|
+
* @param {number} [fanout=0.35] 每跳扩散概率基数
|
|
31
|
+
* @param {() => number} [rng] 随机源
|
|
32
|
+
* @returns {number} 本回合送达数
|
|
33
|
+
*/
|
|
34
|
+
export function tickPropagation(state, round, apply, trustOf, fanout = 0.35, rng = () => Math.random()) {
|
|
35
|
+
let delivered = 0
|
|
36
|
+
/** @type {PropagatedSlash[]} */
|
|
37
|
+
const remaining = []
|
|
38
|
+
for (const alert of state.queue) {
|
|
39
|
+
const age = round - alert.birthRound
|
|
40
|
+
if (age < alert.spread) {
|
|
41
|
+
remaining.push(alert)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
const key = `${alert.targetId}:${alert.senderId}:${alert.verified}:${alert.claim}`
|
|
45
|
+
if (state.delivered.has(key)) continue
|
|
46
|
+
const p = Math.min(0.95, fanout * Math.max(0.05, trustOf(alert.senderId)))
|
|
47
|
+
if (rng() > p) {
|
|
48
|
+
remaining.push({ ...alert, spread: alert.spread + 1 })
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
state.delivered.add(key)
|
|
52
|
+
apply(alert.targetId, alert.senderId, alert.claim, alert.verified)
|
|
53
|
+
delivered++
|
|
54
|
+
}
|
|
55
|
+
state.queue = remaining
|
|
56
|
+
return delivered
|
|
57
|
+
}
|
package/sim/report.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 挖矿结果报告(写入 gitignore 目录)。
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { snapshotMetricRows } from './metrics.mjs'
|
|
9
|
+
import { formatVulnerabilityMarkdown } from './vulnerability.mjs'
|
|
10
|
+
|
|
11
|
+
const SIM_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
|
|
13
|
+
/** gitignore 下的报告输出目录 */
|
|
14
|
+
export const RESULTS_DIR = path.join(SIM_DIR, 'results')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Array<{ generation: number, bestFitness: number, meanFitness: number }>} history 完整历史
|
|
18
|
+
* @param {number} [maxRows=60] 最大行数
|
|
19
|
+
* @returns {Array<{ generation: number, bestFitness: number, meanFitness: number, note?: string }>} 下采样历史
|
|
20
|
+
*/
|
|
21
|
+
export function downsampleHistory(history, maxRows = 60) {
|
|
22
|
+
if (!history?.length || history.length <= maxRows) return history ?? []
|
|
23
|
+
|
|
24
|
+
/** @type {Array<{ generation: number, bestFitness: number, meanFitness: number, note?: string }>} */
|
|
25
|
+
const out = []
|
|
26
|
+
let lastBest = -Infinity
|
|
27
|
+
const stride = Math.ceil(history.length / maxRows)
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < history.length; i++) {
|
|
30
|
+
const row = history[i]
|
|
31
|
+
const improved = row.bestFitness > lastBest + 1e-6
|
|
32
|
+
const sampled = i % stride === 0 || i === history.length - 1
|
|
33
|
+
if (improved || sampled) {
|
|
34
|
+
out.push({ ...row, note: improved && !sampled ? 'best↑' : undefined })
|
|
35
|
+
if (improved) lastBest = row.bestFitness
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (out.length && out[0].generation !== history[0].generation)
|
|
40
|
+
out.unshift({ ...history[0] })
|
|
41
|
+
|
|
42
|
+
const last = history[history.length - 1]
|
|
43
|
+
if (out[out.length - 1]?.generation !== last.generation)
|
|
44
|
+
out.push({ ...last })
|
|
45
|
+
|
|
46
|
+
if (out.length <= maxRows) return out
|
|
47
|
+
|
|
48
|
+
const first = out[0]
|
|
49
|
+
const tail = out[out.length - 1]
|
|
50
|
+
const middle = out.slice(1, -1)
|
|
51
|
+
const midStride = Math.max(1, Math.ceil(middle.length / Math.max(1, maxRows - 2)))
|
|
52
|
+
const sampledMiddle = middle.filter((_, i) => i % midStride === 0)
|
|
53
|
+
return [first, ...sampledMiddle, tail].slice(0, maxRows)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} payload 报告内容
|
|
58
|
+
* @param {string} [tag] 文件名标签
|
|
59
|
+
* @returns {Promise<{ jsonPath: string, mdPath: string }>} 写入路径
|
|
60
|
+
*/
|
|
61
|
+
export async function writeReport(payload, tag = 'latest') {
|
|
62
|
+
await mkdir(RESULTS_DIR, { recursive: true })
|
|
63
|
+
const stamp = new Date().toISOString().replace(/[.:]/g, '-')
|
|
64
|
+
const base = `${tag}-${stamp}`
|
|
65
|
+
const jsonPath = path.join(RESULTS_DIR, `${base}.json`)
|
|
66
|
+
const mdPath = path.join(RESULTS_DIR, `${base}.md`)
|
|
67
|
+
|
|
68
|
+
const reportPayload = {
|
|
69
|
+
...payload,
|
|
70
|
+
historyFull: payload.history,
|
|
71
|
+
history: downsampleHistory(payload.history),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await writeFile(jsonPath, `${JSON.stringify(reportPayload, null, 2)}\n`, 'utf8')
|
|
75
|
+
await writeFile(mdPath, formatMarkdown(reportPayload), 'utf8')
|
|
76
|
+
return { jsonPath, mdPath }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {object} payload 报告载荷
|
|
81
|
+
* @returns {string} Markdown 正文
|
|
82
|
+
*/
|
|
83
|
+
function formatMarkdown(payload) {
|
|
84
|
+
const lines = [
|
|
85
|
+
'# P2P 参数挖矿报告',
|
|
86
|
+
'',
|
|
87
|
+
`生成时间: ${payload.generatedAt || new Date().toISOString()}`,
|
|
88
|
+
'',
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
if (payload.durationMs != null) {
|
|
92
|
+
const elapsed = Number(payload.elapsedMs)
|
|
93
|
+
const limit = Number(payload.durationMs)
|
|
94
|
+
lines.push(
|
|
95
|
+
`运行时长: ${Number.isFinite(elapsed) ? (elapsed / 1000).toFixed(1) : '—'}s / ${Number.isFinite(limit) ? (limit / 1000).toFixed(1) : '—'}s`,
|
|
96
|
+
`停止原因: ${payload.stoppedBy || '—'}(完成 ${payload.generationsRun ?? '—'} 代)`,
|
|
97
|
+
'',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(
|
|
102
|
+
'## 适应度',
|
|
103
|
+
'',
|
|
104
|
+
'| 指标 | 基线 | 最优 |',
|
|
105
|
+
'| --- | ---: | ---: |',
|
|
106
|
+
`| fitness | ${fmt(payload.baseline?.result?.fitness)} | ${fmt(payload.best?.result?.fitness)} |`,
|
|
107
|
+
`| mean | ${fmt(payload.baseline?.result?.mean)} | ${fmt(payload.best?.result?.mean)} |`,
|
|
108
|
+
`| min | ${fmt(payload.baseline?.result?.min)} | ${fmt(payload.best?.result?.min)} |`,
|
|
109
|
+
'',
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const baselineSnap = payload.baseline?.result?.byScenario
|
|
113
|
+
? Object.values(payload.baseline.result.byScenario)[0]?.snapshots?.[0]
|
|
114
|
+
: null
|
|
115
|
+
const bestSnap = payload.best?.result?.byScenario
|
|
116
|
+
? Object.values(payload.best.result.byScenario)[0]?.snapshots?.[0]
|
|
117
|
+
: null
|
|
118
|
+
|
|
119
|
+
if (baselineSnap && bestSnap) {
|
|
120
|
+
lines.push('## 评测维度', '', '| 维度 | 基线 | 最优 |', '| --- | ---: | ---: |')
|
|
121
|
+
for (const row of snapshotMetricRows(baselineSnap, bestSnap))
|
|
122
|
+
lines.push(`| ${row.key} | ${fmt(row.baseline)} | ${fmt(row.best)} |`)
|
|
123
|
+
lines.push('')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (payload.apply?.applied)
|
|
127
|
+
lines.push('## 已应用', '', '最优参数已写回各模块 JSON。', '')
|
|
128
|
+
else if (payload.apply)
|
|
129
|
+
lines.push('## 未应用', '', String(payload.apply.reason || '未达提升门槛'), '')
|
|
130
|
+
|
|
131
|
+
if (payload.bestRed?.attackGenome)
|
|
132
|
+
lines.push(
|
|
133
|
+
'## 红队(攻击基因)',
|
|
134
|
+
'',
|
|
135
|
+
'| 指标 | 值 |',
|
|
136
|
+
'| --- | --- |',
|
|
137
|
+
`| 最大伤害 | ${fmt(-(payload.bestRed.result?.fitness ?? 0))} |`,
|
|
138
|
+
`| 名人堂规模 | ${payload.attackHof?.length ?? 0} |`,
|
|
139
|
+
'',
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
const history = payload.history ?? []
|
|
144
|
+
const historyFull = payload.historyFull ?? history
|
|
145
|
+
if (history.length) {
|
|
146
|
+
const folded = historyFull.length > history.length
|
|
147
|
+
? `(由 ${historyFull.length} 代下采样至 ${history.length} 行)`
|
|
148
|
+
: ''
|
|
149
|
+
lines.push('## 收敛', '', `| generation | bestFitness | meanFitness | note |${folded}`, '| ---: | ---: | ---: | --- |')
|
|
150
|
+
for (const row of history) {
|
|
151
|
+
const note = row.note ?? ''
|
|
152
|
+
lines.push(`| ${row.generation} | ${fmt(row.bestFitness)} | ${fmt(row.meanFitness)} | ${note} |`)
|
|
153
|
+
}
|
|
154
|
+
lines.push('')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (payload.best?.result?.byScenario) {
|
|
158
|
+
lines.push('## 分场景', '')
|
|
159
|
+
for (const [id, agg] of Object.entries(payload.best.result.byScenario))
|
|
160
|
+
lines.push(`- **${id}**: fitness=${fmt(agg.fitness)}, mean=${fmt(agg.mean)}, min=${fmt(agg.min)}`)
|
|
161
|
+
lines.push('')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (payload.vulnerability)
|
|
165
|
+
lines.push(...formatVulnerabilityMarkdown(payload.vulnerability))
|
|
166
|
+
|
|
167
|
+
return `${lines.join('\n')}\n`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {number | undefined} n 数值
|
|
172
|
+
* @returns {string} 格式化字符串
|
|
173
|
+
*/
|
|
174
|
+
function fmt(n) {
|
|
175
|
+
return Number.isFinite(n) ? n.toFixed(4) : '—'
|
|
176
|
+
}
|
package/sim/rng.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 确定性 PRNG(mulberry32)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {number} seed 种子
|
|
7
|
+
* @returns {() => number} [0, 1) 均匀随机函数
|
|
8
|
+
*/
|
|
9
|
+
export function createRng(seed) {
|
|
10
|
+
let state = seed >>> 0
|
|
11
|
+
return () => {
|
|
12
|
+
state = (state + 0x6D2B79F5) >>> 0
|
|
13
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
|
14
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
|
|
15
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {() => number} rng 随机源
|
|
21
|
+
* @param {number} min 下界(含)
|
|
22
|
+
* @param {number} max 上界(不含)
|
|
23
|
+
* @returns {number} [min, max) 整数
|
|
24
|
+
*/
|
|
25
|
+
export function randInt(rng, min, max) {
|
|
26
|
+
return Math.floor(rng() * (max - min)) + min
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {() => number} rng 随机源
|
|
31
|
+
* @param {readonly T[]} items 候选数组
|
|
32
|
+
* @returns {T} 随机元素
|
|
33
|
+
* @template T
|
|
34
|
+
*/
|
|
35
|
+
export function pickOne(rng, items) {
|
|
36
|
+
return items[Math.floor(rng() * items.length)]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {() => number} rng 随机源
|
|
41
|
+
* @param {readonly T[]} items 候选数组
|
|
42
|
+
* @param {number} k 抽取个数
|
|
43
|
+
* @returns {T[]} 不重复随机子集
|
|
44
|
+
* @template T
|
|
45
|
+
*/
|
|
46
|
+
export function pickMany(rng, items, k) {
|
|
47
|
+
const copy = [...items]
|
|
48
|
+
const out = []
|
|
49
|
+
while (copy.length && out.length < k) {
|
|
50
|
+
const i = Math.floor(rng() * copy.length)
|
|
51
|
+
out.push(copy.splice(i, 1)[0])
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {number} index 序号
|
|
58
|
+
* @returns {string} 64 位十六进制 伪 nodeHash
|
|
59
|
+
*/
|
|
60
|
+
export function fakeNodeHash(index) {
|
|
61
|
+
return index.toString(16).padStart(64, '0')
|
|
62
|
+
}
|