@agent-e/core 1.2.1 → 1.3.0

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/dist/index.mjs CHANGED
@@ -84,9 +84,9 @@ var DEFAULT_TICK_CONFIG = {
84
84
  var Observer = class {
85
85
  constructor(tickConfig) {
86
86
  this.previousMetrics = null;
87
- this.previousPrices = {};
87
+ this.previousPricesByCurrency = {};
88
88
  this.customMetricFns = {};
89
- this.anchorBaseline = null;
89
+ this.anchorBaselineByCurrency = {};
90
90
  this.tickConfig = { ...DEFAULT_TICK_CONFIG, ...tickConfig };
91
91
  }
92
92
  registerCustomMetric(name, fn) {
@@ -94,24 +94,25 @@ var Observer = class {
94
94
  }
95
95
  compute(state, recentEvents) {
96
96
  const tick = state.tick;
97
- const balances = Object.values(state.agentBalances);
98
97
  const roles = Object.values(state.agentRoles);
99
- const totalAgents = balances.length;
100
- let faucetVolume = 0;
101
- let sinkVolume = 0;
98
+ const totalAgents = Object.keys(state.agentBalances).length;
99
+ let productionAmount = 0;
100
+ const faucetVolumeByCurrency = {};
101
+ const sinkVolumeByCurrency = {};
102
102
  const tradeEvents = [];
103
103
  const roleChangeEvents = [];
104
104
  let churnCount = 0;
105
- let productionAmount = 0;
105
+ const defaultCurrency = state.currencies[0] ?? "default";
106
106
  for (const e of recentEvents) {
107
+ const curr = e.currency ?? defaultCurrency;
107
108
  switch (e.type) {
108
109
  case "mint":
109
110
  case "spawn":
110
- faucetVolume += e.amount ?? 0;
111
+ faucetVolumeByCurrency[curr] = (faucetVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
111
112
  break;
112
113
  case "burn":
113
114
  case "consume":
114
- sinkVolume += e.amount ?? 0;
115
+ sinkVolumeByCurrency[curr] = (sinkVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
115
116
  break;
116
117
  case "produce":
117
118
  productionAmount += e.amount ?? 1;
@@ -128,20 +129,68 @@ var Observer = class {
128
129
  break;
129
130
  }
130
131
  }
131
- const totalSupply = balances.reduce((s, b) => s + b, 0);
132
+ const currencies = state.currencies;
133
+ const totalSupplyByCurrency = {};
134
+ const balancesByCurrency = {};
135
+ for (const [_agentId, balances] of Object.entries(state.agentBalances)) {
136
+ for (const [curr, bal] of Object.entries(balances)) {
137
+ totalSupplyByCurrency[curr] = (totalSupplyByCurrency[curr] ?? 0) + bal;
138
+ if (!balancesByCurrency[curr]) balancesByCurrency[curr] = [];
139
+ balancesByCurrency[curr].push(bal);
140
+ }
141
+ }
142
+ const netFlowByCurrency = {};
143
+ const tapSinkRatioByCurrency = {};
144
+ const inflationRateByCurrency = {};
145
+ const velocityByCurrency = {};
146
+ for (const curr of currencies) {
147
+ const faucet = faucetVolumeByCurrency[curr] ?? 0;
148
+ const sink = sinkVolumeByCurrency[curr] ?? 0;
149
+ netFlowByCurrency[curr] = faucet - sink;
150
+ tapSinkRatioByCurrency[curr] = sink > 0 ? faucet / sink : faucet > 0 ? Infinity : 1;
151
+ const prevSupply = this.previousMetrics?.totalSupplyByCurrency?.[curr] ?? totalSupplyByCurrency[curr] ?? 0;
152
+ const currSupply = totalSupplyByCurrency[curr] ?? 0;
153
+ inflationRateByCurrency[curr] = prevSupply > 0 ? (currSupply - prevSupply) / prevSupply : 0;
154
+ const currTrades = tradeEvents.filter((e) => (e.currency ?? defaultCurrency) === curr);
155
+ velocityByCurrency[curr] = currSupply > 0 ? currTrades.length / currSupply : 0;
156
+ }
157
+ const giniCoefficientByCurrency = {};
158
+ const medianBalanceByCurrency = {};
159
+ const meanBalanceByCurrency = {};
160
+ const top10PctShareByCurrency = {};
161
+ const meanMedianDivergenceByCurrency = {};
162
+ for (const curr of currencies) {
163
+ const bals = balancesByCurrency[curr] ?? [];
164
+ const sorted = [...bals].sort((a, b) => a - b);
165
+ const supply = totalSupplyByCurrency[curr] ?? 0;
166
+ const count = sorted.length;
167
+ const median = computeMedian(sorted);
168
+ const mean = count > 0 ? supply / count : 0;
169
+ const top10Idx = Math.floor(count * 0.9);
170
+ const top10Sum = sorted.slice(top10Idx).reduce((s, b) => s + b, 0);
171
+ giniCoefficientByCurrency[curr] = computeGini(sorted);
172
+ medianBalanceByCurrency[curr] = median;
173
+ meanBalanceByCurrency[curr] = mean;
174
+ top10PctShareByCurrency[curr] = supply > 0 ? top10Sum / supply : 0;
175
+ meanMedianDivergenceByCurrency[curr] = median > 0 ? Math.abs(mean - median) / median : 0;
176
+ }
177
+ const avgOf = (rec) => {
178
+ const vals = Object.values(rec);
179
+ return vals.length > 0 ? vals.reduce((s, v) => s + v, 0) / vals.length : 0;
180
+ };
181
+ const totalSupply = Object.values(totalSupplyByCurrency).reduce((s, v) => s + v, 0);
182
+ const faucetVolume = Object.values(faucetVolumeByCurrency).reduce((s, v) => s + v, 0);
183
+ const sinkVolume = Object.values(sinkVolumeByCurrency).reduce((s, v) => s + v, 0);
132
184
  const netFlow = faucetVolume - sinkVolume;
133
185
  const tapSinkRatio = sinkVolume > 0 ? faucetVolume / sinkVolume : faucetVolume > 0 ? Infinity : 1;
134
- const prevSupply = this.previousMetrics?.totalSupply ?? totalSupply;
135
- const inflationRate = prevSupply > 0 ? (totalSupply - prevSupply) / prevSupply : 0;
136
186
  const velocity = totalSupply > 0 ? tradeEvents.length / totalSupply : 0;
137
- const sortedBalances = [...balances].sort((a, b) => a - b);
187
+ const prevTotalSupply = this.previousMetrics?.totalSupply ?? totalSupply;
188
+ const inflationRate = prevTotalSupply > 0 ? (totalSupply - prevTotalSupply) / prevTotalSupply : 0;
189
+ const giniCoefficient = avgOf(giniCoefficientByCurrency);
190
+ const medianBalance = avgOf(medianBalanceByCurrency);
138
191
  const meanBalance = totalAgents > 0 ? totalSupply / totalAgents : 0;
139
- const medianBalance = computeMedian(sortedBalances);
140
- const top10Idx = Math.floor(totalAgents * 0.9);
141
- const top10Sum = sortedBalances.slice(top10Idx).reduce((s, b) => s + b, 0);
142
- const top10PctShare = totalSupply > 0 ? top10Sum / totalSupply : 0;
143
- const giniCoefficient = computeGini(sortedBalances);
144
- const meanMedianDivergence = medianBalance > 0 ? Math.abs(meanBalance - medianBalance) / medianBalance : 0;
192
+ const top10PctShare = avgOf(top10PctShareByCurrency);
193
+ const meanMedianDivergence = avgOf(meanMedianDivergenceByCurrency);
145
194
  const populationByRole = {};
146
195
  const roleShares = {};
147
196
  for (const role of roles) {
@@ -156,15 +205,25 @@ var Observer = class {
156
205
  churnByRole[role] = (churnByRole[role] ?? 0) + 1;
157
206
  }
158
207
  const churnRate = churnCount / Math.max(1, totalAgents);
159
- const prices = { ...state.marketPrices };
160
- const priceVolatility = {};
161
- for (const [resource, price] of Object.entries(prices)) {
162
- const prev = this.previousPrices[resource] ?? price;
163
- priceVolatility[resource] = prev > 0 ? Math.abs(price - prev) / prev : 0;
164
- }
165
- this.previousPrices = { ...prices };
166
- const priceValues = Object.values(prices);
167
- const priceIndex = priceValues.length > 0 ? priceValues.reduce((s, p) => s + p, 0) / priceValues.length : 0;
208
+ const pricesByCurrency = {};
209
+ const priceVolatilityByCurrency = {};
210
+ const priceIndexByCurrency = {};
211
+ for (const [curr, resourcePrices] of Object.entries(state.marketPrices)) {
212
+ pricesByCurrency[curr] = { ...resourcePrices };
213
+ const pricePrev = this.previousPricesByCurrency?.[curr] ?? {};
214
+ const volMap = {};
215
+ for (const [resource, price] of Object.entries(resourcePrices)) {
216
+ const prev = pricePrev[resource] ?? price;
217
+ volMap[resource] = prev > 0 ? Math.abs(price - prev) / prev : 0;
218
+ }
219
+ priceVolatilityByCurrency[curr] = volMap;
220
+ const pVals = Object.values(resourcePrices);
221
+ priceIndexByCurrency[curr] = pVals.length > 0 ? pVals.reduce((s, p) => s + p, 0) / pVals.length : 0;
222
+ }
223
+ this.previousPricesByCurrency = JSON.parse(JSON.stringify(pricesByCurrency));
224
+ const prices = pricesByCurrency[defaultCurrency] ?? {};
225
+ const priceVolatility = priceVolatilityByCurrency[defaultCurrency] ?? {};
226
+ const priceIndex = priceIndexByCurrency[defaultCurrency] ?? 0;
168
227
  const supplyByResource = {};
169
228
  for (const inv of Object.values(state.agentInventories)) {
170
229
  for (const [resource, qty] of Object.entries(inv)) {
@@ -196,63 +255,85 @@ var Observer = class {
196
255
  const avgSatisfaction = satisfactions.length > 0 ? satisfactions.reduce((s, v) => s + v, 0) / satisfactions.length : 80;
197
256
  const blockedAgentCount = satisfactions.filter((s) => s < 20).length;
198
257
  const timeToValue = totalAgents > 0 ? blockedAgentCount / totalAgents * 100 : 0;
199
- const poolSizes = { ...state.poolSizes ?? {} };
200
- if (!this.anchorBaseline && tick === 1 && totalSupply > 0) {
201
- this.anchorBaseline = {
202
- currencyPerPeriod: totalSupply / Math.max(1, totalAgents),
203
- itemsPerCurrency: priceIndex > 0 ? 1 / priceIndex : 0
204
- };
258
+ const poolSizesByCurrency = {};
259
+ const poolSizesAggregate = {};
260
+ if (state.poolSizes) {
261
+ for (const [pool, currencyAmounts] of Object.entries(state.poolSizes)) {
262
+ poolSizesByCurrency[pool] = { ...currencyAmounts };
263
+ poolSizesAggregate[pool] = Object.values(currencyAmounts).reduce((s, v) => s + v, 0);
264
+ }
265
+ }
266
+ const anchorRatioDriftByCurrency = {};
267
+ if (tick === 1) {
268
+ for (const curr of currencies) {
269
+ const supply = totalSupplyByCurrency[curr] ?? 0;
270
+ if (supply > 0) {
271
+ this.anchorBaselineByCurrency[curr] = {
272
+ currencyPerPeriod: supply / Math.max(1, totalAgents),
273
+ itemsPerCurrency: (priceIndexByCurrency[curr] ?? 0) > 0 ? 1 / priceIndexByCurrency[curr] : 0
274
+ };
275
+ }
276
+ }
277
+ }
278
+ for (const curr of currencies) {
279
+ const baseline = this.anchorBaselineByCurrency[curr];
280
+ if (baseline && totalAgents > 0) {
281
+ const currentCPP = (totalSupplyByCurrency[curr] ?? 0) / totalAgents;
282
+ anchorRatioDriftByCurrency[curr] = baseline.currencyPerPeriod > 0 ? (currentCPP - baseline.currencyPerPeriod) / baseline.currencyPerPeriod : 0;
283
+ } else {
284
+ anchorRatioDriftByCurrency[curr] = 0;
285
+ }
205
286
  }
206
- let anchorRatioDrift = 0;
207
- if (this.anchorBaseline && totalAgents > 0) {
208
- const currentCurrencyPerPeriod = totalSupply / totalAgents;
209
- anchorRatioDrift = this.anchorBaseline.currencyPerPeriod > 0 ? (currentCurrencyPerPeriod - this.anchorBaseline.currencyPerPeriod) / this.anchorBaseline.currencyPerPeriod : 0;
210
- }
211
- let arbitrageIndex = 0;
212
- const priceKeys = Object.keys(prices).filter((k) => prices[k] > 0);
213
- if (priceKeys.length >= 2) {
214
- let pairCount = 0;
215
- let totalDivergence = 0;
216
- for (let i = 0; i < priceKeys.length; i++) {
217
- for (let j = i + 1; j < priceKeys.length; j++) {
218
- const pA = prices[priceKeys[i]];
219
- const pB = prices[priceKeys[j]];
220
- let ratio = pA / pB;
221
- ratio = Math.max(1e-3, Math.min(1e3, ratio));
222
- totalDivergence += Math.abs(Math.log(ratio));
223
- pairCount++;
287
+ const anchorRatioDrift = avgOf(anchorRatioDriftByCurrency);
288
+ const arbitrageIndexByCurrency = {};
289
+ for (const curr of currencies) {
290
+ const cPrices = pricesByCurrency[curr] ?? {};
291
+ const priceKeys = Object.keys(cPrices).filter((k) => cPrices[k] > 0);
292
+ if (priceKeys.length >= 2) {
293
+ let pairCount = 0;
294
+ let totalDivergence = 0;
295
+ for (let i = 0; i < priceKeys.length; i++) {
296
+ for (let j = i + 1; j < priceKeys.length; j++) {
297
+ const pA = cPrices[priceKeys[i]];
298
+ const pB = cPrices[priceKeys[j]];
299
+ let ratio = pA / pB;
300
+ ratio = Math.max(1e-3, Math.min(1e3, ratio));
301
+ totalDivergence += Math.abs(Math.log(ratio));
302
+ pairCount++;
303
+ }
224
304
  }
305
+ arbitrageIndexByCurrency[curr] = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
306
+ } else {
307
+ arbitrageIndexByCurrency[curr] = 0;
225
308
  }
226
- arbitrageIndex = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
227
309
  }
310
+ const arbitrageIndex = avgOf(arbitrageIndexByCurrency);
228
311
  const contentDropEvents = recentEvents.filter(
229
312
  (e) => e.metadata?.["contentDrop"] === true
230
313
  );
231
314
  const contentDropAge = contentDropEvents.length > 0 ? tick - Math.max(...contentDropEvents.map((e) => e.timestamp)) : (this.previousMetrics?.contentDropAge ?? 0) + 1;
232
- let giftTrades = 0;
233
- if (tradeEvents.length > 0) {
234
- for (const e of tradeEvents) {
235
- const marketPrice = prices[e.resource ?? ""] ?? 0;
315
+ const giftTradeRatioByCurrency = {};
316
+ const disposalTradeRatioByCurrency = {};
317
+ for (const curr of currencies) {
318
+ const currTrades = tradeEvents.filter((e) => (e.currency ?? defaultCurrency) === curr);
319
+ const cPrices = pricesByCurrency[curr] ?? {};
320
+ let gifts = 0;
321
+ let disposals = 0;
322
+ for (const e of currTrades) {
323
+ const marketPrice = cPrices[e.resource ?? ""] ?? 0;
236
324
  const tradePrice = e.price ?? 0;
237
- if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) {
238
- giftTrades++;
239
- }
240
- }
241
- }
242
- const giftTradeRatio = tradeEvents.length > 0 ? giftTrades / tradeEvents.length : 0;
243
- let disposalTrades = 0;
244
- if (tradeEvents.length > 0) {
245
- for (const e of tradeEvents) {
325
+ if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) gifts++;
246
326
  if (e.from && e.resource) {
247
327
  const sellerInv = state.agentInventories[e.from]?.[e.resource] ?? 0;
248
328
  const avgInv = (supplyByResource[e.resource] ?? 0) / Math.max(1, totalAgents);
249
- if (sellerInv > avgInv * 3) {
250
- disposalTrades++;
251
- }
329
+ if (sellerInv > avgInv * 3) disposals++;
252
330
  }
253
331
  }
332
+ giftTradeRatioByCurrency[curr] = currTrades.length > 0 ? gifts / currTrades.length : 0;
333
+ disposalTradeRatioByCurrency[curr] = currTrades.length > 0 ? disposals / currTrades.length : 0;
254
334
  }
255
- const disposalTradeRatio = tradeEvents.length > 0 ? disposalTrades / tradeEvents.length : 0;
335
+ const giftTradeRatio = avgOf(giftTradeRatioByCurrency);
336
+ const disposalTradeRatio = avgOf(disposalTradeRatioByCurrency);
256
337
  const custom = {};
257
338
  for (const [name, fn] of Object.entries(this.customMetricFns)) {
258
339
  try {
@@ -264,10 +345,57 @@ var Observer = class {
264
345
  const metrics = {
265
346
  tick,
266
347
  timestamp: Date.now(),
348
+ currencies,
349
+ // Per-currency
350
+ totalSupplyByCurrency,
351
+ netFlowByCurrency,
352
+ velocityByCurrency,
353
+ inflationRateByCurrency,
354
+ faucetVolumeByCurrency,
355
+ sinkVolumeByCurrency,
356
+ tapSinkRatioByCurrency,
357
+ anchorRatioDriftByCurrency,
358
+ giniCoefficientByCurrency,
359
+ medianBalanceByCurrency,
360
+ meanBalanceByCurrency,
361
+ top10PctShareByCurrency,
362
+ meanMedianDivergenceByCurrency,
363
+ priceIndexByCurrency,
364
+ pricesByCurrency,
365
+ priceVolatilityByCurrency,
366
+ poolSizesByCurrency,
367
+ extractionRatioByCurrency: {},
368
+ newUserDependencyByCurrency: {},
369
+ currencyInsulationByCurrency: {},
370
+ arbitrageIndexByCurrency,
371
+ giftTradeRatioByCurrency,
372
+ disposalTradeRatioByCurrency,
373
+ // Aggregates
267
374
  totalSupply,
268
375
  netFlow,
269
376
  velocity,
270
377
  inflationRate,
378
+ faucetVolume,
379
+ sinkVolume,
380
+ tapSinkRatio,
381
+ anchorRatioDrift,
382
+ giniCoefficient,
383
+ medianBalance,
384
+ meanBalance,
385
+ top10PctShare,
386
+ meanMedianDivergence,
387
+ priceIndex,
388
+ prices,
389
+ priceVolatility,
390
+ poolSizes: poolSizesAggregate,
391
+ extractionRatio: NaN,
392
+ newUserDependency: NaN,
393
+ smokeTestRatio: NaN,
394
+ currencyInsulation: NaN,
395
+ arbitrageIndex,
396
+ giftTradeRatio,
397
+ disposalTradeRatio,
398
+ // Unchanged
271
399
  populationByRole,
272
400
  roleShares,
273
401
  totalAgents,
@@ -275,38 +403,18 @@ var Observer = class {
275
403
  churnByRole,
276
404
  personaDistribution: {},
277
405
  // populated by PersonaTracker
278
- giniCoefficient,
279
- medianBalance,
280
- meanBalance,
281
- top10PctShare,
282
- meanMedianDivergence,
283
- priceIndex,
284
406
  productionIndex,
285
407
  capacityUsage,
286
- prices,
287
- priceVolatility,
288
408
  supplyByResource,
289
409
  demandSignals,
290
410
  pinchPoints,
291
411
  avgSatisfaction,
292
412
  blockedAgentCount,
293
413
  timeToValue,
294
- faucetVolume,
295
- sinkVolume,
296
- tapSinkRatio,
297
- poolSizes,
298
- anchorRatioDrift,
299
- extractionRatio: NaN,
300
- newUserDependency: NaN,
301
- smokeTestRatio: NaN,
302
- currencyInsulation: NaN,
303
414
  sharkToothPeaks: this.previousMetrics?.sharkToothPeaks ?? [],
304
415
  sharkToothValleys: this.previousMetrics?.sharkToothValleys ?? [],
305
416
  eventCompletionRate: NaN,
306
- arbitrageIndex,
307
417
  contentDropAge,
308
- giftTradeRatio,
309
- disposalTradeRatio,
310
418
  custom
311
419
  };
312
420
  this.previousMetrics = metrics;
@@ -380,48 +488,75 @@ function emptyMetrics(tick = 0) {
380
488
  return {
381
489
  tick,
382
490
  timestamp: Date.now(),
491
+ currencies: [],
492
+ // Per-currency
493
+ totalSupplyByCurrency: {},
494
+ netFlowByCurrency: {},
495
+ velocityByCurrency: {},
496
+ inflationRateByCurrency: {},
497
+ faucetVolumeByCurrency: {},
498
+ sinkVolumeByCurrency: {},
499
+ tapSinkRatioByCurrency: {},
500
+ anchorRatioDriftByCurrency: {},
501
+ giniCoefficientByCurrency: {},
502
+ medianBalanceByCurrency: {},
503
+ meanBalanceByCurrency: {},
504
+ top10PctShareByCurrency: {},
505
+ meanMedianDivergenceByCurrency: {},
506
+ priceIndexByCurrency: {},
507
+ pricesByCurrency: {},
508
+ priceVolatilityByCurrency: {},
509
+ poolSizesByCurrency: {},
510
+ extractionRatioByCurrency: {},
511
+ newUserDependencyByCurrency: {},
512
+ currencyInsulationByCurrency: {},
513
+ arbitrageIndexByCurrency: {},
514
+ giftTradeRatioByCurrency: {},
515
+ disposalTradeRatioByCurrency: {},
516
+ // Aggregates
383
517
  totalSupply: 0,
384
518
  netFlow: 0,
385
519
  velocity: 0,
386
520
  inflationRate: 0,
387
- populationByRole: {},
388
- roleShares: {},
389
- totalAgents: 0,
390
- churnRate: 0,
391
- churnByRole: {},
392
- personaDistribution: {},
521
+ faucetVolume: 0,
522
+ sinkVolume: 0,
523
+ tapSinkRatio: 1,
524
+ anchorRatioDrift: 0,
393
525
  giniCoefficient: 0,
394
526
  medianBalance: 0,
395
527
  meanBalance: 0,
396
528
  top10PctShare: 0,
397
529
  meanMedianDivergence: 0,
398
530
  priceIndex: 0,
399
- productionIndex: 0,
400
- capacityUsage: 0,
401
531
  prices: {},
402
532
  priceVolatility: {},
533
+ poolSizes: {},
534
+ extractionRatio: NaN,
535
+ newUserDependency: NaN,
536
+ smokeTestRatio: NaN,
537
+ currencyInsulation: NaN,
538
+ arbitrageIndex: 0,
539
+ giftTradeRatio: 0,
540
+ disposalTradeRatio: 0,
541
+ // Unchanged
542
+ populationByRole: {},
543
+ roleShares: {},
544
+ totalAgents: 0,
545
+ churnRate: 0,
546
+ churnByRole: {},
547
+ personaDistribution: {},
548
+ productionIndex: 0,
549
+ capacityUsage: 0,
403
550
  supplyByResource: {},
404
551
  demandSignals: {},
405
552
  pinchPoints: {},
406
553
  avgSatisfaction: 100,
407
554
  blockedAgentCount: 0,
408
555
  timeToValue: 0,
409
- faucetVolume: 0,
410
- sinkVolume: 0,
411
- tapSinkRatio: 1,
412
- poolSizes: {},
413
- anchorRatioDrift: 0,
414
- extractionRatio: NaN,
415
- newUserDependency: NaN,
416
- smokeTestRatio: NaN,
417
- currencyInsulation: NaN,
418
556
  sharkToothPeaks: [],
419
557
  sharkToothValleys: [],
420
558
  eventCompletionRate: NaN,
421
- arbitrageIndex: 0,
422
559
  contentDropAge: 0,
423
- giftTradeRatio: 0,
424
- disposalTradeRatio: 0,
425
560
  custom: {}
426
561
  };
427
562
  }
@@ -889,36 +1024,42 @@ var P12_OnePrimaryFaucet = {
889
1024
  category: "currency",
890
1025
  description: "Multiple independent currency sources (gathering + production + quests) each creating currency causes uncontrolled inflation. One clear primary faucet makes the economy predictable and auditable.",
891
1026
  check(metrics, thresholds) {
892
- const { netFlow, faucetVolume, sinkVolume } = metrics;
893
- if (netFlow > thresholds.netFlowWarnThreshold) {
894
- return {
895
- violated: true,
896
- severity: 5,
897
- evidence: { netFlow, faucetVolume, sinkVolume },
898
- suggestedAction: {
899
- parameter: "productionCost",
900
- direction: "increase",
901
- magnitude: 0.15,
902
- reasoning: `Net flow +${netFlow.toFixed(1)}/tick. Inflationary. Increase production cost (primary sink) to balance faucet output.`
903
- },
904
- confidence: 0.8,
905
- estimatedLag: 8
906
- };
907
- }
908
- if (netFlow < -thresholds.netFlowWarnThreshold) {
909
- return {
910
- violated: true,
911
- severity: 4,
912
- evidence: { netFlow, faucetVolume, sinkVolume },
913
- suggestedAction: {
914
- parameter: "productionCost",
915
- direction: "decrease",
916
- magnitude: 0.15,
917
- reasoning: `Net flow ${netFlow.toFixed(1)}/tick. Deflationary. Decrease production cost to ease sink pressure.`
918
- },
919
- confidence: 0.8,
920
- estimatedLag: 8
921
- };
1027
+ for (const curr of metrics.currencies) {
1028
+ const netFlow = metrics.netFlowByCurrency[curr] ?? 0;
1029
+ const faucetVolume = metrics.faucetVolumeByCurrency[curr] ?? 0;
1030
+ const sinkVolume = metrics.sinkVolumeByCurrency[curr] ?? 0;
1031
+ if (netFlow > thresholds.netFlowWarnThreshold) {
1032
+ return {
1033
+ violated: true,
1034
+ severity: 5,
1035
+ evidence: { currency: curr, netFlow, faucetVolume, sinkVolume },
1036
+ suggestedAction: {
1037
+ parameter: "productionCost",
1038
+ direction: "increase",
1039
+ currency: curr,
1040
+ magnitude: 0.15,
1041
+ reasoning: `[${curr}] Net flow +${netFlow.toFixed(1)}/tick. Inflationary. Increase production cost (primary sink) to balance faucet output.`
1042
+ },
1043
+ confidence: 0.8,
1044
+ estimatedLag: 8
1045
+ };
1046
+ }
1047
+ if (netFlow < -thresholds.netFlowWarnThreshold) {
1048
+ return {
1049
+ violated: true,
1050
+ severity: 4,
1051
+ evidence: { currency: curr, netFlow, faucetVolume, sinkVolume },
1052
+ suggestedAction: {
1053
+ parameter: "productionCost",
1054
+ direction: "decrease",
1055
+ currency: curr,
1056
+ magnitude: 0.15,
1057
+ reasoning: `[${curr}] Net flow ${netFlow.toFixed(1)}/tick. Deflationary. Decrease production cost to ease sink pressure.`
1058
+ },
1059
+ confidence: 0.8,
1060
+ estimatedLag: 8
1061
+ };
1062
+ }
922
1063
  }
923
1064
  return { violated: false };
924
1065
  }
@@ -929,28 +1070,30 @@ var P13_PotsAreZeroSumAndSelfRegulate = {
929
1070
  category: "currency",
930
1071
  description: "Competitive pot math: winRate \xD7 multiplier > (1 - houseCut) drains the pot. At 65% win rate, multiplier must be \u2264 1.38. We use 1.5 for slight surplus buffer.",
931
1072
  check(metrics, thresholds) {
932
- const { poolSizes, populationByRole } = metrics;
933
- const totalAgents = metrics.totalAgents;
1073
+ const { populationByRole } = metrics;
934
1074
  const roleEntries = Object.entries(populationByRole).sort((a, b) => b[1] - a[1]);
935
- const dominantRole = roleEntries[0]?.[0];
936
1075
  const dominantCount = roleEntries[0]?.[1] ?? 0;
937
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
938
- if (dominantCount > 5 && poolSize < 50) {
939
- const { poolWinRate, poolHouseCut } = thresholds;
940
- const maxSustainableMultiplier = (1 - poolHouseCut) / poolWinRate;
941
- return {
942
- violated: true,
943
- severity: 7,
944
- evidence: { pool: poolName, poolSize, participants: dominantCount, maxSustainableMultiplier },
945
- suggestedAction: {
946
- parameter: "rewardRate",
947
- direction: "decrease",
948
- magnitude: 0.15,
949
- reasoning: `${poolName} pool at ${poolSize.toFixed(0)} currency with ${dominantCount} active participants. Sustainable multiplier \u2264 ${maxSustainableMultiplier.toFixed(2)}. Reduce reward multiplier to prevent pool drain.`
950
- },
951
- confidence: 0.85,
952
- estimatedLag: 3
953
- };
1076
+ for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1077
+ for (const curr of metrics.currencies) {
1078
+ const poolSize = currencyAmounts[curr] ?? 0;
1079
+ if (dominantCount > 5 && poolSize < 50) {
1080
+ const { poolWinRate, poolHouseCut } = thresholds;
1081
+ const maxSustainableMultiplier = (1 - poolHouseCut) / poolWinRate;
1082
+ return {
1083
+ violated: true,
1084
+ severity: 7,
1085
+ evidence: { currency: curr, pool: poolName, poolSize, participants: dominantCount, maxSustainableMultiplier },
1086
+ suggestedAction: {
1087
+ parameter: "rewardRate",
1088
+ direction: "decrease",
1089
+ currency: curr,
1090
+ magnitude: 0.15,
1091
+ reasoning: `[${curr}] ${poolName} pool at ${poolSize.toFixed(0)} currency with ${dominantCount} active participants. Sustainable multiplier \u2264 ${maxSustainableMultiplier.toFixed(2)}. Reduce reward multiplier to prevent pool drain.`
1092
+ },
1093
+ confidence: 0.85,
1094
+ estimatedLag: 3
1095
+ };
1096
+ }
954
1097
  }
955
1098
  }
956
1099
  return { violated: false };
@@ -962,22 +1105,27 @@ var P14_TrackActualInjection = {
962
1105
  category: "currency",
963
1106
  description: 'Counting resource gathering as "currency injected" is misleading. Currency enters through faucet mechanisms (spawning, rewards). Fake metrics break every downstream decision.',
964
1107
  check(metrics, _thresholds) {
965
- const { faucetVolume, netFlow, totalSupply } = metrics;
966
- const supplyGrowthRate = Math.abs(netFlow) / Math.max(1, totalSupply);
967
- if (supplyGrowthRate > 0.1) {
968
- return {
969
- violated: true,
970
- severity: 4,
971
- evidence: { faucetVolume, netFlow, supplyGrowthRate },
972
- suggestedAction: {
973
- parameter: "yieldRate",
974
- direction: "decrease",
975
- magnitude: 0.1,
976
- reasoning: `Supply growing at ${(supplyGrowthRate * 100).toFixed(1)}%/tick. Verify currency injection tracking. Resources should not create currency directly.`
977
- },
978
- confidence: 0.55,
979
- estimatedLag: 5
980
- };
1108
+ for (const curr of metrics.currencies) {
1109
+ const faucetVolume = metrics.faucetVolumeByCurrency[curr] ?? 0;
1110
+ const netFlow = metrics.netFlowByCurrency[curr] ?? 0;
1111
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1112
+ const supplyGrowthRate = Math.abs(netFlow) / Math.max(1, totalSupply);
1113
+ if (supplyGrowthRate > 0.1) {
1114
+ return {
1115
+ violated: true,
1116
+ severity: 4,
1117
+ evidence: { currency: curr, faucetVolume, netFlow, supplyGrowthRate },
1118
+ suggestedAction: {
1119
+ parameter: "yieldRate",
1120
+ direction: "decrease",
1121
+ currency: curr,
1122
+ magnitude: 0.1,
1123
+ reasoning: `[${curr}] Supply growing at ${(supplyGrowthRate * 100).toFixed(1)}%/tick. Verify currency injection tracking. Resources should not create currency directly.`
1124
+ },
1125
+ confidence: 0.55,
1126
+ estimatedLag: 5
1127
+ };
1128
+ }
981
1129
  }
982
1130
  return { violated: false };
983
1131
  }
@@ -988,24 +1136,28 @@ var P15_PoolsNeedCapAndDecay = {
988
1136
  category: "currency",
989
1137
  description: "Any pool (bank, reward pool) without a cap accumulates infinitely. A pool at 42% of total supply means 42% of the economy is frozen. Cap at 5%, decay at 2%/tick.",
990
1138
  check(metrics, thresholds) {
991
- const { poolSizes, totalSupply } = metrics;
992
1139
  const { poolCapPercent } = thresholds;
993
- for (const [pool, size] of Object.entries(poolSizes)) {
994
- const shareOfSupply = size / Math.max(1, totalSupply);
995
- if (shareOfSupply > poolCapPercent * 2) {
996
- return {
997
- violated: true,
998
- severity: 6,
999
- evidence: { pool, size, shareOfSupply, cap: poolCapPercent },
1000
- suggestedAction: {
1001
- parameter: "transactionFee",
1002
- direction: "decrease",
1003
- magnitude: 0.1,
1004
- reasoning: `${pool} pool at ${(shareOfSupply * 100).toFixed(1)}% of supply (cap: ${(poolCapPercent * 100).toFixed(0)}%). Currency frozen. Lower fees to encourage circulation over accumulation.`
1005
- },
1006
- confidence: 0.85,
1007
- estimatedLag: 5
1008
- };
1140
+ for (const [pool, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1141
+ for (const curr of metrics.currencies) {
1142
+ const size = currencyAmounts[curr] ?? 0;
1143
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1144
+ const shareOfSupply = size / Math.max(1, totalSupply);
1145
+ if (shareOfSupply > poolCapPercent * 2) {
1146
+ return {
1147
+ violated: true,
1148
+ severity: 6,
1149
+ evidence: { currency: curr, pool, size, shareOfSupply, cap: poolCapPercent },
1150
+ suggestedAction: {
1151
+ parameter: "transactionFee",
1152
+ direction: "decrease",
1153
+ currency: curr,
1154
+ magnitude: 0.1,
1155
+ reasoning: `[${curr}] ${pool} pool at ${(shareOfSupply * 100).toFixed(1)}% of supply (cap: ${(poolCapPercent * 100).toFixed(0)}%). Currency frozen. Lower fees to encourage circulation over accumulation.`
1156
+ },
1157
+ confidence: 0.85,
1158
+ estimatedLag: 5
1159
+ };
1160
+ }
1009
1161
  }
1010
1162
  }
1011
1163
  return { violated: false };
@@ -1017,23 +1169,27 @@ var P16_WithdrawalPenaltyScales = {
1017
1169
  category: "currency",
1018
1170
  description: "A 50-tick lock period with a penalty calculated as /100 means agents can exit after 1 tick and keep 99% of accrued yield. Penalty must scale linearly: (1 - ticksStaked/lockDuration) \xD7 yield.",
1019
1171
  check(metrics, _thresholds) {
1020
- const { poolSizes, totalSupply } = metrics;
1021
- const stakedEstimate = totalSupply * 0.15;
1022
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
1023
- if (poolSize < 10 && stakedEstimate > 100) {
1024
- return {
1025
- violated: true,
1026
- severity: 3,
1027
- evidence: { pool: poolName, poolSize, estimatedStaked: stakedEstimate },
1028
- suggestedAction: {
1029
- parameter: "transactionFee",
1030
- direction: "increase",
1031
- magnitude: 0.05,
1032
- reasoning: `${poolName} pool depleted while significant currency should be locked. Early withdrawals may be draining the pool. Ensure withdrawal penalty scales with lock duration.`
1033
- },
1034
- confidence: 0.45,
1035
- estimatedLag: 10
1036
- };
1172
+ for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1173
+ for (const curr of metrics.currencies) {
1174
+ const poolSize = currencyAmounts[curr] ?? 0;
1175
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1176
+ const stakedEstimate = totalSupply * 0.15;
1177
+ if (poolSize < 10 && stakedEstimate > 100) {
1178
+ return {
1179
+ violated: true,
1180
+ severity: 3,
1181
+ evidence: { currency: curr, pool: poolName, poolSize, estimatedStaked: stakedEstimate },
1182
+ suggestedAction: {
1183
+ parameter: "transactionFee",
1184
+ direction: "increase",
1185
+ currency: curr,
1186
+ magnitude: 0.05,
1187
+ reasoning: `[${curr}] ${poolName} pool depleted while significant currency should be locked. Early withdrawals may be draining the pool. Ensure withdrawal penalty scales with lock duration.`
1188
+ },
1189
+ confidence: 0.45,
1190
+ estimatedLag: 10
1191
+ };
1192
+ }
1037
1193
  }
1038
1194
  }
1039
1195
  return { violated: false };
@@ -1045,22 +1201,27 @@ var P32_VelocityAboveSupply = {
1045
1201
  category: "currency",
1046
1202
  description: "Low transactions despite adequate supply means liquidity is trapped. High supply with low velocity = stagnation, not abundance.",
1047
1203
  check(metrics, _thresholds) {
1048
- const { velocity, totalSupply, supplyByResource } = metrics;
1204
+ const { supplyByResource } = metrics;
1049
1205
  const totalResources = Object.values(supplyByResource).reduce((s, v) => s + v, 0);
1050
- if (velocity < 3 && totalSupply > 100 && totalResources > 20) {
1051
- return {
1052
- violated: true,
1053
- severity: 4,
1054
- evidence: { velocity, totalSupply, totalResources },
1055
- suggestedAction: {
1056
- parameter: "transactionFee",
1057
- direction: "decrease",
1058
- magnitude: 0.2,
1059
- reasoning: `Velocity ${velocity}/t with ${totalResources} resources in system. Economy stagnant despite available supply. Lower trading friction.`
1060
- },
1061
- confidence: 0.75,
1062
- estimatedLag: 5
1063
- };
1206
+ for (const curr of metrics.currencies) {
1207
+ const velocity = metrics.velocityByCurrency[curr] ?? 0;
1208
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1209
+ if (velocity < 3 && totalSupply > 100 && totalResources > 20) {
1210
+ return {
1211
+ violated: true,
1212
+ severity: 4,
1213
+ evidence: { currency: curr, velocity, totalSupply, totalResources },
1214
+ suggestedAction: {
1215
+ parameter: "transactionFee",
1216
+ direction: "decrease",
1217
+ currency: curr,
1218
+ magnitude: 0.2,
1219
+ reasoning: `[${curr}] Velocity ${velocity}/t with ${totalResources} resources in system. Economy stagnant despite available supply. Lower trading friction.`
1220
+ },
1221
+ confidence: 0.75,
1222
+ estimatedLag: 5
1223
+ };
1224
+ }
1064
1225
  }
1065
1226
  return { violated: false };
1066
1227
  }
@@ -1071,32 +1232,38 @@ var P58_NoNaturalNumeraire = {
1071
1232
  category: "currency",
1072
1233
  description: "No single commodity naturally stabilizes as currency in barter-heavy economies. Multiple items rotate as de facto units of account, but none locks in. If a num\xE9raire is needed, design and enforce it \u2014 emergence alone will not produce one.",
1073
1234
  check(metrics, _thresholds) {
1074
- const { prices, velocity, totalSupply } = metrics;
1075
- const priceValues = Object.values(prices).filter((p) => p > 0);
1076
- if (priceValues.length < 3) return { violated: false };
1077
- const mean = priceValues.reduce((s, p) => s + p, 0) / priceValues.length;
1078
- const coeffOfVariation = mean > 0 ? Math.sqrt(
1079
- priceValues.reduce((s, p) => s + (p - mean) ** 2, 0) / priceValues.length
1080
- ) / mean : 0;
1081
- if (coeffOfVariation < 0.25 && velocity > 5 && totalSupply > 100) {
1082
- return {
1083
- violated: true,
1084
- severity: 3,
1085
- evidence: {
1086
- coeffOfVariation,
1087
- velocity,
1088
- numResources: priceValues.length,
1089
- meanPrice: mean
1090
- },
1091
- suggestedAction: {
1092
- parameter: "productionCost",
1093
- direction: "increase",
1094
- magnitude: 0.1,
1095
- reasoning: `Price coefficient of variation ${coeffOfVariation.toFixed(2)} with velocity ${velocity.toFixed(1)}. All items priced similarly in an active economy \u2014 no natural num\xE9raire emerging. If a designated currency exists, increase its sink demand to differentiate it.`
1096
- },
1097
- confidence: 0.5,
1098
- estimatedLag: 20
1099
- };
1235
+ for (const curr of metrics.currencies) {
1236
+ const currPrices = metrics.pricesByCurrency[curr] ?? {};
1237
+ const velocity = metrics.velocityByCurrency[curr] ?? 0;
1238
+ const totalSupply = metrics.totalSupplyByCurrency[curr] ?? 0;
1239
+ const priceValues = Object.values(currPrices).filter((p) => p > 0);
1240
+ if (priceValues.length < 3) continue;
1241
+ const mean = priceValues.reduce((s, p) => s + p, 0) / priceValues.length;
1242
+ const coeffOfVariation = mean > 0 ? Math.sqrt(
1243
+ priceValues.reduce((s, p) => s + (p - mean) ** 2, 0) / priceValues.length
1244
+ ) / mean : 0;
1245
+ if (coeffOfVariation < 0.25 && velocity > 5 && totalSupply > 100) {
1246
+ return {
1247
+ violated: true,
1248
+ severity: 3,
1249
+ evidence: {
1250
+ currency: curr,
1251
+ coeffOfVariation,
1252
+ velocity,
1253
+ numResources: priceValues.length,
1254
+ meanPrice: mean
1255
+ },
1256
+ suggestedAction: {
1257
+ parameter: "productionCost",
1258
+ direction: "increase",
1259
+ currency: curr,
1260
+ magnitude: 0.1,
1261
+ reasoning: `[${curr}] Price coefficient of variation ${coeffOfVariation.toFixed(2)} with velocity ${velocity.toFixed(1)}. All items priced similarly in an active economy \u2014 no natural num\xE9raire emerging. If a designated currency exists, increase its sink demand to differentiate it.`
1262
+ },
1263
+ confidence: 0.5,
1264
+ estimatedLag: 20
1265
+ };
1266
+ }
1100
1267
  }
1101
1268
  return { violated: false };
1102
1269
  }
@@ -1784,26 +1951,33 @@ var P42_TheMedianPrinciple = {
1784
1951
  category: "statistical",
1785
1952
  description: "When (mean - median) / median > 0.3, mean is a lie. A few high-balance agents raise the mean while most agents have low balances. Always balance to median when divergence exceeds 30%.",
1786
1953
  check(metrics, thresholds) {
1787
- const { meanMedianDivergence, giniCoefficient } = metrics;
1788
- if (meanMedianDivergence > thresholds.meanMedianDivergenceMax) {
1789
- return {
1790
- violated: true,
1791
- severity: 5,
1792
- evidence: {
1793
- meanMedianDivergence,
1794
- giniCoefficient,
1795
- meanBalance: metrics.meanBalance,
1796
- medianBalance: metrics.medianBalance
1797
- },
1798
- suggestedAction: {
1799
- parameter: "transactionFee",
1800
- direction: "increase",
1801
- magnitude: 0.15,
1802
- reasoning: `Mean/median divergence ${(meanMedianDivergence * 100).toFixed(0)}% (threshold: ${(thresholds.meanMedianDivergenceMax * 100).toFixed(0)}%). Economy has outliers skewing metrics. Use median for decisions. Raise transaction fees to redistribute wealth.`
1803
- },
1804
- confidence: 0.85,
1805
- estimatedLag: 15
1806
- };
1954
+ for (const curr of metrics.currencies) {
1955
+ const meanMedianDivergence = metrics.meanMedianDivergenceByCurrency[curr] ?? 0;
1956
+ const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
1957
+ const meanBalance = metrics.meanBalanceByCurrency[curr] ?? 0;
1958
+ const medianBalance = metrics.medianBalanceByCurrency[curr] ?? 0;
1959
+ if (meanMedianDivergence > thresholds.meanMedianDivergenceMax) {
1960
+ return {
1961
+ violated: true,
1962
+ severity: 5,
1963
+ evidence: {
1964
+ currency: curr,
1965
+ meanMedianDivergence,
1966
+ giniCoefficient,
1967
+ meanBalance,
1968
+ medianBalance
1969
+ },
1970
+ suggestedAction: {
1971
+ parameter: "transactionFee",
1972
+ direction: "increase",
1973
+ currency: curr,
1974
+ magnitude: 0.15,
1975
+ reasoning: `[${curr}] Mean/median divergence ${(meanMedianDivergence * 100).toFixed(0)}% (threshold: ${(thresholds.meanMedianDivergenceMax * 100).toFixed(0)}%). Economy has outliers skewing metrics. Use median for decisions. Raise transaction fees to redistribute wealth.`
1976
+ },
1977
+ confidence: 0.85,
1978
+ estimatedLag: 15
1979
+ };
1980
+ }
1807
1981
  }
1808
1982
  return { violated: false };
1809
1983
  }
@@ -2011,51 +2185,56 @@ var P33_FairNotEqual = {
2011
2185
  category: "player_experience",
2012
2186
  description: "Gini = 0 is boring \u2014 everyone has the same and there is nothing to strive for. Healthy inequality from skill/effort is fine. Inequality from money (pay-to-win) is toxic. Target Gini 0.3-0.45: meaningful spread, not oligarchy.",
2013
2187
  check(metrics, thresholds) {
2014
- const { giniCoefficient } = metrics;
2015
- if (giniCoefficient < 0.1) {
2016
- return {
2017
- violated: true,
2018
- severity: 3,
2019
- evidence: { giniCoefficient },
2020
- suggestedAction: {
2021
- parameter: "rewardRate",
2022
- direction: "increase",
2023
- magnitude: 0.1,
2024
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 near-perfect equality. Economy lacks stakes. Increase winner rewards to create meaningful spread.`
2025
- },
2026
- confidence: 0.6,
2027
- estimatedLag: 20
2028
- };
2029
- }
2030
- if (giniCoefficient > thresholds.giniRedThreshold) {
2031
- return {
2032
- violated: true,
2033
- severity: 7,
2034
- evidence: { giniCoefficient },
2035
- suggestedAction: {
2036
- parameter: "transactionFee",
2037
- direction: "increase",
2038
- magnitude: 0.2,
2039
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 oligarchy level. Toxic inequality. Raise transaction fees to redistribute wealth from rich to pool.`
2040
- },
2041
- confidence: 0.85,
2042
- estimatedLag: 10
2043
- };
2044
- }
2045
- if (giniCoefficient > thresholds.giniWarnThreshold) {
2046
- return {
2047
- violated: true,
2048
- severity: 4,
2049
- evidence: { giniCoefficient },
2050
- suggestedAction: {
2051
- parameter: "transactionFee",
2052
- direction: "increase",
2053
- magnitude: 0.1,
2054
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 high inequality warning. Gently raise fees to slow wealth concentration.`
2055
- },
2056
- confidence: 0.75,
2057
- estimatedLag: 15
2058
- };
2188
+ for (const curr of metrics.currencies) {
2189
+ const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
2190
+ if (giniCoefficient < 0.1) {
2191
+ return {
2192
+ violated: true,
2193
+ severity: 3,
2194
+ evidence: { currency: curr, giniCoefficient },
2195
+ suggestedAction: {
2196
+ parameter: "rewardRate",
2197
+ direction: "increase",
2198
+ currency: curr,
2199
+ magnitude: 0.1,
2200
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 near-perfect equality. Economy lacks stakes. Increase winner rewards to create meaningful spread.`
2201
+ },
2202
+ confidence: 0.6,
2203
+ estimatedLag: 20
2204
+ };
2205
+ }
2206
+ if (giniCoefficient > thresholds.giniRedThreshold) {
2207
+ return {
2208
+ violated: true,
2209
+ severity: 7,
2210
+ evidence: { currency: curr, giniCoefficient },
2211
+ suggestedAction: {
2212
+ parameter: "transactionFee",
2213
+ direction: "increase",
2214
+ currency: curr,
2215
+ magnitude: 0.2,
2216
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 oligarchy level. Toxic inequality. Raise transaction fees to redistribute wealth from rich to pool.`
2217
+ },
2218
+ confidence: 0.85,
2219
+ estimatedLag: 10
2220
+ };
2221
+ }
2222
+ if (giniCoefficient > thresholds.giniWarnThreshold) {
2223
+ return {
2224
+ violated: true,
2225
+ severity: 4,
2226
+ evidence: { currency: curr, giniCoefficient },
2227
+ suggestedAction: {
2228
+ parameter: "transactionFee",
2229
+ direction: "increase",
2230
+ currency: curr,
2231
+ magnitude: 0.1,
2232
+ reasoning: `[${curr}] Gini ${giniCoefficient.toFixed(2)} \u2014 high inequality warning. Gently raise fees to slow wealth concentration.`
2233
+ },
2234
+ confidence: 0.75,
2235
+ estimatedLag: 15
2236
+ };
2237
+ }
2059
2238
  }
2060
2239
  return { violated: false };
2061
2240
  }
@@ -2588,33 +2767,41 @@ var Simulator = class {
2588
2767
  runForward(metrics, action, ticks, _thresholds) {
2589
2768
  const multiplier = this.actionMultiplier(action);
2590
2769
  const noise = () => 1 + (Math.random() - 0.5) * 0.1;
2591
- let supply = metrics.totalSupply;
2770
+ const currencies = metrics.currencies;
2771
+ const targetCurrency = action.currency;
2772
+ const supplies = { ...metrics.totalSupplyByCurrency };
2773
+ const netFlows = { ...metrics.netFlowByCurrency };
2774
+ const ginis = { ...metrics.giniCoefficientByCurrency };
2775
+ const velocities = { ...metrics.velocityByCurrency };
2592
2776
  let satisfaction = metrics.avgSatisfaction;
2593
- let gini = metrics.giniCoefficient;
2594
- let velocity = metrics.velocity;
2595
- let netFlow = metrics.netFlow;
2596
- const churnRate = metrics.churnRate;
2597
2777
  for (let t = 0; t < ticks; t++) {
2598
- const effectOnFlow = this.flowEffect(action, metrics) * multiplier * noise();
2599
- netFlow = netFlow * 0.9 + effectOnFlow * 0.1;
2600
- supply += netFlow * noise();
2601
- supply = Math.max(0, supply);
2602
- const satDelta = netFlow > 0 && netFlow < 20 ? 0.5 : netFlow < 0 ? -1 : 0;
2778
+ for (const curr of currencies) {
2779
+ const isTarget = !targetCurrency || targetCurrency === curr;
2780
+ const effectOnFlow = isTarget ? this.flowEffect(action, metrics, curr) * multiplier * noise() : 0;
2781
+ netFlows[curr] = (netFlows[curr] ?? 0) * 0.9 + effectOnFlow * 0.1;
2782
+ supplies[curr] = Math.max(0, (supplies[curr] ?? 0) + (netFlows[curr] ?? 0) * noise());
2783
+ ginis[curr] = (ginis[curr] ?? 0) * 0.99 + 0.35 * 0.01 * noise();
2784
+ velocities[curr] = (supplies[curr] ?? 0) / Math.max(1, metrics.totalAgents) * 0.01 * noise();
2785
+ }
2786
+ const avgNetFlow = currencies.length > 0 ? Object.values(netFlows).reduce((s, v) => s + v, 0) / currencies.length : 0;
2787
+ const satDelta = avgNetFlow > 0 && avgNetFlow < 20 ? 0.5 : avgNetFlow < 0 ? -1 : 0;
2603
2788
  satisfaction = Math.min(100, Math.max(0, satisfaction + satDelta * noise()));
2604
- gini = gini * 0.99 + 0.35 * 0.01 * noise();
2605
- velocity = supply / Math.max(1, metrics.totalAgents) * 0.01 * noise();
2606
- const agentLoss = metrics.totalAgents * churnRate * noise();
2607
- void agentLoss;
2608
2789
  }
2790
+ const totalSupply = Object.values(supplies).reduce((s, v) => s + v, 0);
2609
2791
  const projected = {
2610
2792
  ...metrics,
2611
2793
  tick: metrics.tick + ticks,
2612
- totalSupply: supply,
2613
- netFlow,
2614
- velocity,
2615
- giniCoefficient: Math.max(0, Math.min(1, gini)),
2794
+ currencies,
2795
+ totalSupplyByCurrency: supplies,
2796
+ netFlowByCurrency: netFlows,
2797
+ velocityByCurrency: velocities,
2798
+ giniCoefficientByCurrency: ginis,
2799
+ totalSupply,
2800
+ netFlow: Object.values(netFlows).reduce((s, v) => s + v, 0),
2801
+ velocity: totalSupply > 0 && currencies.length > 0 ? Object.values(velocities).reduce((s, v) => s + v, 0) / currencies.length : 0,
2802
+ giniCoefficient: currencies.length > 0 ? Object.values(ginis).reduce((s, v) => s + v, 0) / currencies.length : 0,
2616
2803
  avgSatisfaction: satisfaction,
2617
- inflationRate: metrics.totalSupply > 0 ? (supply - metrics.totalSupply) / metrics.totalSupply : 0
2804
+ inflationRate: metrics.totalSupply > 0 ? (totalSupply - metrics.totalSupply) / metrics.totalSupply : 0
2618
2805
  };
2619
2806
  return projected;
2620
2807
  }
@@ -2622,16 +2809,16 @@ var Simulator = class {
2622
2809
  const base = action.magnitude ?? 0.1;
2623
2810
  return action.direction === "increase" ? 1 + base : 1 - base;
2624
2811
  }
2625
- flowEffect(action, metrics) {
2812
+ flowEffect(action, metrics, currency) {
2626
2813
  const { parameter, direction } = action;
2627
2814
  const sign = direction === "increase" ? -1 : 1;
2628
2815
  const roleEntries = Object.entries(metrics.populationByRole).sort((a, b) => b[1] - a[1]);
2629
2816
  const dominantRoleCount = roleEntries[0]?.[1] ?? 0;
2630
2817
  if (parameter === "productionCost") {
2631
- return sign * metrics.netFlow * 0.2;
2818
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.2;
2632
2819
  }
2633
2820
  if (parameter === "transactionFee") {
2634
- return sign * metrics.velocity * 10 * 0.1;
2821
+ return sign * (metrics.velocityByCurrency[currency] ?? 0) * 10 * 0.1;
2635
2822
  }
2636
2823
  if (parameter === "entryFee") {
2637
2824
  return sign * dominantRoleCount * 0.5;
@@ -2640,14 +2827,22 @@ var Simulator = class {
2640
2827
  return -sign * dominantRoleCount * 0.3;
2641
2828
  }
2642
2829
  if (parameter === "yieldRate") {
2643
- return sign * metrics.faucetVolume * 0.15;
2830
+ return sign * (metrics.faucetVolumeByCurrency[currency] ?? 0) * 0.15;
2644
2831
  }
2645
- return sign * metrics.netFlow * 0.1;
2832
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.1;
2646
2833
  }
2647
2834
  checkImprovement(before, after, action) {
2648
2835
  const satisfactionImproved = after.avgSatisfaction >= before.avgSatisfaction - 2;
2649
- const flowMoreBalanced = Math.abs(after.netFlow) <= Math.abs(before.netFlow) * 1.2;
2650
- const notWorseGini = after.giniCoefficient <= before.giniCoefficient + 0.05;
2836
+ const flowMoreBalanced = before.currencies.every((curr) => {
2837
+ const afterFlow = Math.abs(after.netFlowByCurrency[curr] ?? 0);
2838
+ const beforeFlow = Math.abs(before.netFlowByCurrency[curr] ?? 0);
2839
+ return afterFlow <= beforeFlow * 1.2 || afterFlow < 1;
2840
+ });
2841
+ const notWorseGini = before.currencies.every((curr) => {
2842
+ const afterGini = after.giniCoefficientByCurrency[curr] ?? 0;
2843
+ const beforeGini = before.giniCoefficientByCurrency[curr] ?? 0;
2844
+ return afterGini <= beforeGini + 0.05;
2845
+ });
2651
2846
  void action;
2652
2847
  return satisfactionImproved && flowMoreBalanced && notWorseGini;
2653
2848
  }
@@ -2658,6 +2853,21 @@ var Simulator = class {
2658
2853
  const vals = outcomes.map((o) => o[key]).filter((v) => typeof v === "number" && !isNaN(v));
2659
2854
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
2660
2855
  };
2856
+ const avgRecord = (key) => {
2857
+ const allKeys = /* @__PURE__ */ new Set();
2858
+ for (const o of outcomes) {
2859
+ const rec = o[key];
2860
+ if (rec && typeof rec === "object" && !Array.isArray(rec)) {
2861
+ Object.keys(rec).forEach((k) => allKeys.add(k));
2862
+ }
2863
+ }
2864
+ const result = {};
2865
+ for (const k of allKeys) {
2866
+ const vals = outcomes.map((o) => o[key]?.[k]).filter((v) => typeof v === "number" && !isNaN(v));
2867
+ result[k] = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
2868
+ }
2869
+ return result;
2870
+ };
2661
2871
  return {
2662
2872
  ...base,
2663
2873
  totalSupply: avg("totalSupply"),
@@ -2665,7 +2875,11 @@ var Simulator = class {
2665
2875
  velocity: avg("velocity"),
2666
2876
  giniCoefficient: avg("giniCoefficient"),
2667
2877
  avgSatisfaction: avg("avgSatisfaction"),
2668
- inflationRate: avg("inflationRate")
2878
+ inflationRate: avg("inflationRate"),
2879
+ totalSupplyByCurrency: avgRecord("totalSupplyByCurrency"),
2880
+ netFlowByCurrency: avgRecord("netFlowByCurrency"),
2881
+ velocityByCurrency: avgRecord("velocityByCurrency"),
2882
+ giniCoefficientByCurrency: avgRecord("giniCoefficientByCurrency")
2669
2883
  };
2670
2884
  }
2671
2885
  };
@@ -2724,6 +2938,7 @@ var Planner = class {
2724
2938
  id: `plan_${metrics.tick}_${param}`,
2725
2939
  diagnosis,
2726
2940
  parameter: param,
2941
+ ...action.currency !== void 0 ? { currency: action.currency } : {},
2727
2942
  currentValue,
2728
2943
  targetValue,
2729
2944
  maxChangePercent: thresholds.maxAdjustmentPercent,
@@ -2765,7 +2980,7 @@ var Executor = class {
2765
2980
  }
2766
2981
  async apply(plan, adapter, currentParams) {
2767
2982
  const originalValue = currentParams[plan.parameter] ?? plan.currentValue;
2768
- await adapter.setParam(plan.parameter, plan.targetValue);
2983
+ await adapter.setParam(plan.parameter, plan.targetValue, plan.diagnosis.violation.suggestedAction.currency);
2769
2984
  plan.appliedAt = plan.diagnosis.tick;
2770
2985
  this.activePlans.push({ plan, originalValue });
2771
2986
  }
@@ -2787,7 +3002,7 @@ var Executor = class {
2787
3002
  const metricValue = this.getMetricValue(metrics, rc.metric);
2788
3003
  const shouldRollback = rc.direction === "below" ? metricValue < rc.threshold : metricValue > rc.threshold;
2789
3004
  if (shouldRollback) {
2790
- await adapter.setParam(plan.parameter, originalValue);
3005
+ await adapter.setParam(plan.parameter, originalValue, plan.diagnosis.violation.suggestedAction.currency);
2791
3006
  rolledBack.push(plan);
2792
3007
  } else {
2793
3008
  const settledTick = rc.checkAfterTick + 10;
@@ -2920,6 +3135,18 @@ var DecisionLog = class {
2920
3135
  };
2921
3136
 
2922
3137
  // src/MetricStore.ts
3138
+ function getNestedValue(obj, path) {
3139
+ const parts = path.split(".");
3140
+ let val = obj;
3141
+ for (const part of parts) {
3142
+ if (val !== null && typeof val === "object") {
3143
+ val = val[part];
3144
+ } else {
3145
+ return NaN;
3146
+ }
3147
+ }
3148
+ return typeof val === "number" ? val : NaN;
3149
+ }
2923
3150
  var RingBuffer = class {
2924
3151
  constructor(capacity) {
2925
3152
  this.capacity = capacity;
@@ -2997,10 +3224,9 @@ var MetricStore = class {
2997
3224
  if (q.to !== void 0 && m.tick > q.to) return false;
2998
3225
  return true;
2999
3226
  });
3000
- const metricKey = q.metric;
3001
3227
  const points = filtered.map((m) => ({
3002
3228
  tick: m.tick,
3003
- value: typeof m[metricKey] === "number" ? m[metricKey] : NaN
3229
+ value: getNestedValue(m, q.metric)
3004
3230
  }));
3005
3231
  return { metric: q.metric, resolution, points };
3006
3232
  }
@@ -3025,6 +3251,21 @@ var MetricStore = class {
3025
3251
  const vals = snapshots.map((s) => s[key]).filter((v) => !isNaN(v));
3026
3252
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
3027
3253
  };
3254
+ const avgRecord = (key) => {
3255
+ const allKeys = /* @__PURE__ */ new Set();
3256
+ for (const s of snapshots) {
3257
+ const rec = s[key];
3258
+ if (rec && typeof rec === "object" && !Array.isArray(rec)) {
3259
+ Object.keys(rec).forEach((k) => allKeys.add(k));
3260
+ }
3261
+ }
3262
+ const result = {};
3263
+ for (const k of allKeys) {
3264
+ const vals = snapshots.map((s) => s[key]?.[k]).filter((v) => typeof v === "number" && !isNaN(v));
3265
+ result[k] = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
3266
+ }
3267
+ return result;
3268
+ };
3028
3269
  return {
3029
3270
  ...last,
3030
3271
  totalSupply: avg("totalSupply"),
@@ -3044,7 +3285,21 @@ var MetricStore = class {
3044
3285
  tapSinkRatio: avg("tapSinkRatio"),
3045
3286
  productionIndex: avg("productionIndex"),
3046
3287
  capacityUsage: avg("capacityUsage"),
3047
- anchorRatioDrift: avg("anchorRatioDrift")
3288
+ anchorRatioDrift: avg("anchorRatioDrift"),
3289
+ // Per-currency averages
3290
+ totalSupplyByCurrency: avgRecord("totalSupplyByCurrency"),
3291
+ netFlowByCurrency: avgRecord("netFlowByCurrency"),
3292
+ velocityByCurrency: avgRecord("velocityByCurrency"),
3293
+ inflationRateByCurrency: avgRecord("inflationRateByCurrency"),
3294
+ faucetVolumeByCurrency: avgRecord("faucetVolumeByCurrency"),
3295
+ sinkVolumeByCurrency: avgRecord("sinkVolumeByCurrency"),
3296
+ tapSinkRatioByCurrency: avgRecord("tapSinkRatioByCurrency"),
3297
+ anchorRatioDriftByCurrency: avgRecord("anchorRatioDriftByCurrency"),
3298
+ giniCoefficientByCurrency: avgRecord("giniCoefficientByCurrency"),
3299
+ medianBalanceByCurrency: avgRecord("medianBalanceByCurrency"),
3300
+ meanBalanceByCurrency: avgRecord("meanBalanceByCurrency"),
3301
+ top10PctShareByCurrency: avgRecord("top10PctShareByCurrency"),
3302
+ meanMedianDivergenceByCurrency: avgRecord("meanMedianDivergenceByCurrency")
3048
3303
  };
3049
3304
  }
3050
3305
  };