@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
package/sim/cli.mjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env -S deno run --allow-scripts=npm:node-datachannel --allow-all
|
|
2
|
+
/* global Deno */
|
|
3
|
+
/**
|
|
4
|
+
* P2P 参数模拟器 + 挖矿器 CLI。
|
|
5
|
+
*
|
|
6
|
+
* 用法(勿用 blanket --allow-scripts:optional BLE 原生编译失败会整命令 abort,见 docs/runtime.md):
|
|
7
|
+
* deno run --allow-scripts=npm:node-datachannel --allow-all sim/cli.mjs sim --scenario balanced
|
|
8
|
+
* deno run --allow-scripts=npm:node-datachannel --allow-all sim/cli.mjs mine --generations 20
|
|
9
|
+
* deno run --allow-scripts=npm:node-datachannel --allow-all sim/cli.mjs mine --duration 5m
|
|
10
|
+
* deno run --allow-scripts=npm:node-datachannel --allow-all sim/cli.mjs mine --duration 5m --no-apply
|
|
11
|
+
* deno run --allow-scripts=npm:node-datachannel --allow-all sim/cli.mjs mine --social-tunables ../shells/social/src/federation/reputation_social.tunables.json
|
|
12
|
+
*/
|
|
13
|
+
import { applyTunablesBundle } from './apply.mjs'
|
|
14
|
+
import { normalizeAttackGenome } from './attack_space.mjs'
|
|
15
|
+
import { runCoevolution } from './coevolution.mjs'
|
|
16
|
+
import { parseDurationMs } from './duration.mjs'
|
|
17
|
+
import { DEFAULT_WEIGHTS, evaluateTunablesAgainstAttacks } from './metrics.mjs'
|
|
18
|
+
import { runSimulation } from './model.mjs'
|
|
19
|
+
import { shouldApplyResult } from './optimizer.mjs'
|
|
20
|
+
import { writeReport } from './report.mjs'
|
|
21
|
+
import { resolveScenarios } from './scenarios.mjs'
|
|
22
|
+
import { defaultConcurrency, shutdownSimPool } from './sim_pool.mjs'
|
|
23
|
+
import { loadDefaultTunables } from './tunables_bundle.mjs'
|
|
24
|
+
import { analyzeVulnerabilities, formatVulnerabilityConsole } from './vulnerability.mjs'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string[]} argv CLI 参数(不含 node/脚本路径)
|
|
28
|
+
* @returns {Record<string, string | boolean> & { _: string[] }} 解析结果(`_` 为位置参数)
|
|
29
|
+
*/
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
/** @type {Record<string, string | boolean> & { _: string[] }} */
|
|
32
|
+
const out = { _: [] }
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const arg = argv[i]
|
|
35
|
+
if (arg.startsWith('--')) {
|
|
36
|
+
const key = arg.slice(2)
|
|
37
|
+
const next = argv[i + 1]
|
|
38
|
+
if (!next || next.startsWith('--')) out[key] = true
|
|
39
|
+
else {
|
|
40
|
+
out[key] = next
|
|
41
|
+
i++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else out._.push(arg)
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string | boolean | undefined} raw 原始参数值
|
|
51
|
+
* @param {number} fallback 解析失败时的默认值
|
|
52
|
+
* @returns {number} 数值
|
|
53
|
+
*/
|
|
54
|
+
function num(raw, fallback) {
|
|
55
|
+
const n = Number(raw)
|
|
56
|
+
return Number.isFinite(n) ? n : fallback
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | boolean | undefined} raw 原始参数值
|
|
61
|
+
* @returns {number[]} 种子列表
|
|
62
|
+
*/
|
|
63
|
+
function parseSeeds(raw) {
|
|
64
|
+
if (typeof raw !== 'string') return [1, 2, 3, 4, 5, 6]
|
|
65
|
+
return raw.split(',').map(s => Number(s.trim())).filter(Number.isFinite)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string | boolean | undefined} raw 原始 --jobs 参数
|
|
70
|
+
* @returns {number} 并发度
|
|
71
|
+
*/
|
|
72
|
+
function parseJobsLimit(raw) {
|
|
73
|
+
const n = Number(raw)
|
|
74
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : defaultConcurrency()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {Record<string, string | boolean> & { _: string[] }} cliArguments 已解析 CLI 参数
|
|
79
|
+
* @returns {import('./metrics.mjs').EvalOptions} 评估并行选项(默认自动占满 CPU)
|
|
80
|
+
*/
|
|
81
|
+
function buildEvalOptions(cliArguments) {
|
|
82
|
+
if (cliArguments.serial || cliArguments.jobs === '1' || cliArguments.jobs === 1)
|
|
83
|
+
return { serial: true }
|
|
84
|
+
if (cliArguments.jobs != null && cliArguments.jobs !== true)
|
|
85
|
+
return { concurrency: parseJobsLimit(String(cliArguments.jobs)) }
|
|
86
|
+
return {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {number | null} durationMs 时长上限
|
|
91
|
+
* @param {number} generations 代数上限
|
|
92
|
+
* @returns {(info: import('./optimizer.mjs').OptimizerProgress) => void} 进度打印器
|
|
93
|
+
*/
|
|
94
|
+
function createProgressPrinter(durationMs, generations) {
|
|
95
|
+
const isTty = typeof Deno.stdout.isTerminal === 'function' && Deno.stdout.isTerminal()
|
|
96
|
+
let lastWrite = 0
|
|
97
|
+
const encoder = new TextEncoder()
|
|
98
|
+
|
|
99
|
+
return (info) => {
|
|
100
|
+
const now = Date.now()
|
|
101
|
+
if (now - lastWrite < 200 && info.generation > 0) return
|
|
102
|
+
lastWrite = now
|
|
103
|
+
|
|
104
|
+
const pct = info.percent.toFixed(1)
|
|
105
|
+
const elapsed = (info.elapsedMs / 1000).toFixed(1)
|
|
106
|
+
const limit = durationMs != null ? `${(durationMs / 1000).toFixed(1)}` : `${generations}`
|
|
107
|
+
const line = `coevo ${pct}% | gen ${info.generationsRun} | blue ${info.bestFitness.toFixed(4)} | mean ${info.meanFitness.toFixed(4)} | ${elapsed}s/${limit}${durationMs != null ? 's' : ' gen'}`
|
|
108
|
+
|
|
109
|
+
if (isTty)
|
|
110
|
+
Deno.stdout.writeSync(encoder.encode(`\r${line.padEnd(80)}`))
|
|
111
|
+
else if (info.generation === 0 || info.generationsRun % 10 === 0)
|
|
112
|
+
console.log(line)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {Record<string, string | boolean> & { _: string[] }} cliArguments 已解析参数
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
120
|
+
async function cmdSim(cliArguments) {
|
|
121
|
+
const scenarioId = String(cliArguments.scenario || cliArguments.scenarios || 'balanced')
|
|
122
|
+
const seeds = parseSeeds(cliArguments.seeds)
|
|
123
|
+
const tunables = loadDefaultTunables()
|
|
124
|
+
const scenarios = resolveScenarios(scenarioId)
|
|
125
|
+
|
|
126
|
+
for (const scenario of scenarios)
|
|
127
|
+
for (const seed of seeds) {
|
|
128
|
+
const snap = runSimulation(scenario, seed, tunables)
|
|
129
|
+
console.log(JSON.stringify({ scenario: scenario.id, seed, snap }, null, 2))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {Record<string, string | boolean> & { _: string[] }} cliArguments 已解析参数
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
async function cmdMine(cliArguments) {
|
|
138
|
+
const scenarioId = String(cliArguments.scenarios || cliArguments.scenario || 'all')
|
|
139
|
+
const generations = num(cliArguments.generations, 20)
|
|
140
|
+
const population = num(cliArguments.population, 12)
|
|
141
|
+
const seeds = parseSeeds(cliArguments.seeds)
|
|
142
|
+
const seedBase = num(cliArguments.seedBase, 42)
|
|
143
|
+
const durationMs = parseDurationMs(cliArguments.duration)
|
|
144
|
+
const doApply = !(cliArguments['no-apply'] || cliArguments['dry-run'])
|
|
145
|
+
const skipFullEval = Boolean(cliArguments['skip-full-eval'])
|
|
146
|
+
const scenarios = resolveScenarios(scenarioId)
|
|
147
|
+
const evalOptions = buildEvalOptions(cliArguments)
|
|
148
|
+
const jobCount = evalOptions.serial ? 1 : evalOptions.concurrency ?? defaultConcurrency()
|
|
149
|
+
|
|
150
|
+
if (durationMs == null && cliArguments.duration != null && cliArguments.duration !== true)
|
|
151
|
+
console.warn(`warning: invalid --duration ${JSON.stringify(cliArguments.duration)}, using --generations instead`)
|
|
152
|
+
|
|
153
|
+
console.log(`parallel: ${evalOptions.serial ? 'off (serial)' : `${jobCount} workers (auto CPU)`}`)
|
|
154
|
+
|
|
155
|
+
const onProgress = createProgressPrinter(durationMs, generations)
|
|
156
|
+
|
|
157
|
+
const result = await runCoevolution({
|
|
158
|
+
scenarios,
|
|
159
|
+
generations,
|
|
160
|
+
population,
|
|
161
|
+
seeds,
|
|
162
|
+
seedBase,
|
|
163
|
+
weights: DEFAULT_WEIGHTS,
|
|
164
|
+
durationMs,
|
|
165
|
+
evalOptions,
|
|
166
|
+
onProgress,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (typeof Deno.stdout.isTerminal === 'function' && Deno.stdout.isTerminal())
|
|
170
|
+
console.log('')
|
|
171
|
+
|
|
172
|
+
const { baseline, best, bestRed, attackHof, history, stoppedBy, generationsRun, elapsedMs } = result
|
|
173
|
+
|
|
174
|
+
const attackPanel = [
|
|
175
|
+
...attackHof.map(h => h.genome),
|
|
176
|
+
bestRed.attackGenome,
|
|
177
|
+
normalizeAttackGenome(undefined),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
let baselineFull = best.result
|
|
181
|
+
let bestFull = best.result
|
|
182
|
+
let vulnerability = analyzeVulnerabilities(scenarios, bestFull)
|
|
183
|
+
/** @type {{ ok: boolean, reason: string }} */
|
|
184
|
+
let gate = { ok: false, reason: 'skip-full-eval' }
|
|
185
|
+
|
|
186
|
+
if (!skipFullEval) {
|
|
187
|
+
// 写回前一律在**全场景**上复评(含红队名人堂面板),避免单场景过拟合后污染全局默认值。
|
|
188
|
+
const allScenarios = resolveScenarios('all')
|
|
189
|
+
baselineFull = await evaluateTunablesAgainstAttacks(
|
|
190
|
+
allScenarios, seeds, baseline.tunables, attackPanel, runSimulation, DEFAULT_WEIGHTS, evalOptions,
|
|
191
|
+
)
|
|
192
|
+
bestFull = await evaluateTunablesAgainstAttacks(
|
|
193
|
+
allScenarios, seeds, best.tunables, attackPanel, runSimulation, DEFAULT_WEIGHTS, evalOptions,
|
|
194
|
+
)
|
|
195
|
+
vulnerability = analyzeVulnerabilities(allScenarios, bestFull)
|
|
196
|
+
gate = shouldApplyResult(baselineFull, bestFull)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @type {object} */
|
|
200
|
+
const applyInfo = {
|
|
201
|
+
applied: false,
|
|
202
|
+
reason: skipFullEval ? 'skip-full-eval' : doApply ? gate.reason : 'dry-run',
|
|
203
|
+
full: { baseline: baselineFull.fitness, best: bestFull.fitness },
|
|
204
|
+
}
|
|
205
|
+
if (doApply && gate.ok) {
|
|
206
|
+
const socialTunablesPath = cliArguments['social-tunables']
|
|
207
|
+
if (typeof socialTunablesPath !== 'string' || !socialTunablesPath.trim())
|
|
208
|
+
throw new Error('mine apply requires --social-tunables PATH (shell reputation_social.tunables.json)')
|
|
209
|
+
const written = await applyTunablesBundle(best.tunables, { socialTunablesPath })
|
|
210
|
+
applyInfo.applied = true
|
|
211
|
+
applyInfo.written = written
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const payload = {
|
|
215
|
+
generatedAt: new Date().toISOString(),
|
|
216
|
+
scenarios: scenarios.map(s => s.id),
|
|
217
|
+
seeds,
|
|
218
|
+
generations: durationMs != null ? null : generations,
|
|
219
|
+
population,
|
|
220
|
+
durationMs,
|
|
221
|
+
elapsedMs,
|
|
222
|
+
stoppedBy,
|
|
223
|
+
generationsRun,
|
|
224
|
+
baseline: { result: baseline.result, tunables: baseline.tunables },
|
|
225
|
+
best: { result: best.result, tunables: best.tunables },
|
|
226
|
+
bestRed: { result: bestRed.result, attackGenome: bestRed.attackGenome },
|
|
227
|
+
attackHof,
|
|
228
|
+
history,
|
|
229
|
+
apply: applyInfo,
|
|
230
|
+
vulnerability,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { jsonPath, mdPath } = await writeReport(payload, applyInfo.applied ? 'mine-applied' : 'mine')
|
|
234
|
+
console.log(`fitness baseline=${baseline.result.fitness.toFixed(4)} best=${best.result.fitness.toFixed(4)}`)
|
|
235
|
+
console.log(`red harm best=${(-bestRed.result.fitness).toFixed(4)} hof=${attackHof.length}`)
|
|
236
|
+
console.log(`all-scenario fitness baseline=${baselineFull.fitness.toFixed(4)} best=${bestFull.fitness.toFixed(4)}`)
|
|
237
|
+
if (durationMs != null)
|
|
238
|
+
console.log(`stopped by ${stoppedBy} after ${generationsRun} generation(s), elapsed ${(elapsedMs / 1000).toFixed(1)}s / ${(durationMs / 1000).toFixed(1)}s`)
|
|
239
|
+
console.log(`report: ${jsonPath}`)
|
|
240
|
+
console.log(`report: ${mdPath}`)
|
|
241
|
+
if (applyInfo.applied) console.log('applied best tunables to module JSON files')
|
|
242
|
+
else if (doApply) console.log(`not applied: ${applyInfo.reason}`)
|
|
243
|
+
else console.log('dry-run: tunables not written (use default apply; pass --no-apply to suppress)')
|
|
244
|
+
|
|
245
|
+
for (const line of formatVulnerabilityConsole(vulnerability))
|
|
246
|
+
console.log(line)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 打印 CLI 用法与选项说明。
|
|
251
|
+
*/
|
|
252
|
+
function printHelp() {
|
|
253
|
+
console.log(`usage:
|
|
254
|
+
cli.mjs sim [--scenario ID] [--seeds 1,2,3,4,5,6]
|
|
255
|
+
cli.mjs mine [--scenarios ID|all] [--generations N] [--duration 5m]
|
|
256
|
+
[--population N] [--seeds 1,2,3,4,5,6] [--jobs N] [--serial]
|
|
257
|
+
[--no-apply|--dry-run] [--skip-full-eval]
|
|
258
|
+
[--social-tunables PATH] (required when applying social tunables)
|
|
259
|
+
|
|
260
|
+
默认 mine 自动按 CPU 逻辑核心数并行仿真(无需额外参数)。
|
|
261
|
+
--jobs N 可手动限制并发;--serial 或 --jobs 1 强制串行。
|
|
262
|
+
默认 mine 会将最优参数写回各模块 JSON(需超过基线 + margin)。
|
|
263
|
+
加 --no-apply 或 --dry-run 仅生成报告。
|
|
264
|
+
快速迭代:--seeds 1 --population 8 --skip-full-eval`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const cliArguments = parseArgs(Deno.args)
|
|
268
|
+
const cmd = cliArguments._[0] || 'mine'
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
if (cliArguments.help || cliArguments.h) printHelp()
|
|
272
|
+
else if (cmd === 'sim') await cmdSim(cliArguments)
|
|
273
|
+
else if (cmd === 'mine') await cmdMine(cliArguments)
|
|
274
|
+
else {
|
|
275
|
+
console.error(`unknown command: ${cmd}`)
|
|
276
|
+
printHelp()
|
|
277
|
+
Deno.exit(1)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
shutdownSimPool()
|
|
282
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 竞争式共演进:蓝队(tunables) vs 红队(attack genome) + 名人堂。
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
mutateAttackGenome,
|
|
6
|
+
normalizeAttackGenome,
|
|
7
|
+
randomAttackGenome,
|
|
8
|
+
updateAttackHallOfFame,
|
|
9
|
+
} from './attack_space.mjs'
|
|
10
|
+
import { pastDeadline } from './duration.mjs'
|
|
11
|
+
import { evaluateManyAgainstAttacks, evaluateTunablesAgainstAttacks, minPanelFitness } from './metrics.mjs'
|
|
12
|
+
import { runSimulation } from './model.mjs'
|
|
13
|
+
import { computeProgressPercent } from './optimizer.mjs'
|
|
14
|
+
import { createRng } from './rng.mjs'
|
|
15
|
+
import { mutateCandidate, randomCandidate } from './space.mjs'
|
|
16
|
+
import { loadDefaultTunables } from './tunables_bundle.mjs'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* tunables: import('./tunables_bundle.mjs').TunablesBundle,
|
|
21
|
+
* attackGenome: import('./attack_space.mjs').AttackGenome,
|
|
22
|
+
* result: Awaited<ReturnType<typeof evaluateTunablesAgainstAttacks>>,
|
|
23
|
+
* generation: number,
|
|
24
|
+
* }} CoevoCandidate
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 蓝队参数
|
|
29
|
+
* @param {import('./attack_space.mjs').AttackGenome[]} attackGenomes 红队基因组
|
|
30
|
+
* @returns {import('./metrics.mjs').EvalCandidate[]} 单基因组候选
|
|
31
|
+
*/
|
|
32
|
+
function redEvalCandidates(tunables, attackGenomes) {
|
|
33
|
+
return attackGenomes.map(attackGenome => ({
|
|
34
|
+
tunables,
|
|
35
|
+
attackPanel: [attackGenome],
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Awaited<ReturnType<typeof evaluateManyAgainstAttacks>>} results 蓝队评估
|
|
41
|
+
* @returns {Awaited<ReturnType<typeof evaluateTunablesAgainstAttacks>>[]} 红队伤害评估
|
|
42
|
+
*/
|
|
43
|
+
function toRedHarmResults(results) {
|
|
44
|
+
return results.map(r => ({ ...r, fitness: 1 - r.fitness }))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} options 选项
|
|
49
|
+
* @param {import('./scenarios.mjs').SimScenario[]} options.scenarios 场景
|
|
50
|
+
* @param {number} [options.generations=20] 代数
|
|
51
|
+
* @param {number} [options.population=12] 蓝队种群
|
|
52
|
+
* @param {number} [options.redPopulation=8] 红队种群
|
|
53
|
+
* @param {number[]} [options.seeds] 种子
|
|
54
|
+
* @param {number} [options.seedBase=42] 搜索种子基
|
|
55
|
+
* @param {import('./metrics.mjs').MetricWeights} [options.weights] 权重
|
|
56
|
+
* @param {number | null} [options.durationMs] 时长上限
|
|
57
|
+
* @param {import('./metrics.mjs').EvalOptions} [options.evalOptions] 评估并行选项
|
|
58
|
+
* @param {(info: import('./optimizer.mjs').OptimizerProgress) => void} [options.onProgress] 进度
|
|
59
|
+
* @returns {Promise<object>} 共演进结果
|
|
60
|
+
*/
|
|
61
|
+
export async function runCoevolution(options) {
|
|
62
|
+
const {
|
|
63
|
+
scenarios,
|
|
64
|
+
generations = 20,
|
|
65
|
+
population = 12,
|
|
66
|
+
redPopulation = 8,
|
|
67
|
+
seeds = [1, 2, 3],
|
|
68
|
+
seedBase = 42,
|
|
69
|
+
weights,
|
|
70
|
+
durationMs = null,
|
|
71
|
+
evalOptions,
|
|
72
|
+
onProgress,
|
|
73
|
+
} = options
|
|
74
|
+
|
|
75
|
+
const startedAt = Date.now()
|
|
76
|
+
const deadline = durationMs != null && durationMs > 0 ? startedAt + durationMs : null
|
|
77
|
+
const timed = deadline != null
|
|
78
|
+
|
|
79
|
+
const baselineTunables = loadDefaultTunables()
|
|
80
|
+
const baselineAttack = normalizeAttackGenome(undefined)
|
|
81
|
+
const baselineEval = await evaluateTunablesAgainstAttacks(
|
|
82
|
+
scenarios, seeds, baselineTunables, [baselineAttack], runSimulation, weights, evalOptions,
|
|
83
|
+
)
|
|
84
|
+
const baseline = { tunables: baselineTunables, attackGenome: baselineAttack, result: baselineEval, generation: 0 }
|
|
85
|
+
|
|
86
|
+
let bestBlue = { ...baseline }
|
|
87
|
+
let bestRed = {
|
|
88
|
+
tunables: baselineTunables,
|
|
89
|
+
attackGenome: randomAttackGenome(() => Math.random()),
|
|
90
|
+
result: baselineEval,
|
|
91
|
+
generation: 0,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @type {Array<{ genome: import('./attack_space.mjs').AttackGenome, fitness: number }>} */
|
|
95
|
+
let attackHof = []
|
|
96
|
+
/** @type {CoevoCandidate[]} */
|
|
97
|
+
let bluePool = [{ ...baseline }]
|
|
98
|
+
|
|
99
|
+
const initBluePanel = [baselineAttack, ...attackHof.map(h => h.genome)]
|
|
100
|
+
const initBlueCandidates = []
|
|
101
|
+
for (let i = 1; i < population; i++)
|
|
102
|
+
initBlueCandidates.push({ tunables: randomCandidate(seedBase + i), attackPanel: initBluePanel })
|
|
103
|
+
|
|
104
|
+
if (initBlueCandidates.length) {
|
|
105
|
+
const initBlueResults = await evaluateManyAgainstAttacks(
|
|
106
|
+
scenarios, seeds, initBlueCandidates, runSimulation, weights, evalOptions,
|
|
107
|
+
)
|
|
108
|
+
for (let i = 0; i < initBlueResults.length; i++) {
|
|
109
|
+
const rec = {
|
|
110
|
+
tunables: initBlueCandidates[i].tunables,
|
|
111
|
+
attackGenome: baselineAttack,
|
|
112
|
+
result: initBlueResults[i],
|
|
113
|
+
generation: 0,
|
|
114
|
+
}
|
|
115
|
+
bluePool.push(rec)
|
|
116
|
+
if (initBlueResults[i].fitness > bestBlue.result.fitness) bestBlue = rec
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @type {CoevoCandidate[]} */
|
|
121
|
+
let redPool = []
|
|
122
|
+
const initRedGenomes = []
|
|
123
|
+
for (let i = 0; i < redPopulation; i++)
|
|
124
|
+
initRedGenomes.push(randomAttackGenome(createRng(seedBase + 500 + i)))
|
|
125
|
+
|
|
126
|
+
const initRedResults = toRedHarmResults(await evaluateManyAgainstAttacks(
|
|
127
|
+
scenarios, seeds, redEvalCandidates(bestBlue.tunables, initRedGenomes), runSimulation, weights, evalOptions,
|
|
128
|
+
))
|
|
129
|
+
for (let i = 0; i < initRedResults.length; i++) {
|
|
130
|
+
const attackGenome = initRedGenomes[i]
|
|
131
|
+
const rec = { tunables: bestBlue.tunables, attackGenome, result: initRedResults[i], generation: 0 }
|
|
132
|
+
redPool.push(rec)
|
|
133
|
+
attackHof = updateAttackHallOfFame(attackHof, attackGenome, -initRedResults[i].fitness)
|
|
134
|
+
if (-initRedResults[i].fitness > -bestRed.result.fitness) bestRed = rec
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @type {Array<{ generation: number, bestFitness: number, meanFitness: number, bestRedHarm: number }>} */
|
|
138
|
+
const history = [{
|
|
139
|
+
generation: 0,
|
|
140
|
+
bestFitness: bestBlue.result.fitness,
|
|
141
|
+
meanFitness: bluePool.reduce((s, c) => s + c.result.fitness, 0) / bluePool.length,
|
|
142
|
+
bestRedHarm: -bestRed.result.fitness,
|
|
143
|
+
}]
|
|
144
|
+
|
|
145
|
+
let gen = 1
|
|
146
|
+
let generationsRun = 0
|
|
147
|
+
while (true) {
|
|
148
|
+
if (!timed && gen > generations) break
|
|
149
|
+
if (timed && pastDeadline(deadline)) break
|
|
150
|
+
|
|
151
|
+
const attackPanel = [...attackHof.map(h => h.genome), ...redPool.slice(0, 3).map(r => r.attackGenome)]
|
|
152
|
+
|
|
153
|
+
bluePool.sort((a, b) => b.result.fitness - a.result.fitness)
|
|
154
|
+
const blueElites = bluePool.slice(0, Math.max(2, Math.floor(population / 4)))
|
|
155
|
+
/** @type {CoevoCandidate[]} */
|
|
156
|
+
const nextBlue = [...blueElites]
|
|
157
|
+
/** @type {Array<{ parent: CoevoCandidate, tunables: import('./tunables_bundle.mjs').TunablesBundle }>} */
|
|
158
|
+
const blueMutations = []
|
|
159
|
+
const blueOffspring = population - nextBlue.length
|
|
160
|
+
while (blueMutations.length < blueOffspring) {
|
|
161
|
+
const parent = blueElites[blueMutations.length % blueElites.length]
|
|
162
|
+
blueMutations.push({
|
|
163
|
+
parent,
|
|
164
|
+
tunables: mutateCandidate(parent.tunables, seedBase + gen * 1000 + nextBlue.length + blueMutations.length),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
if (blueMutations.length) {
|
|
168
|
+
const blueBatch = blueMutations.map(m => ({ tunables: m.tunables, attackPanel }))
|
|
169
|
+
const blueResults = await evaluateManyAgainstAttacks(
|
|
170
|
+
scenarios, seeds, blueBatch, runSimulation, weights, evalOptions,
|
|
171
|
+
)
|
|
172
|
+
for (let i = 0; i < blueResults.length; i++) {
|
|
173
|
+
const rec = {
|
|
174
|
+
tunables: blueMutations[i].tunables,
|
|
175
|
+
attackGenome: blueMutations[i].parent.attackGenome,
|
|
176
|
+
result: blueResults[i],
|
|
177
|
+
generation: gen,
|
|
178
|
+
}
|
|
179
|
+
nextBlue.push(rec)
|
|
180
|
+
if (blueResults[i].fitness > bestBlue.result.fitness) bestBlue = rec
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
bluePool = nextBlue
|
|
184
|
+
|
|
185
|
+
redPool.sort((a, b) => a.result.fitness - b.result.fitness)
|
|
186
|
+
const redElites = redPool.slice(0, Math.max(2, Math.floor(redPopulation / 3)))
|
|
187
|
+
/** @type {CoevoCandidate[]} */
|
|
188
|
+
const nextRed = [...redElites]
|
|
189
|
+
/** @type {import('./attack_space.mjs').AttackGenome[]} */
|
|
190
|
+
const redMutations = []
|
|
191
|
+
const redOffspring = redPopulation - nextRed.length
|
|
192
|
+
while (redMutations.length < redOffspring) {
|
|
193
|
+
const parent = redElites[redMutations.length % redElites.length]
|
|
194
|
+
redMutations.push(mutateAttackGenome(parent.attackGenome, seedBase + gen * 2000 + nextRed.length + redMutations.length))
|
|
195
|
+
}
|
|
196
|
+
if (redMutations.length) {
|
|
197
|
+
const redResults = toRedHarmResults(await evaluateManyAgainstAttacks(
|
|
198
|
+
scenarios, seeds, redEvalCandidates(bestBlue.tunables, redMutations), runSimulation, weights, evalOptions,
|
|
199
|
+
))
|
|
200
|
+
for (let i = 0; i < redResults.length; i++) {
|
|
201
|
+
const attackGenome = redMutations[i]
|
|
202
|
+
const rec = { tunables: bestBlue.tunables, attackGenome, result: redResults[i], generation: gen }
|
|
203
|
+
nextRed.push(rec)
|
|
204
|
+
attackHof = updateAttackHallOfFame(attackHof, attackGenome, -redResults[i].fitness)
|
|
205
|
+
if (-redResults[i].fitness > -bestRed.result.fitness) bestRed = rec
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
redPool = nextRed
|
|
209
|
+
|
|
210
|
+
generationsRun = gen
|
|
211
|
+
history.push({
|
|
212
|
+
generation: gen,
|
|
213
|
+
bestFitness: bestBlue.result.fitness,
|
|
214
|
+
meanFitness: bluePool.reduce((s, c) => s + c.result.fitness, 0) / bluePool.length,
|
|
215
|
+
bestRedHarm: -bestRed.result.fitness,
|
|
216
|
+
})
|
|
217
|
+
if (onProgress)
|
|
218
|
+
onProgress({
|
|
219
|
+
generation: gen,
|
|
220
|
+
generationsRun,
|
|
221
|
+
bestFitness: bestBlue.result.fitness,
|
|
222
|
+
meanFitness: history[history.length - 1].meanFitness,
|
|
223
|
+
elapsedMs: Date.now() - startedAt,
|
|
224
|
+
durationMs,
|
|
225
|
+
generations,
|
|
226
|
+
percent: computeProgressPercent(durationMs, generations, gen, Date.now() - startedAt, timed),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
gen++
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
baseline,
|
|
234
|
+
best: bestBlue,
|
|
235
|
+
bestRed,
|
|
236
|
+
attackHof,
|
|
237
|
+
history,
|
|
238
|
+
stoppedBy: timed && pastDeadline(deadline) ? 'duration' : 'generations',
|
|
239
|
+
generationsRun,
|
|
240
|
+
durationMs,
|
|
241
|
+
elapsedMs: Date.now() - startedAt,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void minPanelFitness
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Sim cold-start (K = 0)
|
|
2
|
+
|
|
3
|
+
Mesh policy: [docs/mesh.md](../docs/mesh.md). Day-to-day sim rules: [AGENTS.md](AGENTS.md).
|
|
4
|
+
|
|
5
|
+
When the observer has no acquaintances (`trustedPeers=[]`), the sim must still join via discovery explore slots — not idle.
|
|
6
|
+
|
|
7
|
+
| Knob | Behavior |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `scenario.coldStartObserver: true` | Observer starts with `trustedPeers=[]` |
|
|
10
|
+
| `initObserverDiscovery(..., coldBootstrap=true)` | Discovery starts empty |
|
|
11
|
+
| Each round: `coldStartDiscoveryJoin` | Simulates mesh scan / join |
|
|
12
|
+
|
|
13
|
+
Scenario + regression: `cold_start` in `scenarios.mjs`, `sim/test/cold_start.test.mjs`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仿真用拓扑/可达魔法常数(定性趋势,非真实网络推导)。
|
|
3
|
+
*/
|
|
4
|
+
export const MAILBOX_REACH_LOW_HOP_FACTOR = 0.45
|
|
5
|
+
/**
|
|
6
|
+
* maxHop=2 时 mailbox 可达折损系数。
|
|
7
|
+
*/
|
|
8
|
+
export const MAILBOX_REACH_MID_HOP_FACTOR = 0.72
|
|
9
|
+
/**
|
|
10
|
+
* 跳数成本指数(hopFactor 幂次)。
|
|
11
|
+
*/
|
|
12
|
+
export const MAILBOX_HOP_COST_EXPONENT = 1.5
|
|
13
|
+
/**
|
|
14
|
+
* 跳数成本线性缩放系数。
|
|
15
|
+
*/
|
|
16
|
+
export const MAILBOX_HOP_COST_SCALE = 0.28
|
|
17
|
+
/**
|
|
18
|
+
* wantFanout 超基准(3)的附加成本系数。
|
|
19
|
+
*/
|
|
20
|
+
export const MAILBOX_FANOUT_COST_SCALE = 0.05
|
|
21
|
+
/**
|
|
22
|
+
* 超过必要跳数时的惩罚系数。
|
|
23
|
+
*/
|
|
24
|
+
export const MAILBOX_EXCESS_HOP_PENALTY = 0.24
|
|
25
|
+
/**
|
|
26
|
+
* relay 步数归一化除数(分母项)。
|
|
27
|
+
*/
|
|
28
|
+
export const MAILBOX_RELAY_COST_DIVISOR = 6
|
|
29
|
+
/**
|
|
30
|
+
* 联邦单 peer 覆盖概率上限。
|
|
31
|
+
*/
|
|
32
|
+
export const FEDERATION_SINGLE_PEER_CAP = 0.55
|
|
33
|
+
/**
|
|
34
|
+
* 联邦单 peer 覆盖概率缩放(÷ 在线诚实 relay 数)。
|
|
35
|
+
*/
|
|
36
|
+
export const FEDERATION_SINGLE_PEER_SCALE = 1.4
|
|
37
|
+
/**
|
|
38
|
+
* 攻击方发现门控基线概率(仿真代理)。
|
|
39
|
+
*/
|
|
40
|
+
export const ATTACK_DISCOVERY_GATE_BASE = 0.55
|
|
41
|
+
/**
|
|
42
|
+
* Sybil 声誉收益冷却轮数(此前轮次不发放)。
|
|
43
|
+
*/
|
|
44
|
+
export const SYBIL_REP_EARN_COST_ROUNDS = 4
|