@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,71 @@
1
+ /**
2
+ * 聚合各模块 tunables(运行时默认值 + 挖矿候选参数)。
3
+ */
4
+ import archiveTunables from '../dag/tunables.json' with { type: 'json' }
5
+ import admissionTunables from '../governance/tunables.json' with { type: 'json' }
6
+ import mailboxTunables from '../mailbox/tunables.json' with { type: 'json' }
7
+ import reputationTunables from '../reputation/tunables.json' with { type: 'json' }
8
+ import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
9
+
10
+ import socialTunables from './reputation_social.tunables.json' with { type: 'json' }
11
+
12
+ /**
13
+ * @typedef {{
14
+ * reputation: typeof reputationTunables,
15
+ * trustGraph: typeof trustGraphTunables,
16
+ * social: typeof socialTunables,
17
+ * mailbox: typeof mailboxTunables,
18
+ * archive: typeof archiveTunables,
19
+ * admission: typeof admissionTunables,
20
+ * }} TunablesBundle
21
+ */
22
+
23
+ /** @type {Readonly<Record<keyof TunablesBundle, string>>} */
24
+ export const TUNABLES_PATHS = Object.freeze({
25
+ reputation: new URL('../reputation/tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
26
+ trustGraph: new URL('../trust_graph/tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
27
+ social: new URL('./reputation_social.tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
28
+ mailbox: new URL('../mailbox/tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
29
+ archive: new URL('../dag/tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
30
+ admission: new URL('../governance/tunables.json', import.meta.url).pathname.replace(/\\/g, '/'),
31
+ })
32
+
33
+ /** 只读默认模板(模块加载时构建一次) */
34
+ const DEFAULT_TUNABLES_TEMPLATE = Object.freeze({
35
+ reputation: structuredClone(reputationTunables),
36
+ trustGraph: structuredClone(trustGraphTunables),
37
+ social: structuredClone(socialTunables),
38
+ mailbox: structuredClone(mailboxTunables),
39
+ archive: structuredClone(archiveTunables),
40
+ admission: structuredClone(admissionTunables),
41
+ })
42
+
43
+ /**
44
+ * @returns {TunablesBundle} 当前默认 tunables 深拷贝
45
+ */
46
+ export function loadDefaultTunables() {
47
+ return {
48
+ reputation: structuredClone(DEFAULT_TUNABLES_TEMPLATE.reputation),
49
+ trustGraph: structuredClone(DEFAULT_TUNABLES_TEMPLATE.trustGraph),
50
+ social: structuredClone(DEFAULT_TUNABLES_TEMPLATE.social),
51
+ mailbox: structuredClone(DEFAULT_TUNABLES_TEMPLATE.mailbox),
52
+ archive: structuredClone(DEFAULT_TUNABLES_TEMPLATE.archive),
53
+ admission: structuredClone(DEFAULT_TUNABLES_TEMPLATE.admission),
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @param {Partial<TunablesBundle>} patch 局部覆盖
59
+ * @returns {TunablesBundle} 合并后的 tunables
60
+ */
61
+ export function mergeTunables(patch = {}) {
62
+ const base = loadDefaultTunables()
63
+ return {
64
+ reputation: { ...base.reputation, ...patch.reputation },
65
+ trustGraph: { ...base.trustGraph, ...patch.trustGraph },
66
+ social: { ...base.social, ...patch.social },
67
+ mailbox: { ...base.mailbox, ...patch.mailbox },
68
+ archive: { ...base.archive, ...patch.archive },
69
+ admission: { ...base.admission, ...patch.admission },
70
+ }
71
+ }
@@ -0,0 +1,532 @@
1
+ /**
2
+ * 按攻击者类型评估当前 tunables 的防御薄弱点。
3
+ */
4
+
5
+ /** @typedef {import('./attacks.mjs').AttackKind} AttackKind */
6
+ /** @typedef {import('./metrics.mjs').SimSnapshot} SimSnapshot */
7
+ /** @typedef {import('./scenarios.mjs').SimScenario} SimScenario */
8
+
9
+ /**
10
+ * @typedef {{
11
+ * attack: AttackKind,
12
+ * label: string,
13
+ * defenseRate: number,
14
+ * severity: string,
15
+ * impact: string,
16
+ * worstScenario: string,
17
+ * worstScenarioLabel: string,
18
+ * metricHint: string,
19
+ * meanDefenseRate?: number,
20
+ * defenseVariance?: number,
21
+ * weakSeedFraction?: number,
22
+ * stableWeak?: boolean,
23
+ * }} VulnerabilityEntry
24
+ */
25
+
26
+ /** @typedef {(snap: SimSnapshot, attack: AttackKind) => number} DefenseMetricFn */
27
+
28
+ /**
29
+ * @param {SimSnapshot} snap 快照
30
+ * @param {AttackKind} attack 攻击类型
31
+ * @returns {number} 该类型节点被抑制的比率
32
+ */
33
+ function attackSuppression(snap, attack) {
34
+ return snap.byAttackDefense?.[attack]?.rate ?? snap.malSuppressionRate
35
+ }
36
+
37
+ /**
38
+ * @param {SimSnapshot} snap 快照
39
+ * @returns {number} 联邦/邮箱可达综合防御
40
+ */
41
+ function reachDefense(snap) {
42
+ return (snap.federationReachRate + snap.mailboxReachRate) / 2
43
+ }
44
+
45
+ /**
46
+ * @param {SimSnapshot} snap 快照
47
+ * @returns {number} 共谋环崩溃率(叠加 topK 俘获)
48
+ */
49
+ function defenseCollusion(snap) {
50
+ const harm = attackImpactHarm(snap, 'collusion')
51
+ const suppression = snap.byAttackDefense?.collusion?.rate ?? snap.malSuppressionRate
52
+ const structural = Math.max(snap.collusionCollapseRate, suppression)
53
+ return Math.min(structural, 1 - harm)
54
+ }
55
+
56
+ /**
57
+ * @param {SimSnapshot} snap 快照
58
+ * @returns {number} Sybil 容纳率(叠加 topK 俘获)
59
+ */
60
+ function defenseSybil(snap) {
61
+ const harm = attackImpactHarm(snap, 'sybil')
62
+ return Math.min(snap.sybilContainmentRate, 1 - harm * 0.85)
63
+ }
64
+
65
+ /**
66
+ * @param {SimSnapshot} snap 快照
67
+ * @returns {number} rep_pump 综合防御
68
+ */
69
+ function defenseRepPump(snap) {
70
+ const harm = attackImpactHarm(snap, 'rep_pump')
71
+ const late = Math.min(snap.archiveDefenseRate, snap.archiveQuorumAccuracy)
72
+ return Math.min(Math.max(snap.sybilContainmentRate, 1 - harm), late)
73
+ }
74
+
75
+ /**
76
+ * @param {SimSnapshot} snap 快照
77
+ * @returns {number} 社交攻击综合防御
78
+ */
79
+ function defenseSocialAttack(snap) {
80
+ return Math.min(snap.honestPreservationRate, 1 - snap.falsePositiveRate)
81
+ }
82
+
83
+ /**
84
+ * @param {SimSnapshot} snap 快照
85
+ * @returns {number} 诚实节点保留率
86
+ */
87
+ function defenseHonestPreservation(snap) { return snap.honestPreservationRate }
88
+
89
+ /**
90
+ * @param {SimSnapshot} snap 快照
91
+ * @param {AttackKind} attack 攻击类型
92
+ * @returns {number} 中转刷信誉综合防御
93
+ */
94
+ function defenseRelayFarmer(snap, attack) {
95
+ return defenseGainType(snap, attack)
96
+ }
97
+
98
+ /**
99
+ * @param {SimSnapshot} snap 快照
100
+ * @param {AttackKind} attack 攻击类型
101
+ * @returns {number} 可达综合防御(含 eclipse 影响)
102
+ */
103
+ function defenseReach(snap, attack) {
104
+ const base = reachDefense(snap)
105
+ const harm = attackImpactHarm(snap, attack)
106
+ if (attack === 'eclipse' || attack === 'targeted_eclipse')
107
+ return Math.min(base, defenseGainType(snap, attack), 1 - harm)
108
+ return base
109
+ }
110
+
111
+ /**
112
+ * @param {SimSnapshot} snap 快照
113
+ * @param {AttackKind} attack 攻击类型
114
+ * @returns {number} 节点抑制率
115
+ */
116
+ function defenseBySuppression(snap, attack) { return attackSuppression(snap, attack) }
117
+
118
+ /**
119
+ * @param {SimSnapshot} snap 快照
120
+ * @param {AttackKind} attack 攻击类型
121
+ * @returns {number} chunk 与仲裁综合防御
122
+ */
123
+ function defenseLazyChunk(snap, attack) {
124
+ return Math.min(attackSuppression(snap, attack), snap.archiveQuorumAccuracy)
125
+ }
126
+
127
+ /**
128
+ * @param {SimSnapshot} snap 快照
129
+ * @returns {number} 归档伪造综合防御
130
+ */
131
+ function defenseArchiveForger(snap) {
132
+ return Math.min(snap.archiveDefenseRate, snap.archiveQuorumAccuracy)
133
+ }
134
+
135
+ /** reach 型攻击才计入 reachCollapse 伤害 */
136
+ const REACH_IMPACT_ATTACKS = new Set(['eclipse', 'targeted_eclipse', 'hint_poisoner'])
137
+
138
+ /**
139
+ * @param {SimSnapshot} snap 快照
140
+ * @param {AttackKind} attack 攻击类型
141
+ * @returns {number} 增益型攻击的影响伤害 0..1(越高=攻击越成功)
142
+ */
143
+ function attackImpactHarm(snap, attack) {
144
+ const impact = snap.byAttackImpact?.[attack]
145
+ const topK = impact?.topKCapture ?? 0
146
+ const reachCollapse = REACH_IMPACT_ATTACKS.has(attack) ? impact?.reachCollapse ?? 0 : 0
147
+ return Math.max(topK, reachCollapse * 0.75)
148
+ }
149
+
150
+ /**
151
+ * @param {SimSnapshot} snap 快照
152
+ * @param {AttackKind} attack 攻击类型
153
+ * @returns {number} 增益型综合防御率
154
+ */
155
+ function defenseGainType(snap, attack) {
156
+ const harm = attackImpactHarm(snap, attack)
157
+ const suppression = attackSuppression(snap, attack)
158
+ return Math.max(1 - harm, suppression * 0.55)
159
+ }
160
+
161
+ /**
162
+ * @param {SimSnapshot} snap 快照
163
+ * @returns {number} 误伤反向防御
164
+ */
165
+ function defenseReportFlooder(snap) { return 1 - snap.falsePositiveRate }
166
+
167
+ /**
168
+ * @param {SimSnapshot} snap 快照
169
+ * @returns {number} 身份盗用容纳率
170
+ */
171
+ function defenseKeyThief(snap) { return snap.compromiseContainmentRate }
172
+
173
+ /**
174
+ * @param {SimSnapshot} snap 快照
175
+ * @returns {number} 肉鸡反应率
176
+ */
177
+ function defenseSleeper(snap) { return snap.sleeperReactionRate }
178
+
179
+ /**
180
+ * @param {SimSnapshot} snap 快照
181
+ * @returns {number} 等价欺骗综合防御
182
+ */
183
+ function defenseEquivocator(snap) {
184
+ return Math.min(snap.equivocationDefenseRate, snap.archiveQuorumAccuracy)
185
+ }
186
+
187
+ /**
188
+ * @param {SimSnapshot} snap 快照
189
+ * @returns {number} 定向 eclipse 综合防御
190
+ */
191
+ function defenseTargetedEclipse(snap) {
192
+ const harm = attackImpactHarm(snap, 'targeted_eclipse')
193
+ return Math.min(Math.min(reachDefense(snap), snap.churnReachRate), 1 - harm)
194
+ }
195
+
196
+ /**
197
+ * @param {SimSnapshot} snap 快照
198
+ * @returns {number} 信令洪水防御
199
+ */
200
+ function defenseSignalingFlood(snap) {
201
+ return Math.min(Number(snap.joinThrottleEffectiveness ?? 0), Number(snap.transportReachRate ?? 0))
202
+ }
203
+
204
+ /**
205
+ * @param {SimSnapshot} snap 快照
206
+ * @returns {number} 信令 eclipse 防御
207
+ */
208
+ function defenseSignalingEclipse(snap) {
209
+ return Math.min(Number(snap.signalingDiversityRate ?? 0), Number(snap.transportReachRate ?? 0))
210
+ }
211
+
212
+ /** @type {Record<AttackKind, { label: string, metricHint: string, stressScenarios?: string[], defense: DefenseMetricFn }>} */
213
+ export const ATTACK_PROFILES = {
214
+ sybil: {
215
+ label: 'Sybil 分身洪水',
216
+ metricHint: 'sybilContainmentRate',
217
+ stressScenarios: ['sybil_heavy'],
218
+ defense: defenseSybil,
219
+ },
220
+ rep_pump: {
221
+ label: '信誉刷分泵',
222
+ metricHint: 'defenseRepPump (sybilContainment + archive)',
223
+ defense: defenseRepPump,
224
+ },
225
+ collusion: {
226
+ label: '邀请链共谋环',
227
+ metricHint: 'collusionCollapseRate + byAttackDefense.collusion',
228
+ stressScenarios: ['collusion_ring'],
229
+ defense: defenseCollusion,
230
+ },
231
+ spammer: {
232
+ label: '消息刷屏',
233
+ metricHint: 'byAttackDefense.spammer',
234
+ stressScenarios: ['spam_eclipse'],
235
+ defense: defenseBySuppression,
236
+ },
237
+ false_accuser: {
238
+ label: '诬告诚实节点',
239
+ metricHint: 'honestPreservationRate + falsePositiveRate',
240
+ stressScenarios: ['social_war'],
241
+ defense: defenseSocialAttack,
242
+ },
243
+ eclipse: {
244
+ label: 'Want 列表 eclipse',
245
+ metricHint: 'federationReachRate + mailboxReachRate',
246
+ stressScenarios: ['spam_eclipse', 'eclipse_targeted'],
247
+ defense: defenseReach,
248
+ },
249
+ lazy_chunk: {
250
+ label: '惰性/错误 chunk 服务',
251
+ metricHint: 'archiveQuorumAccuracy',
252
+ defense: defenseLazyChunk,
253
+ },
254
+ social_mob: {
255
+ label: 'Social 群体拉黑',
256
+ metricHint: 'honestPreservationRate + falsePositiveRate',
257
+ stressScenarios: ['social_war'],
258
+ defense: defenseSocialAttack,
259
+ },
260
+ archive_forger: {
261
+ label: '归档摘要伪造',
262
+ metricHint: 'archiveDefenseRate',
263
+ stressScenarios: ['digest_equivocation', 'key_compromise'],
264
+ defense: defenseArchiveForger,
265
+ },
266
+ relay_farmer: {
267
+ label: '中转刷信誉',
268
+ metricHint: 'relayPreservationRate + byAttackImpact.topKCapture(非 reachCollapse)',
269
+ stressScenarios: ['relay_mesh'],
270
+ defense: defenseRelayFarmer,
271
+ },
272
+ whitewasher: {
273
+ label: '洗白复活',
274
+ metricHint: 'byAttackDefense.whitewasher',
275
+ stressScenarios: ['collusion_ring'],
276
+ defense: defenseBySuppression,
277
+ },
278
+ report_flooder: {
279
+ label: '举报洪泛',
280
+ metricHint: '1 - falsePositiveRate',
281
+ stressScenarios: ['social_war'],
282
+ defense: defenseReportFlooder,
283
+ },
284
+ oscillator: {
285
+ label: '信誉振荡',
286
+ metricHint: 'byAttackDefense.oscillator',
287
+ stressScenarios: ['spam_eclipse'],
288
+ defense: defenseBySuppression,
289
+ },
290
+ hint_poisoner: {
291
+ label: '发现提示投毒',
292
+ metricHint: 'byAttackImpact.hint_poisoner.topKCapture',
293
+ defense: defenseGainType,
294
+ },
295
+ key_thief: {
296
+ label: '高信誉身份被盗',
297
+ metricHint: 'compromiseContainmentRate',
298
+ stressScenarios: ['key_compromise'],
299
+ defense: defenseKeyThief,
300
+ },
301
+ sleeper: {
302
+ label: '肉鸡休眠突变',
303
+ metricHint: 'sleeperReactionRate',
304
+ stressScenarios: ['sleeper_turn'],
305
+ defense: defenseSleeper,
306
+ },
307
+ equivocator: {
308
+ label: '归档 digest 等价欺骗',
309
+ metricHint: 'equivocationDefenseRate',
310
+ stressScenarios: ['digest_equivocation'],
311
+ defense: defenseEquivocator,
312
+ },
313
+ targeted_eclipse: {
314
+ label: '定向 eclipse 分区',
315
+ metricHint: 'churnReachRate + federationReachRate',
316
+ stressScenarios: ['eclipse_targeted'],
317
+ defense: defenseTargetedEclipse,
318
+ },
319
+ slow_drip_spammer: {
320
+ label: '慢速 drip 刷屏',
321
+ metricHint: 'byAttackDefense.slow_drip_spammer',
322
+ defense: defenseBySuppression,
323
+ },
324
+ signaling_flood: {
325
+ label: '信令 join 洪水',
326
+ metricHint: 'joinThrottleEffectiveness + transportReachRate',
327
+ stressScenarios: ['transport_siege'],
328
+ defense: defenseSignalingFlood,
329
+ },
330
+ signaling_eclipse: {
331
+ label: '信令源 eclipse',
332
+ metricHint: 'signalingDiversityRate + transportReachRate',
333
+ stressScenarios: ['fresh_join_eclipse'],
334
+ defense: defenseSignalingEclipse,
335
+ },
336
+ }
337
+
338
+ /**
339
+ * @param {number} defenseRate 防御率(越高越好)
340
+ * @returns {{ severity: string, impact: string }} 严重度与影响描述
341
+ */
342
+ export function classifyDefenseSeverity(defenseRate) {
343
+ const d = Math.max(0, Math.min(1, defenseRate))
344
+ if (d < 0.2)
345
+ return {
346
+ severity: '群级瘫痪',
347
+ impact: '作用域为单个群/小联邦(非全网崩溃):该群内联邦发现或消息可达可能瘫痪,攻击者可主导该群',
348
+ }
349
+ if (d < 0.4)
350
+ return {
351
+ severity: '群级接管',
352
+ impact: '作用域为单个群/小联邦:约半数以上能力丧失,或该类型攻击者大面积漏网、诚实节点误伤显著',
353
+ }
354
+ if (d < 0.6)
355
+ return {
356
+ severity: '明显薄弱',
357
+ impact: '三成左右未抑制或连带误伤,该群/小联邦韧性明显下降但尚未全面崩溃',
358
+ }
359
+ if (d < 0.75)
360
+ return {
361
+ severity: '尚可',
362
+ impact: '多数情况能挡住,但该攻击在压力场景下仍有可乘之机',
363
+ }
364
+ return {
365
+ severity: '较稳',
366
+ impact: '该类型攻击下表现良好,不是当前配置的主要短板',
367
+ }
368
+ }
369
+
370
+ /**
371
+ * @param {number[]} samples 防御率样本
372
+ * @returns {{ mean: number, variance: number, min: number }} 跨 seed 统计
373
+ */
374
+ export function defenseStatsAcrossSeeds(samples) {
375
+ if (!samples.length) return { mean: NaN, variance: 0, min: NaN }
376
+ const mean = samples.reduce((a, b) => a + b, 0) / samples.length
377
+ const variance = samples.reduce((a, b) => a + (b - mean) ** 2, 0) / samples.length
378
+ return { mean, variance, min: Math.min(...samples) }
379
+ }
380
+
381
+ /**
382
+ * @param {SimScenario[]} scenarios 场景列表
383
+ * @param {{ byScenario: Record<string, { snapshots?: SimSnapshot[] }> }} evalResult 评估结果
384
+ * @param {number} [topN=5] 返回最弱条目数
385
+ * @returns {{ weakest: VulnerabilityEntry | null, ranked: VulnerabilityEntry[], seedVariance?: object[] }} 薄弱攻击分析
386
+ */
387
+ export function analyzeVulnerabilities(scenarios, evalResult, topN = 5) {
388
+ const scenarioById = new Map(scenarios.map(s => [s.id, s]))
389
+ /** @type {VulnerabilityEntry[]} */
390
+ const ranked = []
391
+
392
+ for (const [attack, profile] of Object.entries(ATTACK_PROFILES)) {
393
+ const relevant = scenarios.filter(s => (s.attacks[/** @type {AttackKind} */ attack] ?? 0) > 0)
394
+ if (!relevant.length) continue
395
+
396
+ const stressIds = profile.stressScenarios?.filter(id => relevant.some(s => s.id === id)) ?? []
397
+ const pool = stressIds.length ? relevant.filter(s => stressIds.includes(s.id)) : relevant
398
+
399
+ /** @type {number[]} */
400
+ const defenses = []
401
+ let worstDefense = Infinity
402
+ let worstScenario = pool[0]?.id ?? ''
403
+ for (const sc of pool) {
404
+ const agg = evalResult.byScenario[sc.id]
405
+ if (!agg?.snapshots?.length) continue
406
+ for (const snap of agg.snapshots) {
407
+ const defense = profile.defense(snap, /** @type {AttackKind} */ attack)
408
+ defenses.push(defense)
409
+ if (defense < worstDefense) {
410
+ worstDefense = defense
411
+ worstScenario = sc.id
412
+ }
413
+ }
414
+ }
415
+
416
+ if (!Number.isFinite(worstDefense) || !defenses.length) continue
417
+
418
+ const { mean, variance } = defenseStatsAcrossSeeds(defenses)
419
+ const weakThreshold = 0.55
420
+ const weakSeedFraction = defenses.filter(d => d < weakThreshold).length / defenses.length
421
+ const stableWeak = weakSeedFraction >= 0.5 || (mean < weakThreshold && variance < 0.02)
422
+
423
+ const { severity, impact } = classifyDefenseSeverity(mean)
424
+ const scMeta = scenarioById.get(worstScenario)
425
+ ranked.push({
426
+ attack: /** @type {AttackKind} */ attack,
427
+ label: profile.label,
428
+ defenseRate: mean,
429
+ meanDefenseRate: mean,
430
+ defenseVariance: variance,
431
+ weakSeedFraction,
432
+ stableWeak,
433
+ severity,
434
+ impact,
435
+ worstScenario,
436
+ worstScenarioLabel: scMeta?.label ?? worstScenario,
437
+ metricHint: profile.metricHint,
438
+ })
439
+ }
440
+
441
+ ranked.sort((a, b) => {
442
+ const aScore = (1 - a.meanDefenseRate) * (0.35 + (a.stableWeak ? a.weakSeedFraction : a.weakSeedFraction * 0.5))
443
+ const bScore = (1 - b.meanDefenseRate) * (0.35 + (b.stableWeak ? b.weakSeedFraction : b.weakSeedFraction * 0.5))
444
+ if (bScore !== aScore) return bScore - aScore
445
+ return a.meanDefenseRate - b.meanDefenseRate
446
+ })
447
+ return {
448
+ weakest: ranked[0] ?? null,
449
+ ranked: ranked.slice(0, topN),
450
+ seedVariance: ranked.map(r => ({
451
+ attack: r.attack,
452
+ mean: r.meanDefenseRate,
453
+ variance: r.defenseVariance,
454
+ weakSeedFraction: r.weakSeedFraction,
455
+ stableWeak: r.stableWeak,
456
+ })),
457
+ }
458
+ }
459
+
460
+ /**
461
+ * @param {{ weakest: VulnerabilityEntry | null, ranked: VulnerabilityEntry[] }} analysis 分析结果
462
+ * @returns {string[]} 控制台行
463
+ */
464
+ export function formatVulnerabilityConsole(analysis) {
465
+ const lines = ['', '=== 防御薄弱点(跨 seed 均值 + 稳定弱项) ===']
466
+ if (!analysis.weakest) {
467
+ lines.push('(无可用攻击场景数据)')
468
+ return lines
469
+ }
470
+
471
+ const w = analysis.weakest
472
+ lines.push(
473
+ `最弱: ${w.label} (${w.attack}) — 均值防御 ${(w.meanDefenseRate * 100).toFixed(1)}%` +
474
+ `(跨 seed 方差 ${(w.defenseVariance ?? 0).toFixed(4)},${((w.weakSeedFraction ?? 0) * 100).toFixed(0)}% seed 低于 55%${w.stableWeak ? ',稳定弱项' : ''})`,
475
+ `严重度: ${w.severity} — ${w.impact}`,
476
+ `最差场景: ${w.worstScenarioLabel} (${w.worstScenario})`,
477
+ )
478
+
479
+ if (analysis.ranked.length > 1) {
480
+ lines.push('', '其他薄弱项:')
481
+ for (const row of analysis.ranked.slice(1))
482
+ lines.push(` · ${row.label}: 均值 ${(row.meanDefenseRate * 100).toFixed(1)}%` +
483
+ ` (var ${(row.defenseVariance ?? 0).toFixed(4)}${row.stableWeak ? ', 稳定' : ''}) — ${row.severity}`)
484
+ }
485
+
486
+ return lines
487
+ }
488
+
489
+ /**
490
+ * @param {{ weakest: VulnerabilityEntry | null, ranked: VulnerabilityEntry[] }} analysis 分析结果
491
+ * @returns {string[]} Markdown 行
492
+ */
493
+ export function formatVulnerabilityMarkdown(analysis) {
494
+ const lines = ['## 防御薄弱点', '']
495
+ if (!analysis.weakest) {
496
+ lines.push('无可用攻击场景数据。', '')
497
+ return lines
498
+ }
499
+
500
+ const w = analysis.weakest
501
+ lines.push(
502
+ `当前配置**最不擅长**应对 **${w.label}**(\`${w.attack}\`)。`,
503
+ '',
504
+ '| 项目 | 值 |',
505
+ '| --- | --- |',
506
+ `| 跨 seed 均值防御率 | ${(w.meanDefenseRate * 100).toFixed(1)}% |`,
507
+ `| 跨 seed 方差 | ${(w.defenseVariance ?? 0).toFixed(4)} |`,
508
+ `| 弱 seed 占比 (<55%) | ${((w.weakSeedFraction ?? 0) * 100).toFixed(0)}% |`,
509
+ `| 稳定弱项 | ${w.stableWeak ? '是' : '否'} |`,
510
+ `| 严重度 | **${w.severity}** |`,
511
+ `| 影响 | ${w.impact} |`,
512
+ `| 最差场景 | ${w.worstScenarioLabel} (\`${w.worstScenario}\`) |`,
513
+ `| 参考指标 | ${w.metricHint} |`,
514
+ '',
515
+ )
516
+
517
+ if (analysis.ranked.length > 1) {
518
+ lines.push('### 其他薄弱攻击者', '', '| 攻击者 | 均值防御 | 方差 | 稳定弱项 | 严重度 | 最差场景 |', '| --- | ---: | ---: | --- | --- | --- |')
519
+ for (const row of analysis.ranked.slice(1))
520
+ lines.push(`| ${row.label} | ${(row.meanDefenseRate * 100).toFixed(1)}% | ${(row.defenseVariance ?? 0).toFixed(4)} | ${row.stableWeak ? '是' : '否'} | ${row.severity} | ${row.worstScenarioLabel} |`)
521
+ lines.push('')
522
+ }
523
+
524
+ if (analysis.seedVariance?.length) {
525
+ lines.push('### 跨 seed 方差(全部攻击类型)', '', '| 攻击者 | 均值 | 方差 | 弱 seed 占比 | 稳定 |', '| --- | ---: | ---: | ---: | --- |')
526
+ for (const row of analysis.seedVariance.slice(0, 12))
527
+ lines.push(`| \`${row.attack}\` | ${(row.mean * 100).toFixed(1)}% | ${row.variance.toFixed(4)} | ${(row.weakSeedFraction * 100).toFixed(0)}% | ${row.stableWeak ? '是' : '否'} |`)
528
+ lines.push('')
529
+ }
530
+
531
+ return lines
532
+ }
package/deno.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "nodeModulesDir": "none",
3
- "lock": false,
4
- "allowScripts": [
5
- "npm:node-datachannel"
6
- ]
7
- }