@agent-e/core 1.2.0 → 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,23 +94,28 @@ 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
+ const defaultCurrency = state.currencies[0] ?? "default";
105
106
  for (const e of recentEvents) {
107
+ const curr = e.currency ?? defaultCurrency;
106
108
  switch (e.type) {
107
109
  case "mint":
108
110
  case "spawn":
109
- faucetVolume += e.amount ?? 0;
111
+ faucetVolumeByCurrency[curr] = (faucetVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
110
112
  break;
111
113
  case "burn":
112
114
  case "consume":
113
- sinkVolume += e.amount ?? 0;
115
+ sinkVolumeByCurrency[curr] = (sinkVolumeByCurrency[curr] ?? 0) + (e.amount ?? 0);
116
+ break;
117
+ case "produce":
118
+ productionAmount += e.amount ?? 1;
114
119
  break;
115
120
  case "trade":
116
121
  tradeEvents.push(e);
@@ -124,20 +129,68 @@ var Observer = class {
124
129
  break;
125
130
  }
126
131
  }
127
- 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);
128
184
  const netFlow = faucetVolume - sinkVolume;
129
185
  const tapSinkRatio = sinkVolume > 0 ? faucetVolume / sinkVolume : faucetVolume > 0 ? Infinity : 1;
130
- const prevSupply = this.previousMetrics?.totalSupply ?? totalSupply;
131
- const inflationRate = prevSupply > 0 ? (totalSupply - prevSupply) / prevSupply : 0;
132
186
  const velocity = totalSupply > 0 ? tradeEvents.length / totalSupply : 0;
133
- 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);
134
191
  const meanBalance = totalAgents > 0 ? totalSupply / totalAgents : 0;
135
- const medianBalance = computeMedian(sortedBalances);
136
- const top10Idx = Math.floor(totalAgents * 0.9);
137
- const top10Sum = sortedBalances.slice(top10Idx).reduce((s, b) => s + b, 0);
138
- const top10PctShare = totalSupply > 0 ? top10Sum / totalSupply : 0;
139
- const giniCoefficient = computeGini(sortedBalances);
140
- const meanMedianDivergence = medianBalance > 0 ? Math.abs(meanBalance - medianBalance) / medianBalance : 0;
192
+ const top10PctShare = avgOf(top10PctShareByCurrency);
193
+ const meanMedianDivergence = avgOf(meanMedianDivergenceByCurrency);
141
194
  const populationByRole = {};
142
195
  const roleShares = {};
143
196
  for (const role of roles) {
@@ -152,15 +205,25 @@ var Observer = class {
152
205
  churnByRole[role] = (churnByRole[role] ?? 0) + 1;
153
206
  }
154
207
  const churnRate = churnCount / Math.max(1, totalAgents);
155
- const prices = { ...state.marketPrices };
156
- const priceVolatility = {};
157
- for (const [resource, price] of Object.entries(prices)) {
158
- const prev = this.previousPrices[resource] ?? price;
159
- priceVolatility[resource] = prev > 0 ? Math.abs(price - prev) / prev : 0;
160
- }
161
- this.previousPrices = { ...prices };
162
- const priceValues = Object.values(prices);
163
- 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;
164
227
  const supplyByResource = {};
165
228
  for (const inv of Object.values(state.agentInventories)) {
166
229
  for (const [resource, qty] of Object.entries(inv)) {
@@ -185,70 +248,92 @@ var Observer = class {
185
248
  pinchPoints[resource] = "optimal";
186
249
  }
187
250
  }
188
- const productionIndex = recentEvents.filter((e) => e.type === "produce").reduce((s, e) => s + (e.amount ?? 1), 0);
251
+ const productionIndex = productionAmount;
189
252
  const maxPossibleProduction = productionIndex + sinkVolume;
190
253
  const capacityUsage = maxPossibleProduction > 0 ? productionIndex / maxPossibleProduction : 0;
191
254
  const satisfactions = Object.values(state.agentSatisfaction ?? {});
192
255
  const avgSatisfaction = satisfactions.length > 0 ? satisfactions.reduce((s, v) => s + v, 0) / satisfactions.length : 80;
193
256
  const blockedAgentCount = satisfactions.filter((s) => s < 20).length;
194
257
  const timeToValue = totalAgents > 0 ? blockedAgentCount / totalAgents * 100 : 0;
195
- const poolSizes = { ...state.poolSizes ?? {} };
196
- if (!this.anchorBaseline && tick === 1 && totalSupply > 0) {
197
- this.anchorBaseline = {
198
- currencyPerPeriod: totalSupply / Math.max(1, totalAgents),
199
- itemsPerCurrency: priceIndex > 0 ? 1 / priceIndex : 0
200
- };
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
+ }
201
265
  }
202
- let anchorRatioDrift = 0;
203
- if (this.anchorBaseline && totalAgents > 0) {
204
- const currentCurrencyPerPeriod = totalSupply / totalAgents;
205
- anchorRatioDrift = this.anchorBaseline.currencyPerPeriod > 0 ? (currentCurrencyPerPeriod - this.anchorBaseline.currencyPerPeriod) / this.anchorBaseline.currencyPerPeriod : 0;
206
- }
207
- let arbitrageIndex = 0;
208
- const priceKeys = Object.keys(prices).filter((k) => prices[k] > 0);
209
- if (priceKeys.length >= 2) {
210
- let pairCount = 0;
211
- let totalDivergence = 0;
212
- for (let i = 0; i < priceKeys.length; i++) {
213
- for (let j = i + 1; j < priceKeys.length; j++) {
214
- const pA = prices[priceKeys[i]];
215
- const pB = prices[priceKeys[j]];
216
- let ratio = pA / pB;
217
- ratio = Math.max(1e-3, Math.min(1e3, ratio));
218
- totalDivergence += Math.abs(Math.log(ratio));
219
- pairCount++;
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
+ };
220
275
  }
221
276
  }
222
- arbitrageIndex = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
223
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
+ }
286
+ }
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
+ }
304
+ }
305
+ arbitrageIndexByCurrency[curr] = pairCount > 0 ? Math.min(1, totalDivergence / pairCount) : 0;
306
+ } else {
307
+ arbitrageIndexByCurrency[curr] = 0;
308
+ }
309
+ }
310
+ const arbitrageIndex = avgOf(arbitrageIndexByCurrency);
224
311
  const contentDropEvents = recentEvents.filter(
225
312
  (e) => e.metadata?.["contentDrop"] === true
226
313
  );
