@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.
Files changed (64) hide show
  1. package/AGENTS.md +79 -0
  2. package/docs/evfs.md +32 -0
  3. package/docs/infra.md +25 -0
  4. package/docs/mesh.md +67 -0
  5. package/docs/reputation.md +22 -0
  6. package/docs/runtime.md +62 -0
  7. package/docs/signaling.md +23 -0
  8. package/docs/transports.md +92 -0
  9. package/docs/wire.md +21 -0
  10. package/link/providers/webrtc.mjs +5 -2
  11. package/link/rtc/ice_local_hostname.mjs +15 -47
  12. package/link/rtc/polyfill.mjs +3 -15
  13. package/package.json +2 -9
  14. package/sim/AGENTS.md +29 -0
  15. package/sim/admission.mjs +56 -0
  16. package/sim/anomaly.mjs +39 -0
  17. package/sim/apply.mjs +77 -0
  18. package/sim/attack_space.mjs +113 -0
  19. package/sim/attacks.mjs +614 -0
  20. package/sim/behavior.mjs +78 -0
  21. package/sim/cli.mjs +282 -0
  22. package/sim/coevolution.mjs +245 -0
  23. package/sim/cold_start.md +13 -0
  24. package/sim/constants.mjs +44 -0
  25. package/sim/discovery.mjs +259 -0
  26. package/sim/duration.mjs +64 -0
  27. package/sim/graph_adj.mjs +28 -0
  28. package/sim/integrity.mjs +58 -0
  29. package/sim/metrics.mjs +423 -0
  30. package/sim/model.mjs +1176 -0
  31. package/sim/optimizer.mjs +250 -0
  32. package/sim/propagation.mjs +57 -0
  33. package/sim/report.mjs +176 -0
  34. package/sim/reputation_social.tunables.json +6 -0
  35. package/sim/rng.mjs +62 -0
  36. package/sim/scenarios.mjs +345 -0
  37. package/sim/sim_pool.mjs +169 -0
  38. package/sim/sim_worker.mjs +31 -0
  39. package/sim/social_reputation.mjs +30 -0
  40. package/sim/space.mjs +312 -0
  41. package/sim/test/admission.test.mjs +25 -0
  42. package/sim/test/attack_space.test.mjs +39 -0
  43. package/sim/test/behavior_vector.test.mjs +37 -0
  44. package/sim/test/coevolution.test.mjs +32 -0
  45. package/sim/test/cold_start.test.mjs +46 -0
  46. package/sim/test/defense.test.mjs +320 -0
  47. package/sim/test/discovery.test.mjs +68 -0
  48. package/sim/test/duration.test.mjs +20 -0
  49. package/sim/test/fidelity.test.mjs +151 -0
  50. package/sim/test/integrity.test.mjs +56 -0
  51. package/sim/test/optimizer.test.mjs +24 -0
  52. package/sim/test/propagation.test.mjs +29 -0
  53. package/sim/test/sim_model_regressions.test.mjs +110 -0
  54. package/sim/test/slow_drip.test.mjs +28 -0
  55. package/sim/test/smoke.test.mjs +52 -0
  56. package/sim/test/transport.test.mjs +55 -0
  57. package/sim/test/tunables_apply.test.mjs +64 -0
  58. package/sim/test/tunables_param_space.test.mjs +74 -0
  59. package/sim/test/vulnerability.test.mjs +127 -0
  60. package/sim/transport.mjs +135 -0
  61. package/sim/tunables_bundle.mjs +71 -0
  62. package/sim/vulnerability.mjs +532 -0
  63. package/deno.json +0 -7
  64. 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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "socialRepHideThreshold": 0,
3
+ "socialBlockDecayFraction": 0.02,
4
+ "followedBlockSelfTrustPenalty": 0.12,
5
+ "followedBlockPenalty": 0.06
6
+ }
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
+ }