@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/AGENTS.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: P2P tunables simulation / co-evolution harness — fidelity boundary between reused real logic and heuristic proxy environment
|
|
3
|
+
globs: sim/**
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# P2P Sim Harness Guide
|
|
8
|
+
|
|
9
|
+
In-process co-evolution of **tunables** (`*.tunables.json`) against an **attack genome**, scored by `metrics.mjs`. Search proxy — **not** wire-protocol replay.
|
|
10
|
+
|
|
11
|
+
## Fidelity boundary
|
|
12
|
+
|
|
13
|
+
- **Reused verbatim** (import real decision functions; do not re-model): `reputation/engine.mjs`, `reputation/math.mjs`, `sim/social_reputation.mjs` (`*Pure`), `trust_graph/engine.mjs` (`pickTop`), `trust_graph/resolve.mjs`, `governance/join_pow.mjs`.
|
|
14
|
+
- **Tunables source:** `tunables_bundle.mjs` loads in-package `*.tunables.json` plus `sim/reputation_social.tunables.json`. Write social tunables back via `cli.mjs mine --social-tunables PATH` (`apply.mjs`).
|
|
15
|
+
- **`PARAM_SPACE` ↔ defaults:** every `PARAM_SPACE` key must exist in `loadDefaultTunables()`; clear both sides when deleting a key.
|
|
16
|
+
- **`socialRepHideThreshold`:** hide when `score < threshold`. Default `0` (suppress negatives only). Raise the threshold to raise `falsePositiveRate` — never use a negative threshold for "stricter".
|
|
17
|
+
- **Heuristic proxy** (not the real path): `model.mjs`, `discovery.mjs`, `transport.mjs`, `integrity.mjs` — analytical "params → defense" only.
|
|
18
|
+
- **Mesh-first:** explore slots and cold-start join are mandatory; fanout/trust stay separate from link presence. Policy: [docs/mesh.md](../docs/mesh.md). Wiring: [cold_start.md](cold_start.md).
|
|
19
|
+
|
|
20
|
+
## Anti-drift
|
|
21
|
+
|
|
22
|
+
- Do not hand-copy runtime constants. RTC budget from `transport/rtc_connection_budget.mjs`. `EXPLORE_MAX_PER_SOURCE` mirrors `peer_pool.mjs` but stays local (importing `peer_pool` pulls fs) — `test/fidelity.test.mjs` asserts equality.
|
|
23
|
+
- Signaling source names (`DEFAULT_SIGNALING_SOURCES`) must be real provider ids (`lan` / `nostr` / `bt`). No `tracker` provider.
|
|
24
|
+
- New sim constants that shadow real ones need a matching assertion in `fidelity.test.mjs`.
|
|
25
|
+
|
|
26
|
+
## Determinism
|
|
27
|
+
|
|
28
|
+
- Seeded via `rng.mjs` (`createRng`). `runSimulation(...)` must be pure — `fidelity.test.mjs` asserts serial == parallel == batched snapshots. Use `simulationContext.now` (virtual clock, +60s/round); no wall-clock or unseeded RNG.
|
|
29
|
+
- Round state object is `simulationContext` (not `ctx`); `buildWorld` returns `{ simulationContext }`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入群 PoW 成本模型:算力预算限速 Sybil/洗白造号。
|
|
3
|
+
*/
|
|
4
|
+
import { expectedJoinPowHashes, powVoluntaryBonus } from '../governance/join_pow.mjs'
|
|
5
|
+
|
|
6
|
+
/** 每回合可消耗的期望哈希预算(相对 2^16 基准) */
|
|
7
|
+
const HASH_BUDGET_PER_ROUND = 65536
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
|
|
11
|
+
* @returns {number} 准入 floor bit
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePowFloorBits(admission) {
|
|
14
|
+
return Math.max(1, Math.floor(Number(admission?.powFloorBits ?? admission?.powDifficultyBits ?? 18)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {number} achievedBits 解达成 bit
|
|
19
|
+
* @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
|
|
20
|
+
* @returns {number} 自愿封顶信誉加成
|
|
21
|
+
*/
|
|
22
|
+
export function simPowVoluntaryBonus(achievedBits, admission) {
|
|
23
|
+
return powVoluntaryBonus(achievedBits, resolvePowFloorBits(admission), admission)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} difficultyBits PoW floor bit
|
|
28
|
+
* @returns {number} 造一个新身份期望回合成本
|
|
29
|
+
*/
|
|
30
|
+
export function roundsPerIdentity(difficultyBits) {
|
|
31
|
+
const expected = expectedJoinPowHashes(difficultyBits)
|
|
32
|
+
return Math.max(1, expected / HASH_BUDGET_PER_ROUND)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {number} requested 请求数量
|
|
37
|
+
* @param {number} difficultyBits PoW 难度
|
|
38
|
+
* @param {number} rounds 仿真总回合
|
|
39
|
+
* @returns {number} 预算内可生成的恶意身份数
|
|
40
|
+
*/
|
|
41
|
+
export function capMaliciousByPowBudget(requested, difficultyBits, rounds) {
|
|
42
|
+
const cost = roundsPerIdentity(difficultyBits)
|
|
43
|
+
const budget = Math.max(1, Math.floor(rounds / cost))
|
|
44
|
+
return Math.min(requested, budget)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {number} difficultyBits PoW 难度
|
|
49
|
+
* @param {number} rounds 回合数
|
|
50
|
+
* @returns {number} 诚实新人入群延迟惩罚 0..1(越高=越慢)
|
|
51
|
+
*/
|
|
52
|
+
export function honestJoinDelayPenalty(difficultyBits, rounds) {
|
|
53
|
+
const cost = roundsPerIdentity(difficultyBits)
|
|
54
|
+
const delay = cost / Math.max(1, rounds * 0.25)
|
|
55
|
+
return Math.min(1, delay)
|
|
56
|
+
}
|
package/sim/anomaly.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仿真器中的本地可逆异常隔离(映射生产 reputation_engine 纯函数)。
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
isQuarantinedPure,
|
|
6
|
+
observeBehaviorSamplePure,
|
|
7
|
+
} from '../reputation/engine.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
|
|
11
|
+
* @param {string} peerId 对端 nodeHash
|
|
12
|
+
* @param {number} sample 行为样本
|
|
13
|
+
* @param {number} now 当前时间
|
|
14
|
+
* @param {import('../reputation/tunables.json')} tunables reputation tunables
|
|
15
|
+
* @returns {boolean} 是否触发隔离
|
|
16
|
+
*/
|
|
17
|
+
export function simObservePeerBehavior(reputation, peerId, sample, now, tunables) {
|
|
18
|
+
return observeBehaviorSamplePure(reputation, peerId, sample, now, tunables).anomaly
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
|
|
23
|
+
* @param {string} peerId 对端 nodeHash
|
|
24
|
+
* @param {number} now 当前时间
|
|
25
|
+
* @returns {boolean} 是否隔离中
|
|
26
|
+
*/
|
|
27
|
+
export function simIsPeerQuarantined(reputation, peerId, now) {
|
|
28
|
+
return isQuarantinedPure(reputation, peerId, now)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} discoveryReach 发现可达 0..1
|
|
33
|
+
* @param {() => number} rng 随机源
|
|
34
|
+
* @returns {boolean} 攻击是否触达该观察者
|
|
35
|
+
*/
|
|
36
|
+
export function attackReachesObserver(discoveryReach, rng) {
|
|
37
|
+
const reach = Math.max(0, Math.min(1, discoveryReach))
|
|
38
|
+
return rng() < 0.15 + reach * 0.85
|
|
39
|
+
}
|
package/sim/apply.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按模块写回 tunables JSON。
|
|
3
|
+
*/
|
|
4
|
+
import { writeFile } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { normalizeBundle, sanitizeBundle } from './space.mjs'
|
|
9
|
+
|
|
10
|
+
/** @typedef {import('./tunables_bundle.mjs').TunablesBundle} TunablesBundle */
|
|
11
|
+
|
|
12
|
+
/** @type {Record<Exclude<keyof TunablesBundle, 'social'>, URL>} */
|
|
13
|
+
const PACKAGE_MODULE_URLS = {
|
|
14
|
+
reputation: new URL('../reputation/tunables.json', import.meta.url),
|
|
15
|
+
trustGraph: new URL('../trust_graph/tunables.json', import.meta.url),
|
|
16
|
+
mailbox: new URL('../mailbox/tunables.json', import.meta.url),
|
|
17
|
+
archive: new URL('../dag/tunables.json', import.meta.url),
|
|
18
|
+
admission: new URL('../governance/tunables.json', import.meta.url),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {TunablesBundle} bundle 完整 tunables
|
|
23
|
+
* @returns {TunablesBundle} 写盘前规整后的 bundle
|
|
24
|
+
*/
|
|
25
|
+
export function prepareBundleForApply(bundle) {
|
|
26
|
+
return sanitizeBundle(normalizeBundle(bundle))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} socialTunablesPath 外部 social shell 的 reputation_social.tunables.json 绝对或相对路径
|
|
31
|
+
* @returns {string} 规范化绝对路径
|
|
32
|
+
*/
|
|
33
|
+
export function resolveSocialTunablesPath(socialTunablesPath) {
|
|
34
|
+
const raw = String(socialTunablesPath ?? '').trim()
|
|
35
|
+
if (!raw) throw new Error('socialTunablesPath required to write social tunables')
|
|
36
|
+
return path.resolve(raw)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {keyof TunablesBundle} module 模块键
|
|
41
|
+
* @param {Record<string, unknown>} data JSON 内容
|
|
42
|
+
* @param {string} [socialTunablesPath] social 模块写入路径(module === 'social' 时必填)
|
|
43
|
+
* @returns {Promise<string>} 写入路径
|
|
44
|
+
*/
|
|
45
|
+
export async function writeModuleTunables(module, data, socialTunablesPath) {
|
|
46
|
+
const filePath = module === 'social'
|
|
47
|
+
? resolveSocialTunablesPath(socialTunablesPath)
|
|
48
|
+
: fileURLToPath(PACKAGE_MODULE_URLS[module])
|
|
49
|
+
await writeFile(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8')
|
|
50
|
+
return filePath
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {TunablesBundle} bundle 完整 tunables
|
|
55
|
+
* @param {{ socialTunablesPath: string }} paths 外部模块路径(social 写回 shell 侧 JSON)
|
|
56
|
+
* @returns {Promise<string[]>} 各模块 JSON 写入路径
|
|
57
|
+
*/
|
|
58
|
+
export async function applyTunablesBundle(bundle, paths) {
|
|
59
|
+
const socialPath = resolveSocialTunablesPath(paths?.socialTunablesPath)
|
|
60
|
+
const ready = prepareBundleForApply(bundle)
|
|
61
|
+
const written = []
|
|
62
|
+
written.push(await writeModuleTunables('reputation', ready.reputation))
|
|
63
|
+
written.push(await writeModuleTunables('trustGraph', ready.trustGraph))
|
|
64
|
+
written.push(await writeModuleTunables('social', ready.social, socialPath))
|
|
65
|
+
written.push(await writeModuleTunables('mailbox', ready.mailbox))
|
|
66
|
+
written.push(await writeModuleTunables('archive', ready.archive))
|
|
67
|
+
written.push(await writeModuleTunables('admission', ready.admission))
|
|
68
|
+
return written
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} [dir] sim 目录(默认本模块目录)
|
|
73
|
+
* @returns {string} results 子目录绝对路径
|
|
74
|
+
*/
|
|
75
|
+
export function resultsDirFromSim(dir = path.dirname(fileURLToPath(import.meta.url))) {
|
|
76
|
+
return path.join(dir, 'results')
|
|
77
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 攻击者可进化基因(与 space.mjs 采样器共用 latent 语义)。
|
|
3
|
+
*/
|
|
4
|
+
import { createRng } from './rng.mjs'
|
|
5
|
+
import { normalizeParam, sampleParam } from './space.mjs'
|
|
6
|
+
import { loadDefaultTunables } from './tunables_bundle.mjs'
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('./attacks.mjs').AttackKind} AttackKind */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* global: { activationRate: number, burstSize: number, targetBiasHighRep: number, eclipseFocus: number },
|
|
13
|
+
* byAttack: Partial<Record<AttackKind, { activationRate?: number, burstSize?: number, collusionAllyRate?: number, sleeperTurnFrac?: number, hintWeightMul?: number }>>,
|
|
14
|
+
* }} AttackGenome
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {import('./space.mjs').ParamSpec[]} */
|
|
18
|
+
const GLOBAL_ATTACK_SPECS = [
|
|
19
|
+
{ module: 'reputation', key: 'relayRepBump', kind: 'unit' }, // reuse kind; values remapped below
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const DEFAULT_GENOME = Object.freeze({
|
|
23
|
+
global: {
|
|
24
|
+
activationRate: 0.55,
|
|
25
|
+
burstSize: 4,
|
|
26
|
+
targetBiasHighRep: 0.35,
|
|
27
|
+
eclipseFocus: 0.65,
|
|
28
|
+
},
|
|
29
|
+
byAttack: {},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {() => number} rng 随机源
|
|
34
|
+
* @returns {AttackGenome} 随机攻击基因组
|
|
35
|
+
*/
|
|
36
|
+
export function randomAttackGenome(rng) {
|
|
37
|
+
return {
|
|
38
|
+
global: {
|
|
39
|
+
activationRate: 0.2 + rng() * 0.75,
|
|
40
|
+
burstSize: 1 + Math.floor(rng() * 8),
|
|
41
|
+
targetBiasHighRep: rng() * 0.9,
|
|
42
|
+
eclipseFocus: 0.3 + rng() * 0.65,
|
|
43
|
+
},
|
|
44
|
+
byAttack: {},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {AttackGenome} parent 父代
|
|
50
|
+
* @param {number} seed 种子
|
|
51
|
+
* @returns {AttackGenome} 变异子代
|
|
52
|
+
*/
|
|
53
|
+
export function mutateAttackGenome(parent, seed) {
|
|
54
|
+
const rng = createRng(seed)
|
|
55
|
+
const child = structuredClone(parent)
|
|
56
|
+
if (rng() < 0.4) child.global.activationRate = Math.max(0.05, Math.min(0.98, child.global.activationRate + (rng() - 0.5) * 0.2))
|
|
57
|
+
if (rng() < 0.35) child.global.burstSize = Math.max(1, Math.min(12, child.global.burstSize + Math.floor((rng() - 0.5) * 4)))
|
|
58
|
+
if (rng() < 0.3) child.global.targetBiasHighRep = Math.max(0, Math.min(1, child.global.targetBiasHighRep + (rng() - 0.5) * 0.25))
|
|
59
|
+
if (rng() < 0.3) child.global.eclipseFocus = Math.max(0.1, Math.min(1, child.global.eclipseFocus + (rng() - 0.5) * 0.25))
|
|
60
|
+
return child
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {AttackKind} attack 攻击类型
|
|
65
|
+
* @param {AttackGenome} genome 基因组
|
|
66
|
+
* @returns {{ activationRate: number, burstSize: number, targetBiasHighRep: number, eclipseFocus: number, collusionAllyRate: number, sleeperTurnFrac: number, hintWeightMul: number }} 有效参数
|
|
67
|
+
*/
|
|
68
|
+
export function resolveAttackParams(attack, genome) {
|
|
69
|
+
const g = genome.global
|
|
70
|
+
const row = genome.byAttack[attack] ?? {}
|
|
71
|
+
return {
|
|
72
|
+
activationRate: row.activationRate ?? g.activationRate,
|
|
73
|
+
burstSize: Math.max(1, Math.floor(row.burstSize ?? g.burstSize)),
|
|
74
|
+
targetBiasHighRep: g.targetBiasHighRep,
|
|
75
|
+
eclipseFocus: g.eclipseFocus,
|
|
76
|
+
collusionAllyRate: row.collusionAllyRate ?? 0.25,
|
|
77
|
+
sleeperTurnFrac: row.sleeperTurnFrac ?? 0.35,
|
|
78
|
+
hintWeightMul: row.hintWeightMul ?? 3,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {AttackGenome | undefined} genome 基因组
|
|
84
|
+
* @returns {AttackGenome} 默认补齐
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeAttackGenome(genome) {
|
|
87
|
+
if (!genome) return structuredClone(DEFAULT_GENOME)
|
|
88
|
+
return {
|
|
89
|
+
global: { ...DEFAULT_GENOME.global, ...genome.global },
|
|
90
|
+
byAttack: { ...genome.byAttack },
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 名人堂容量 */
|
|
95
|
+
export const ATTACK_HOF_SIZE = 6
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Array<{ genome: AttackGenome, fitness: number }>} hall 名人堂
|
|
99
|
+
* @param {AttackGenome} genome 候选
|
|
100
|
+
* @param {number} fitness 适应度
|
|
101
|
+
* @returns {Array<{ genome: AttackGenome, fitness: number }>} 更新后名人堂
|
|
102
|
+
*/
|
|
103
|
+
export function updateAttackHallOfFame(hall, genome, fitness) {
|
|
104
|
+
const next = [...hall, { genome: structuredClone(genome), fitness }]
|
|
105
|
+
next.sort((a, b) => b.fitness - a.fitness)
|
|
106
|
+
return next.slice(0, ATTACK_HOF_SIZE)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// silence unused import warning from eslint if any
|
|
110
|
+
void GLOBAL_ATTACK_SPECS
|
|
111
|
+
void loadDefaultTunables
|
|
112
|
+
void normalizeParam
|
|
113
|
+
void sampleParam
|