227
314
  const contentDropAge = contentDropEvents.length > 0 ? tick - Math.max(...contentDropEvents.map((e) => e.timestamp)) : (this.previousMetrics?.contentDropAge ?? 0) + 1;
228
- let giftTrades = 0;
229
- if (tradeEvents.length > 0) {
230
- for (const e of tradeEvents) {
231
- 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;
232
324
  const tradePrice = e.price ?? 0;
233
- if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) {
234
- giftTrades++;
235
- }
236
- }
237
- }
238
- const giftTradeRatio = tradeEvents.length > 0 ? giftTrades / tradeEvents.length : 0;
239
- let disposalTrades = 0;
240
- if (tradeEvents.length > 0) {
241
- for (const e of tradeEvents) {
325
+ if (tradePrice === 0 || marketPrice > 0 && tradePrice < marketPrice * 0.3) gifts++;
242
326
  if (e.from && e.resource) {
243
327
  const sellerInv = state.agentInventories[e.from]?.[e.resource] ?? 0;
244
328
  const avgInv = (supplyByResource[e.resource] ?? 0) / Math.max(1, totalAgents);
245
- if (sellerInv > avgInv * 3) {
246
- disposalTrades++;
247
- }
329
+ if (sellerInv > avgInv * 3) disposals++;
248
330
  }
249
331
  }
332
+ giftTradeRatioByCurrency[curr] = currTrades.length > 0 ? gifts / currTrades.length : 0;
333
+ disposalTradeRatioByCurrency[curr] = currTrades.length > 0 ? disposals / currTrades.length : 0;
250
334
  }
251
- const disposalTradeRatio = tradeEvents.length > 0 ? disposalTrades / tradeEvents.length : 0;
335
+ const giftTradeRatio = avgOf(giftTradeRatioByCurrency);
336
+ const disposalTradeRatio = avgOf(disposalTradeRatioByCurrency);
252
337
  const custom = {};
253
338
  for (const [name, fn] of Object.entries(this.customMetricFns)) {
254
339
  try {
@@ -260,10 +345,57 @@ var Observer = class {
260
345
  const metrics = {
261
346
  tick,
262
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
263
374
  totalSupply,
264
375
  netFlow,
265
376
  velocity,
266
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
267
399
  populationByRole,
268
400
  roleShares,
269
401
  totalAgents,
@@ -271,38 +403,18 @@ var Observer = class {
271
403
  churnByRole,
272
404
  personaDistribution: {},
273
405
  // populated by PersonaTracker
274
- giniCoefficient,
275
- medianBalance,
276
- meanBalance,
277
- top10PctShare,
278
- meanMedianDivergence,
279
- priceIndex,
280
406
  productionIndex,
281
407
  capacityUsage,
282
- prices,
283
- priceVolatility,
284
408
  supplyByResource,
285
409
  demandSignals,
286
410
  pinchPoints,
287
411
  avgSatisfaction,
288
412
  blockedAgentCount,
289
413
  timeToValue,
290
- faucetVolume,
291
- sinkVolume,
292
- tapSinkRatio,
293
- poolSizes,
294
- anchorRatioDrift,
295
- extractionRatio: NaN,
296
- newUserDependency: NaN,
297
- smokeTestRatio: NaN,
298
- currencyInsulation: NaN,
299
414
  sharkToothPeaks: this.previousMetrics?.sharkToothPeaks ?? [],
300
415
  sharkToothValleys: this.previousMetrics?.sharkToothValleys ?? [],
301
416
  eventCompletionRate: NaN,
302
- arbitrageIndex,
303
417
  contentDropAge,
304
- giftTradeRatio,
305
- disposalTradeRatio,
306
418
  custom
307
419
  };
308
420
  this.previousMetrics = metrics;
@@ -376,48 +488,75 @@ function emptyMetrics(tick = 0) {
376
488
  return {
377
489
  tick,
378
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
379
517
  totalSupply: 0,
380
518
  netFlow: 0,
381
519
  velocity: 0,
382
520
  inflationRate: 0,
383
- populationByRole: {},
384
- roleShares: {},
385
- totalAgents: 0,
386
- churnRate: 0,
387
- churnByRole: {},
388
- personaDistribution: {},
521
+ faucetVolume: 0,
522
+ sinkVolume: 0,
523
+ tapSinkRatio: 1,
524
+ anchorRatioDrift: 0,
389
525
  giniCoefficient: 0,
390
526
  medianBalance: 0,
391
527
  meanBalance: 0,
392
528
  top10PctShare: 0,
393
529
  meanMedianDivergence: 0,
394
530
  priceIndex: 0,
395
- productionIndex: 0,
396
- capacityUsage: 0,
397
531
  prices: {},
398
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,
399
550
  supplyByResource: {},
400
551
  demandSignals: {},
401
552
  pinchPoints: {},
402
553
  avgSatisfaction: 100,
403
554
  blockedAgentCount: 0,
404
555
  timeToValue: 0,
405
- faucetVolume: 0,
406
- sinkVolume: 0,
407
- tapSinkRatio: 1,
408
- poolSizes: {},
409
- anchorRatioDrift: 0,
410
- extractionRatio: NaN,
411
- newUserDependency: NaN,
412
- smokeTestRatio: NaN,
413
- currencyInsulation: NaN,
414
556
  sharkToothPeaks: [],
415
557
  sharkToothValleys: [],
416
558
  eventCompletionRate: NaN,
417
- arbitrageIndex: 0,
418
559
  contentDropAge: 0,
419
- giftTradeRatio: 0,
420
- disposalTradeRatio: 0,
421
560
  custom: {}
422
561
  };
423
562
  }
@@ -883,38 +1022,44 @@ var P12_OnePrimaryFaucet = {
883
1022
  id: "P12",
884
1023
  name: "One Primary Faucet",
885
1024
  category: "currency",
886
- description: "Multiple independent currency sources (gathering + crafting + quests) each creating gold causes uncontrolled inflation. One clear primary faucet makes the economy predictable and auditable.",
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.",
887
1026
  check(metrics, thresholds) {
888
- const { netFlow, faucetVolume, sinkVolume } = metrics;
889
- if (netFlow > thresholds.netFlowWarnThreshold) {
890
- return {
891
- violated: true,
892
- severity: 5,
893
- evidence: { netFlow, faucetVolume, sinkVolume },
894
- suggestedAction: {
895
- parameter: "productionCost",
896
- direction: "increase",
897
- magnitude: 0.15,
898
- reasoning: `Net flow +${netFlow.toFixed(1)} g/t. Inflationary. Increase crafting cost (primary sink) to balance faucet output.`
899
- },
900
- confidence: 0.8,
901
- estimatedLag: 8
902
- };
903
- }
904
- if (netFlow < -thresholds.netFlowWarnThreshold) {
905
- return {
906
- violated: true,
907
- severity: 4,
908
- evidence: { netFlow, faucetVolume, sinkVolume },
909
- suggestedAction: {
910
- parameter: "productionCost",
911
- direction: "decrease",
912
- magnitude: 0.15,
913
- reasoning: `Net flow ${netFlow.toFixed(1)} g/t. Deflationary. Decrease crafting cost to ease sink pressure.`
914
- },
915
- confidence: 0.8,
916
- estimatedLag: 8
917
- };
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
+ }
918
1063
  }
919
1064
  return { violated: false };
920
1065
  }
@@ -925,28 +1070,30 @@ var P13_PotsAreZeroSumAndSelfRegulate = {
925
1070
  category: "currency",
926
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.",
927
1072
  check(metrics, thresholds) {
928
- const { poolSizes, populationByRole } = metrics;
929
- const totalAgents = metrics.totalAgents;
1073
+ const { populationByRole } = metrics;
930
1074
  const roleEntries = Object.entries(populationByRole).sort((a, b) => b[1] - a[1]);
931
- const dominantRole = roleEntries[0]?.[0];
932
1075
  const dominantCount = roleEntries[0]?.[1] ?? 0;
933
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
934
- if (dominantCount > 5 && poolSize < 50) {
935
- const { poolWinRate, poolHouseCut } = thresholds;
936
- const maxSustainableMultiplier = (1 - poolHouseCut) / poolWinRate;
937
- return {
938
- violated: true,
939
- severity: 7,
940
- evidence: { pool: poolName, poolSize, participants: dominantCount, maxSustainableMultiplier },
941
- suggestedAction: {
942
- parameter: "rewardRate",
943
- direction: "decrease",
944
- magnitude: 0.15,
945
- 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.`
946
- },
947
- confidence: 0.85,
948
- estimatedLag: 3
949
- };
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
+ }
950
1097
  }
951
1098
  }
952
1099
  return { violated: false };
@@ -958,22 +1105,27 @@ var P14_TrackActualInjection = {
958
1105
  category: "currency",
959
1106
  description: 'Counting resource gathering as "currency injected" is misleading. Currency enters through faucet mechanisms (spawning, rewards). Fake metrics break every downstream decision.',
960
1107
  check(metrics, _thresholds) {
961
- const { faucetVolume, netFlow, totalSupply } = metrics;
962
- const supplyGrowthRate = Math.abs(netFlow) / Math.max(1, totalSupply);
963
- if (supplyGrowthRate > 0.1) {
964
- return {
965
- violated: true,
966
- severity: 4,
967
- evidence: { faucetVolume, netFlow, supplyGrowthRate },
968
- suggestedAction: {
969
- parameter: "yieldRate",
970
- direction: "decrease",
971
- magnitude: 0.1,
972
- reasoning: `Supply growing at ${(supplyGrowthRate * 100).toFixed(1)}%/tick. Verify gold injection tracking. Resources should not create gold directly.`
973
- },
974
- confidence: 0.55,
975
- estimatedLag: 5
976
- };
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
+ }
977
1129
  }
978
1130
  return { violated: false };
979
1131
  }
@@ -982,26 +1134,30 @@ var P15_PoolsNeedCapAndDecay = {
982
1134
  id: "P15",
983
1135
  name: "Pools Need Cap + Decay",
984
1136
  category: "currency",
985
- description: "Any pool (bank, reward pool) without a cap accumulates infinitely. Bank pool at 42% of gold supply means 42% of the economy is frozen. Cap at 5%, decay at 2%/tick.",
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.",
986
1138
  check(metrics, thresholds) {
987
- const { poolSizes, totalSupply } = metrics;
988
1139
  const { poolCapPercent } = thresholds;
989
- for (const [pool, size] of Object.entries(poolSizes)) {
990
- const shareOfSupply = size / Math.max(1, totalSupply);
991
- if (shareOfSupply > poolCapPercent * 2) {
992
- return {
993
- violated: true,
994
- severity: 6,
995
- evidence: { pool, size, shareOfSupply, cap: poolCapPercent },
996
- suggestedAction: {
997
- parameter: "transactionFee",
998
- direction: "decrease",
999
- magnitude: 0.1,
1000
- reasoning: `${pool} pool at ${(shareOfSupply * 100).toFixed(1)}% of supply (cap: ${(poolCapPercent * 100).toFixed(0)}%). Gold frozen. Lower fees to encourage circulation over accumulation.`
1001
- },
1002
- confidence: 0.85,
1003
- estimatedLag: 5
1004
- };
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
+ }
1005
1161
  }
1006
1162
  }
1007
1163
  return { violated: false };
@@ -1013,23 +1169,27 @@ var P16_WithdrawalPenaltyScales = {
1013
1169
  category: "currency",
1014
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.",
1015
1171
  check(metrics, _thresholds) {
1016
- const { poolSizes, totalSupply } = metrics;
1017
- const stakedEstimate = totalSupply * 0.15;
1018
- for (const [poolName, poolSize] of Object.entries(poolSizes)) {
1019
- if (poolSize < 10 && stakedEstimate > 100) {
1020
- return {
1021
- violated: true,
1022
- severity: 3,
1023
- evidence: { pool: poolName, poolSize, estimatedStaked: stakedEstimate },
1024
- suggestedAction: {
1025
- parameter: "transactionFee",
1026
- direction: "increase",
1027
- magnitude: 0.05,
1028
- reasoning: `${poolName} pool depleted while significant currency should be locked. Early withdrawals may be draining the pool. Ensure withdrawal penalty scales with lock duration.`
1029
- },
1030
- confidence: 0.45,
1031
- estimatedLag: 10
1032
- };
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
+ }
1033
1193
  }
1034
1194
  }
1035
1195
  return { violated: false };
@@ -1041,22 +1201,27 @@ var P32_VelocityAboveSupply = {
1041
1201
  category: "currency",
1042
1202
  description: "Low transactions despite adequate supply means liquidity is trapped. High supply with low velocity = stagnation, not abundance.",
1043
1203
  check(metrics, _thresholds) {
1044
- const { velocity, totalSupply, supplyByResource } = metrics;
1204
+ const { supplyByResource } = metrics;
1045
1205
  const totalResources = Object.values(supplyByResource).reduce((s, v) => s + v, 0);
1046
- if (velocity < 3 && totalSupply > 100 && totalResources > 20) {
1047
- return {
1048
- violated: true,
1049
- severity: 4,
1050
- evidence: { velocity, totalSupply, totalResources },
1051
- suggestedAction: {
1052
- parameter: "transactionFee",
1053
- direction: "decrease",
1054
- magnitude: 0.2,
1055
- reasoning: `Velocity ${velocity}/t with ${totalResources} resources in system. Economy stagnant despite available supply. Lower trading friction.`
1056
- },
1057
- confidence: 0.75,
1058
- estimatedLag: 5
1059
- };
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
+ }
1060
1225
  }
1061
1226
  return { violated: false };
1062
1227
  }
@@ -1067,32 +1232,38 @@ var P58_NoNaturalNumeraire = {
1067
1232
  category: "currency",
1068
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.",
1069
1234
  check(metrics, _thresholds) {
1070
- const { prices, velocity, totalSupply } = metrics;
1071
- const priceValues = Object.values(prices).filter((p) => p > 0);
1072
- if (priceValues.length < 3) return { violated: false };
1073
- const mean = priceValues.reduce((s, p) => s + p, 0) / priceValues.length;
1074
- const coeffOfVariation = mean > 0 ? Math.sqrt(
1075
- priceValues.reduce((s, p) => s + (p - mean) ** 2, 0) / priceValues.length
1076
- ) / mean : 0;
1077
- if (coeffOfVariation < 0.25 && velocity > 5 && totalSupply > 100) {
1078
- return {
1079
- violated: true,
1080
- severity: 3,
1081
- evidence: {
1082
- coeffOfVariation,
1083
- velocity,
1084
- numResources: priceValues.length,
1085
- meanPrice: mean
1086
- },
1087
- suggestedAction: {
1088
- parameter: "productionCost",
1089
- direction: "increase",
1090
- magnitude: 0.1,
1091
- 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.`
1092
- },
1093
- confidence: 0.5,
1094
- estimatedLag: 20
1095
- };
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
+ }
1096
1267
  }
1097
1268
  return { violated: false };
1098
1269
  }
@@ -1139,25 +1310,22 @@ var P18_FirstProducerNeedsStartingInventory = {
1139
1310
  description: "A producer with 0 resources and 0 currency must sell nothing to get currency before they can buy raw materials. This creates a chicken-and-egg freeze. Starting inventory (2 goods + 4 raw materials + 40 currency) breaks the deadlock.",
1140
1311
  check(metrics, _thresholds) {
1141
1312
  if (metrics.tick > 20) return { violated: false };
1313
+ const hasAgents = metrics.totalAgents > 0;
1142
1314
  for (const [resource, supply] of Object.entries(metrics.supplyByResource)) {
1143
- if (supply === 0) {
1144
- for (const [role, population] of Object.entries(metrics.populationByRole)) {
1145
- if (population > 0) {
1146
- return {
1147
- violated: true,
1148
- severity: 8,
1149
- evidence: { tick: metrics.tick, resource, supply, role, population },
1150
- suggestedAction: {
1151
- parameter: "productionCost",
1152
- direction: "decrease",
1153
- magnitude: 0.5,
1154
- reasoning: `Bootstrap failure: ${role} exists but ${resource} supply is 0 on tick 1-20. Drastically reduce production cost to allow immediate output.`
1155
- },
1156
- confidence: 0.9,
1157
- estimatedLag: 2
1158
- };
1159
- }
1160
- }
1315
+ if (supply === 0 && hasAgents) {
1316
+ return {
1317
+ violated: true,
1318
+ severity: 8,
1319
+ evidence: { tick: metrics.tick, resource, supply, totalAgents: metrics.totalAgents },
1320
+ suggestedAction: {
1321
+ parameter: "productionCost",
1322
+ direction: "decrease",
1323
+ magnitude: 0.5,
1324
+ reasoning: `Bootstrap failure: ${resource} supply is 0 at tick ${metrics.tick} with ${metrics.totalAgents} agents. Drastically reduce production cost to allow immediate output.`
1325
+ },
1326
+ confidence: 0.9,
1327
+ estimatedLag: 2
1328
+ };
1161
1329
  }
1162
1330
  }
1163
1331
  return { violated: false };
@@ -1783,26 +1951,33 @@ var P42_TheMedianPrinciple = {
1783
1951
  category: "statistical",
1784
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%.",
1785
1953
  check(metrics, thresholds) {
1786
- const { meanMedianDivergence, giniCoefficient } = metrics;
1787
- if (meanMedianDivergence > thresholds.meanMedianDivergenceMax) {
1788
- return {
1789
- violated: true,
1790
- severity: 5,
1791
- evidence: {
1792
- meanMedianDivergence,
1793
- giniCoefficient,
1794
- meanBalance: metrics.meanBalance,
1795
- medianBalance: metrics.medianBalance
1796
- },
1797
- suggestedAction: {
1798
- parameter: "transactionFee",
1799
- direction: "increase",
1800
- magnitude: 0.15,
1801
- reasoning: `Mean/median divergence ${(meanMedianDivergence * 100).toFixed(0)}% (threshold: ${(thresholds.meanMedianDivergenceMax * 100).toFixed(0)}%). Economy has outliers skewing metrics. Use median for decisions. Raise auction fees to redistribute wealth.`
1802
- },
1803
- confidence: 0.85,
1804
- estimatedLag: 15
1805
- };
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
+ }
1806
1981
  }
1807
1982
  return { violated: false };
1808
1983
  }
@@ -2010,51 +2185,56 @@ var P33_FairNotEqual = {
2010
2185
  category: "player_experience",
2011
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.",
2012
2187
  check(metrics, thresholds) {
2013
- const { giniCoefficient } = metrics;
2014
- if (giniCoefficient < 0.1) {
2015
- return {
2016
- violated: true,
2017
- severity: 3,
2018
- evidence: { giniCoefficient },
2019
- suggestedAction: {
2020
- parameter: "rewardRate",
2021
- direction: "increase",
2022
- magnitude: 0.1,
2023
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 near-perfect equality. Economy lacks stakes. Increase winner rewards to create meaningful spread.`
2024
- },
2025
- confidence: 0.6,
2026
- estimatedLag: 20
2027
- };
2028
- }
2029
- if (giniCoefficient > thresholds.giniRedThreshold) {
2030
- return {
2031
- violated: true,
2032
- severity: 7,
2033
- evidence: { giniCoefficient },
2034
- suggestedAction: {
2035
- parameter: "transactionFee",
2036
- direction: "increase",
2037
- magnitude: 0.2,
2038
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 oligarchy level. Toxic inequality. Raise transaction fees to redistribute wealth from rich to pool.`
2039
- },
2040
- confidence: 0.85,
2041
- estimatedLag: 10
2042
- };
2043
- }
2044
- if (giniCoefficient > thresholds.giniWarnThreshold) {
2045
- return {
2046
- violated: true,
2047
- severity: 4,
2048
- evidence: { giniCoefficient },
2049
- suggestedAction: {
2050
- parameter: "transactionFee",
2051
- direction: "increase",
2052
- magnitude: 0.1,
2053
- reasoning: `Gini ${giniCoefficient.toFixed(2)} \u2014 high inequality warning. Gently raise fees to slow wealth concentration.`
2054
- },
2055
- confidence: 0.75,
2056
- estimatedLag: 15
2057
- };
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
+ }
2058
2238
  }
2059
2239
  return { violated: false };
2060
2240
  }
@@ -2587,33 +2767,41 @@ var Simulator = class {
2587
2767
  runForward(metrics, action, ticks, _thresholds) {
2588
2768
  const multiplier = this.actionMultiplier(action);
2589
2769
  const noise = () => 1 + (Math.random() - 0.5) * 0.1;
2590
- 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 };
2591
2776
  let satisfaction = metrics.avgSatisfaction;
2592
- let gini = metrics.giniCoefficient;
2593
- let velocity = metrics.velocity;
2594
- let netFlow = metrics.netFlow;
2595
- const churnRate = metrics.churnRate;
2596
2777
  for (let t = 0; t < ticks; t++) {
2597
- const effectOnFlow = this.flowEffect(action, metrics) * multiplier * noise();
2598
- netFlow = netFlow * 0.9 + effectOnFlow * 0.1;
2599
- supply += netFlow * noise();
2600
- supply = Math.max(0, supply);
2601
- 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;
2602
2788
  satisfaction = Math.min(100, Math.max(0, satisfaction + satDelta * noise()));
2603
- gini = gini * 0.99 + 0.35 * 0.01 * noise();
2604
- velocity = supply / Math.max(1, metrics.totalAgents) * 0.01 * noise();
2605
- const agentLoss = metrics.totalAgents * churnRate * noise();
2606
- void agentLoss;
2607
2789
  }
2790
+ const totalSupply = Object.values(supplies).reduce((s, v) => s + v, 0);
2608
2791
  const projected = {
2609
2792
  ...metrics,
2610
2793
  tick: metrics.tick + ticks,
2611
- totalSupply: supply,
2612
- netFlow,
2613
- velocity,
2614
- 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,
2615
2803
  avgSatisfaction: satisfaction,
2616
- inflationRate: metrics.totalSupply > 0 ? (supply - metrics.totalSupply) / metrics.totalSupply : 0
2804
+ inflationRate: metrics.totalSupply > 0 ? (totalSupply - metrics.totalSupply) / metrics.totalSupply : 0
2617
2805
  };
2618
2806
  return projected;
2619
2807
  }
@@ -2621,16 +2809,16 @@ var Simulator = class {
2621
2809
  const base = action.magnitude ?? 0.1;
2622
2810
  return action.direction === "increase" ? 1 + base : 1 - base;
2623
2811
  }
2624
- flowEffect(action, metrics) {
2812
+ flowEffect(action, metrics, currency) {
2625
2813
  const { parameter, direction } = action;
2626
2814
  const sign = direction === "increase" ? -1 : 1;
2627
2815
  const roleEntries = Object.entries(metrics.populationByRole).sort((a, b) => b[1] - a[1]);
2628
2816
  const dominantRoleCount = roleEntries[0]?.[1] ?? 0;
2629
2817
  if (parameter === "productionCost") {
2630
- return sign * metrics.netFlow * 0.2;
2818
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.2;
2631
2819
  }
2632
2820
  if (parameter === "transactionFee") {
2633
- return sign * metrics.velocity * 10 * 0.1;
2821
+ return sign * (metrics.velocityByCurrency[currency] ?? 0) * 10 * 0.1;
2634
2822
  }
2635
2823
  if (parameter === "entryFee") {
2636
2824
  return sign * dominantRoleCount * 0.5;
@@ -2639,14 +2827,22 @@ var Simulator = class {
2639
2827
  return -sign * dominantRoleCount * 0.3;
2640
2828
  }
2641
2829
  if (parameter === "yieldRate") {
2642
- return sign * metrics.faucetVolume * 0.15;
2830
+ return sign * (metrics.faucetVolumeByCurrency[currency] ?? 0) * 0.15;
2643
2831
  }
2644
- return sign * metrics.netFlow * 0.1;
2832
+ return sign * (metrics.netFlowByCurrency[currency] ?? 0) * 0.1;
2645
2833
  }
2646
2834
  checkImprovement(before, after, action) {
2647
2835
  const satisfactionImproved = after.avgSatisfaction >= before.avgSatisfaction - 2;
2648
- const flowMoreBalanced = Math.abs(after.netFlow) <= Math.abs(before.netFlow) * 1.2;
2649
- 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
+ });
2650
2846
  void action;
2651
2847
  return satisfactionImproved && flowMoreBalanced && notWorseGini;
2652
2848
  }
@@ -2657,6 +2853,21 @@ var Simulator = class {
2657
2853
  const vals = outcomes.map((o) => o[key]).filter((v) => typeof v === "number" && !isNaN(v));
2658
2854
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
2659
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
+ };
2660
2871
  return {
2661
2872
  ...base,
2662
2873
  totalSupply: avg("totalSupply"),
@@ -2664,7 +2875,11 @@ var Simulator = class {
2664
2875
  velocity: avg("velocity"),
2665
2876
  giniCoefficient: avg("giniCoefficient"),
2666
2877
  avgSatisfaction: avg("avgSatisfaction"),
2667
- inflationRate: avg("inflationRate")
2878
+ inflationRate: avg("inflationRate"),
2879
+ totalSupplyByCurrency: avgRecord("totalSupplyByCurrency"),
2880
+ netFlowByCurrency: avgRecord("netFlowByCurrency"),
2881
+ velocityByCurrency: avgRecord("velocityByCurrency"),
2882
+ giniCoefficientByCurrency: avgRecord("giniCoefficientByCurrency")
2668
2883
  };
2669
2884
  }
2670
2885
  };
@@ -2723,6 +2938,7 @@ var Planner = class {
2723
2938
  id: `plan_${metrics.tick}_${param}`,
2724
2939
  diagnosis,
2725
2940
  parameter: param,
2941
+ ...action.currency !== void 0 ? { currency: action.currency } : {},
2726
2942
  currentValue,
2727
2943
  targetValue,
2728
2944
  maxChangePercent: thresholds.maxAdjustmentPercent,
@@ -2764,7 +2980,7 @@ var Executor = class {
2764
2980
  }
2765
2981
  async apply(plan, adapter, currentParams) {
2766
2982
  const originalValue = currentParams[plan.parameter] ?? plan.currentValue;
2767
- await adapter.setParam(plan.parameter, plan.targetValue);
2983
+ await adapter.setParam(plan.parameter, plan.targetValue, plan.diagnosis.violation.suggestedAction.currency);
2768
2984
  plan.appliedAt = plan.diagnosis.tick;
2769
2985
  this.activePlans.push({ plan, originalValue });
2770
2986
  }
@@ -2786,7 +3002,7 @@ var Executor = class {
2786
3002
  const metricValue = this.getMetricValue(metrics, rc.metric);
2787
3003
  const shouldRollback = rc.direction === "below" ? metricValue < rc.threshold : metricValue > rc.threshold;
2788
3004
  if (shouldRollback) {
2789
- await adapter.setParam(plan.parameter, originalValue);
3005
+ await adapter.setParam(plan.parameter, originalValue, plan.diagnosis.violation.suggestedAction.currency);
2790
3006
  rolledBack.push(plan);
2791
3007
  } else {
2792
3008
  const settledTick = rc.checkAfterTick + 10;
@@ -2919,6 +3135,18 @@ var DecisionLog = class {
2919
3135
  };
2920
3136
 
2921
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
+ }
2922
3150
  var RingBuffer = class {
2923
3151
  constructor(capacity) {
2924
3152
  this.capacity = capacity;
@@ -2996,10 +3224,9 @@ var MetricStore = class {
2996
3224
  if (q.to !== void 0 && m.tick > q.to) return false;
2997
3225
  return true;
2998
3226
  });
2999
- const metricKey = q.metric;
3000
3227
  const points = filtered.map((m) => ({
3001
3228
  tick: m.tick,
3002
- value: typeof m[metricKey] === "number" ? m[metricKey] : NaN
3229
+ value: getNestedValue(m, q.metric)
3003
3230
  }));
3004
3231
  return { metric: q.metric, resolution, points };
3005
3232
  }
@@ -3024,6 +3251,21 @@ var MetricStore = class {
3024
3251
  const vals = snapshots.map((s) => s[key]).filter((v) => !isNaN(v));
3025
3252
  return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
3026
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
+ };
3027
3269
  return {
3028
3270
  ...last,
3029
3271
  totalSupply: avg("totalSupply"),
@@ -3043,7 +3285,21 @@ var MetricStore = class {
3043
3285
  tapSinkRatio: avg("tapSinkRatio"),
3044
3286
  productionIndex: avg("productionIndex"),
3045
3287
  capacityUsage: avg("capacityUsage"),
3046
- 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")
3047
3303
  };
3048
3304
  }
3049
3305
  };