dexterm-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/packages/agents/src/spectator.js +28 -2
  2. package/dist/packages/agents/src/tiers.js +3 -0
  3. package/dist/packages/agents/src/tools.js +3 -0
  4. package/dist/packages/autotrade/src/backtest.js +6 -2
  5. package/dist/packages/autotrade/src/chat/connector.js +5 -4
  6. package/dist/packages/autotrade/src/chat/control.js +3 -0
  7. package/dist/packages/autotrade/src/chat/hub.js +37 -2
  8. package/dist/packages/autotrade/src/chat/index.js +3 -2
  9. package/dist/packages/autotrade/src/chat/readonly.js +530 -0
  10. package/dist/packages/autotrade/src/chat/service.js +12 -1
  11. package/dist/packages/autotrade/src/engine.js +75 -29
  12. package/dist/packages/autotrade/src/fleet.js +2 -2
  13. package/dist/packages/autotrade/src/index.js +4 -0
  14. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  15. package/dist/packages/autotrade/src/rugproof.js +231 -0
  16. package/dist/packages/autotrade/src/runner.js +203 -30
  17. package/dist/packages/autotrade/src/service-install.js +6 -4
  18. package/dist/packages/autotrade/src/sessions.js +16 -7
  19. package/dist/packages/autotrade/src/store.js +1 -1
  20. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  21. package/dist/packages/autotrade/src/strategy.js +29 -1
  22. package/dist/packages/autotrade/src/tiers.js +194 -0
  23. package/dist/packages/classifier/src/calibrate.js +138 -0
  24. package/dist/packages/classifier/src/classify.js +107 -0
  25. package/dist/packages/classifier/src/config.js +298 -0
  26. package/dist/packages/classifier/src/dossier.js +95 -0
  27. package/dist/packages/classifier/src/hardware.js +63 -0
  28. package/dist/packages/classifier/src/index.js +19 -0
  29. package/dist/packages/classifier/src/models.js +111 -0
  30. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  31. package/dist/packages/classifier/src/providers/http.js +63 -0
  32. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  33. package/dist/packages/classifier/src/providers/llm.js +89 -0
  34. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  35. package/dist/packages/classifier/src/questions.js +97 -0
  36. package/dist/packages/classifier/src/registry.js +42 -0
  37. package/dist/packages/classifier/src/snapshot.js +31 -0
  38. package/dist/packages/classifier/src/systemone.js +88 -0
  39. package/dist/packages/classifier/src/types.js +18 -0
  40. package/dist/packages/classifier/src/win-acl.js +71 -0
  41. package/dist/packages/cli/assets/dash/dash.css +139 -0
  42. package/dist/packages/cli/assets/dash/dash.js +200 -0
  43. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  44. package/dist/packages/cli/assets/dash/index.html +36 -0
  45. package/dist/packages/cli/src/app-help-gen.js +13 -1
  46. package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
  47. package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
  48. package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
  49. package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
  50. package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
  51. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
  52. package/dist/packages/cli/src/classifier-cmd.js +361 -0
  53. package/dist/packages/cli/src/classifier-live.js +81 -0
  54. package/dist/packages/cli/src/dash/data.js +217 -0
  55. package/dist/packages/cli/src/dash/server.js +92 -0
  56. package/dist/packages/cli/src/dash-cmd.js +113 -0
  57. package/dist/packages/cli/src/doctor.js +34 -0
  58. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  59. package/dist/packages/cli/src/live/signals.js +8 -0
  60. package/dist/packages/cli/src/main.js +98 -6
  61. package/dist/packages/cli/src/mcp-follow.js +21 -1
  62. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  63. package/dist/packages/cli/src/narratives.js +114 -0
  64. package/dist/packages/cli/src/overlay/model.js +147 -0
  65. package/dist/packages/cli/src/overlay/page.js +146 -0
  66. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  67. package/dist/packages/cli/src/overlay/server.js +170 -0
  68. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  69. package/dist/packages/cli/src/record-cmd.js +82 -11
  70. package/dist/packages/cli/src/rules-cmd.js +2 -1
  71. package/dist/packages/cli/src/rules.js +24 -4
  72. package/dist/packages/cli/src/screens/narrative.js +154 -2
  73. package/dist/packages/cli/src/skill-eval-build.js +7 -2
  74. package/dist/packages/cli/src/state.js +1 -0
  75. package/dist/packages/cli/src/version.js +1 -1
  76. package/dist/packages/custom/src/rules.js +5 -1
  77. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
  78. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  79. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  80. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  81. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
  85. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  87. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  88. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  90. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  92. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  93. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  94. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
  95. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
  96. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +36 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  98. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  101. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  105. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  107. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  109. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  110. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  111. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  112. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  113. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  114. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  115. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  117. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  119. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  120. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  121. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  122. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  123. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  124. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  127. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  131. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
  132. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  134. package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
  135. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
  136. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  138. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  139. package/dist/packages/mcp/src/server.js +106 -1
  140. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  141. package/dist/packages/narratives/src/cluster.js +193 -0
  142. package/dist/packages/narratives/src/embed.js +184 -0
  143. package/dist/packages/narratives/src/export.js +60 -0
  144. package/dist/packages/narratives/src/index.js +10 -0
  145. package/dist/packages/narratives/src/label.js +48 -0
  146. package/dist/packages/narratives/src/normalize.js +179 -0
  147. package/dist/packages/narratives/src/recorder.js +93 -0
  148. package/dist/packages/narratives/src/tracker.js +328 -0
  149. package/dist/packages/packs/src/skill-care.js +10 -2
  150. package/dist/packages/recorder/src/exit-probes.js +215 -0
  151. package/dist/packages/recorder/src/index.js +1 -0
  152. package/dist/packages/recorder/src/recorder.js +11 -1
  153. package/dist/packages/recorder/src/retention.js +4 -0
  154. package/dist/packages/recorder/src/schema.js +20 -1
  155. package/dist/packages/tui/src/keymap.js +5 -0
  156. package/package.json +3 -1
  157. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -12,6 +12,7 @@
12
12
  // three independent fences, never the only one.
13
13
  import { parseFilter } from "../../cli/src/screener/filter.js";
14
14
  import { tickRow } from "./replay.js";
15
+ import { TIERS_HARD_CAP_PCT, selectConfidenceTier, tierTable } from "./tiers.js";
15
16
  export const DAY_MS = 86_400_000;
16
17
  const r2 = (x) => Math.round(x * 100) / 100;
17
18
  const r6 = (x) => Number(x.toPrecision(6));
@@ -297,7 +298,7 @@ export const dayOf = (t) => Math.floor(t / DAY_MS);
297
298
  * cooldowns, counts), then the size the strategy itself picks, then every ceiling, each reduction recorded.
298
299
  * An agent request can only LOWER the size below the strategy's own pick, never raise it above a ceiling.
299
300
  */
300
- export function clampEntry(book, k, req, killSwitch = false) {
301
+ export function clampEntry(book, k, req, killSwitch = false, nowMs = k.t, inFlight) {
301
302
  const s = book.strategy;
302
303
  const st = book.state;
303
304
  const clamps = [];
@@ -306,6 +307,11 @@ export function clampEntry(book, k, req, killSwitch = false) {
306
307
  // fills become trade-fire signals for the user's own executor (the runner admits it only through the real-mode hook).
307
308
  if (s.mode !== 'paper' && s.mode !== 'real')
308
309
  return no('mode-not-paper');
310
+ // 0.2.2: EXPERIMENTAL confidence tiers are PAPER ONLY. validateStrategy and the runner refuse them too; this is the
311
+ // engine's own fence for any document that reached it another way.
312
+ const tiersOn = s.sizing.tiers ?? null;
313
+ if (tiersOn && s.mode !== 'paper')
314
+ return no('tiers-paper-only');
309
315
  if (killSwitch)
310
316
  return no('kill-switch');
311
317
  if (st.halted)
@@ -339,34 +345,57 @@ export function clampEntry(book, k, req, killSwitch = false) {
339
345
  }
340
346
  if ((k.liq ?? 0) < profile.fit.minLiquidityUsd)
341
347
  return no('profile-min-liquidity');
348
+ // 0.2.2 confidence tiers: score the tick (fresh at nowMs), pick the tier; T0 is a skip; one open position unless the
349
+ // block says not. With tiers, an entry decided but not filled yet (the latency window, an open approval) counts as
350
+ // open for one-open and maxConcurrent, and its dollars come off the exposure and cash room (task-2156).
351
+ let tier;
352
+ const fly = tiersOn ? inFlightOf(inFlight) : { opens: 0, usd: 0 };
353
+ if (tiersOn) {
354
+ if (tiersOn.oneOpen !== false && book.open.size + fly.opens >= 1)
355
+ return no('tier-one-open');
356
+ if (book.open.size - bags + fly.opens >= s.risk.maxConcurrent)
357
+ return no('max-concurrent');
358
+ tier = selectConfidenceTier(k, tierTable(tiersOn.table), nowMs);
359
+ if (tier.tier === 'T0')
360
+ return { ok: false, refused: 'tier-T0', clamps, tier };
361
+ }
342
362
  const bank = book.bookEquity();
343
363
  const z = s.sizing;
344
364
  let usd;
345
365
  let basis;
346
- switch (z.mode) {
347
- case 'fixed':
348
- usd = z.fixedUsd ?? 0;
349
- basis = `fixed $${z.fixedUsd}`;
350
- break;
351
- case 'pct':
352
- usd = (bank * (z.pct ?? 0)) / 100;
353
- basis = `${z.pct}% of $${r2(bank)}`;
354
- break;
355
- case 'risk': {
356
- const stop = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
357
- usd = (bank * (z.riskPct ?? 0)) / stop;
358
- basis = `${z.riskPct}% risk / ${stop}% stop of $${r2(bank)}`;
359
- break;
360
- }
361
- case 'confidence': {
362
- const c = z.confidence ?? { minPct: 1, maxPct: 1, default: 0.5 };
363
- const conf = clamp01(req.confidence ?? c.default);
364
- const pct = c.minPct + (c.maxPct - c.minPct) * conf;
365
- usd = (bank * pct) / 100;
366
- basis = `confidence ${r2(conf)} → ${r2(pct)}% of $${r2(bank)}`;
367
- break;
368
- }
366
+ // With tiers on, the tier's runs-at % is the strategy's pick (an unproven tier above the cap is recorded as clamped
367
+ // from its configured size); every ceiling below still applies.
368
+ if (tier) {
369
+ usd = (bank * tier.effectivePct) / 100;
370
+ basis = `tier ${tier.tier} (score ${tier.score}/${tier.of}) ${tier.effectivePct}% of $${r2(bank)}`;
371
+ if (tier.capped)
372
+ clamps.push({ rule: 'tier-unproven-cap', fromUsd: r2((bank * tier.configuredPct) / 100), toUsd: r2(usd) });
369
373
  }
374
+ else
375
+ switch (z.mode) {
376
+ case 'fixed':
377
+ usd = z.fixedUsd ?? 0;
378
+ basis = `fixed $${z.fixedUsd}`;
379
+ break;
380
+ case 'pct':
381
+ usd = (bank * (z.pct ?? 0)) / 100;
382
+ basis = `${z.pct}% of $${r2(bank)}`;
383
+ break;
384
+ case 'risk': {
385
+ const stop = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
386
+ usd = (bank * (z.riskPct ?? 0)) / stop;
387
+ basis = `${z.riskPct}% risk / ${stop}% stop of $${r2(bank)}`;
388
+ break;
389
+ }
390
+ case 'confidence': {
391
+ const c = z.confidence ?? { minPct: 1, maxPct: 1, default: 0.5 };
392
+ const conf = clamp01(req.confidence ?? c.default);
393
+ const pct = c.minPct + (c.maxPct - c.minPct) * conf;
394
+ usd = (bank * pct) / 100;
395
+ basis = `confidence ${r2(conf)} → ${r2(pct)}% of $${r2(bank)}`;
396
+ break;
397
+ }
398
+ }
370
399
  // The agent may ask for LESS than the strategy's pick (never more).
371
400
  const asked = req.sizeUsd ?? (req.sizePct !== undefined ? (bank * req.sizePct) / 100 : undefined);
372
401
  if (asked !== undefined && Number.isFinite(asked)) {
@@ -381,6 +410,12 @@ export function clampEntry(book, k, req, killSwitch = false) {
381
410
  clamps.push({ rule, fromUsd: r2(usd), toUsd: r2(Math.max(0, limit)) });
382
411
  usd = Math.max(0, limit);
383
412
  } };
413
+ // The engine's own ceiling for a tiered entry, whatever the tier pick said: code, not the table or the strategy file.
414
+ if (tier) {
415
+ if (!Number.isFinite(usd))
416
+ usd = 0;
417
+ cap('tier-hard-cap', tierHardCapUsd(bank));
418
+ }
384
419
  if (st.sizeDown)
385
420
  cap('drawdown-size-down', usd * s.risk.drawdown.sizeDownMult);
386
421
  cap('strategy-max-pct', (bank * s.sizing.maxPctOfBankroll) / 100);
@@ -388,14 +423,25 @@ export function clampEntry(book, k, req, killSwitch = false) {
388
423
  cap('strategy-max-usd', s.sizing.maxUsd);
389
424
  const lossPct = profile.hardStop.enabled && profile.hardStop.atPct !== null ? Math.abs(profile.hardStop.atPct) : s.risk.perTrade.assumedLossPctNoStop;
390
425
  cap('per-trade-max-loss', (bank * s.risk.perTrade.maxLossPctOfBankroll) / lossPct);
391
- cap('max-exposure', (bank * s.risk.maxExposurePct) / 100 - book.exposureUsd());
392
- cap('cash', st.cashUsd);
426
+ cap('max-exposure', (bank * s.risk.maxExposurePct) / 100 - book.exposureUsd() - fly.usd);
427
+ cap('cash', st.cashUsd - fly.usd);
393
428
  const R = (k.liq ?? 0) / 2;
394
429
  const imp = s.execution.maxPriceImpactPct / 100;
395
430
  cap('price-impact', (R * imp) / (1 - s.execution.feeBps / 1e4) + s.execution.networkFeeUsd);
396
- if (usd < s.sizing.minUsd)
397
- return no(clamps.length ? `below-min-after-${clamps[clamps.length - 1].rule}` : 'below-min-size');
431
+ if (usd < s.sizing.minUsd) {
432
+ const r = no(clamps.length ? `below-min-after-${clamps[clamps.length - 1].rule}` : 'below-min-size');
433
+ return tier ? { ...r, tier } : r;
434
+ }
398
435
  const first = z.ladder ? usd * z.ladder[0].frac : usd;
399
- return { ok: true, usd: r2(usd), firstTrancheUsd: r2(first), profile, clamps, basis };
436
+ return { ok: true, usd: r2(usd), firstTrancheUsd: r2(first), profile, clamps, basis, ...(tier ? { tier } : {}) };
437
+ }
438
+ /** In-flight entries, failing closed: a negative or non-finite count is one open entry, such dollars are the whole room. */
439
+ const inFlightOf = (f) => ({
440
+ opens: f === undefined ? 0 : Number.isFinite(f.opens) && f.opens >= 0 ? f.opens : 1,
441
+ usd: f === undefined ? 0 : Number.isFinite(f.usd) && f.usd >= 0 ? f.usd : Infinity,
442
+ });
443
+ /** The most a tiered position may cost in total (every tranche and add): TIERS_HARD_CAP_PCT of the book. */
444
+ export function tierHardCapUsd(bookUsd) {
445
+ return Number.isFinite(bookUsd) && bookUsd > 0 ? (bookUsd * TIERS_HARD_CAP_PCT) / 100 : 0;
400
446
  }
401
447
  const clamp01 = (x) => (Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : 0.5);
@@ -268,7 +268,7 @@ export class MemberGate extends SessionGate {
268
268
  overDay() { const o = super.overDay(); if (o)
269
269
  return o; const g = this.fleet.gate.overDay(); return g ? `fleet-${g}` : null; }
270
270
  rateLimit(r) { super.rateLimit(r); this.fleet.rateLimit(r, this); }
271
- block(reason) { super.block(reason); this.fleet.block(reason, this); }
271
+ block(reason, sessionId) { super.block(reason, sessionId); this.fleet.block(reason, this); }
272
272
  paused() { return super.paused() || this.fleet.paused(); }
273
273
  /** A reading another strategy's session made (the same Claude account). */
274
274
  shareRateLimit(r) { super.rateLimit(r); }
@@ -543,7 +543,7 @@ export async function runFleet(o) {
543
543
  l.done = runAutotrade({
544
544
  ...(adopt?.length ? { adopt } : {}),
545
545
  strategy: spec.strategy, source: { name: o.tape?.source ?? o.source.name, ticks, startT: now }, sessions, gate, ledger, runId,
546
- dossier: o.dossier?.(spec), vet: o.vet?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
546
+ dossier: o.dossier?.(spec), vet: o.vet?.(spec), drain: o.drain?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
547
547
  pacing, ...(account ? { account, member: id } : {}),
548
548
  safety: () => {
549
549
  const g = o.globalSafety();
@@ -22,6 +22,7 @@
22
22
  // execution-port where an approved entry goes: the paper port (local single-use proofs) and the live stub (task-1527)
23
23
  // chat Telegram notify, control and approve-to-trade through the user's own bot; chat/ (task-1527)
24
24
  // yolo task-1532: the full-port replay (rugs, loss spread, how often it zeroed the wallet); paper only
25
+ // tiers 0.2.2: EXPERIMENTAL, UNPROVEN confidence-tiered sizing (sizing.tiers), PAPER ONLY, default off
25
26
  // signal / outbox / signal-channels / receipts / real-mode task-1645: mode real = the paper engine + a LIVE TRADE FIRE
26
27
  // signal per fill to the user's own executor (webhook, command hook, JSONL, MCP feed, Telegram copy); no key
27
28
  // limits / executor-health / report task-1654: every real-mode limit user-set per scope (stricter wins), executor
@@ -55,6 +56,9 @@ export * from "./trade-ledger.js";
55
56
  export * from "./execution-port.js";
56
57
  export * from "./chat/index.js";
57
58
  export * from "./yolo.js";
59
+ export * from "./rugproof.js";
60
+ export * from "./rugproof-probe.js";
61
+ export * from "./tiers.js";
58
62
  export * from "./signal.js";
59
63
  export * from "./outbox.js";
60
64
  export * from "./signal-channels.js";
@@ -0,0 +1,415 @@
1
+ // dexterm autotrade — the rug-proof probes (task-1748): the network half of rugproof.ts. READ-ONLY, PAPER ONLY.
2
+ //
3
+ // DexScreener tokens/v1 the coin's deepest pool: dex, base / quote mint, creation time, liquidity keyless
4
+ // Solana RPC the PumpSwap pool account (creator, lp mint, vaults), the LP mint's supply and
5
+ // largest holders, the pool's newest transactions (Withdraw scan), the SOL vault public or the user's own key
6
+ // Jupiter lite quote token -> SOL for a given raw amount keyless
7
+ //
8
+ // No wallet, no key of any wallet, no signing, no send: only reads. An RPC URL may carry an api key: it is never
9
+ // printed (redactSecrets) and never written to the ledger. Every read has a timeout; a read that fails is `error`
10
+ // and the gate that needed it refuses (fail closed). The vet runs the gates in rugproof.ts order and stops at the first
11
+ // refusal, so a coin refused by a free gate costs no network call.
12
+ import { redactSecrets, base58 } from '../../data/src/index.js';
13
+ import { PUMPSWAP_PROGRAM, WSOL_MINT, drainVerdict, gateHolders, gateLiquidityRecheck, gateLpLocked, gatePoolAge, gatePoolStructure, gateSellRoute, gateWithdrawHistory, verdictOf, } from "./rugproof.js";
14
+ export const defaultFetchJson = (timeoutMs = 12_000) => async (url, init) => {
15
+ const r = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
16
+ let json = null;
17
+ try {
18
+ json = await r.json();
19
+ }
20
+ catch {
21
+ json = null;
22
+ }
23
+ return { status: r.status, json };
24
+ };
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
+ const isObj = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
27
+ const str = (x) => (typeof x === 'string' && x ? x : null);
28
+ const fin = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
29
+ export class RugProbe {
30
+ id = 0;
31
+ d;
32
+ /** Per-run cache: a pool's structure does not change, its LP and history are re-read at most every 10 min. */
33
+ lpCache = new Map();
34
+ decimals = new Map();
35
+ rpcCalls = 0;
36
+ /** Backoff unit after a rate limit (ms); tests set 0. */
37
+ backoffMs;
38
+ constructor(d) { this.d = d; this.backoffMs = d.backoffMs ?? 1500; }
39
+ async rpc(method, params) {
40
+ if (!this.d.rpcUrl)
41
+ throw new Error('no RPC endpoint');
42
+ let r = { status: 0, json: null };
43
+ // A rate limit (HTTP 429 or JSON-RPC -32429) is retried twice with a backoff before the read counts as failed.
44
+ for (let attempt = 0; attempt < 3; attempt++) {
45
+ this.rpcCalls++;
46
+ try {
47
+ r = await this.d.fetchJson(this.d.rpcUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++this.id, method, params }) });
48
+ }
49
+ catch (e) {
50
+ throw new Error(redactSecrets(`rpc ${method}: ${e.message}`).slice(0, 200));
51
+ }
52
+ const limited = r.status === 429 || (isObj(r.json) && isObj(r.json.error) && r.json.error.code === -32429);
53
+ if (!limited || attempt === 2)
54
+ break;
55
+ await sleep(this.backoffMs * (attempt + 1));
56
+ }
57
+ if (!isObj(r.json))
58
+ throw new Error(`rpc ${method}: HTTP ${r.status}`);
59
+ if (r.json.error)
60
+ throw new Error(redactSecrets(`rpc ${method}: ${JSON.stringify(r.json.error)}`).slice(0, 200));
61
+ return r.json.result;
62
+ }
63
+ pairCache = new Map();
64
+ scanCache = new Map();
65
+ /** The coin's deepest DexScreener pool (shared for 30 s: every fleet member asks about the same coin). */
66
+ pair(mint, now = Date.now()) {
67
+ const hit = this.pairCache.get(mint);
68
+ if (hit && now - hit.t < 30_000)
69
+ return hit.v;
70
+ const v = this.readPair(mint);
71
+ this.pairCache.set(mint, { t: now, v });
72
+ v.catch(() => this.pairCache.delete(mint));
73
+ return v;
74
+ }
75
+ async readPair(mint) {
76
+ const r = await this.d.fetchJson(`${this.d.dexscreenerBase ?? 'https://api.dexscreener.com'}/tokens/v1/solana/${mint}`);
77
+ if (!Array.isArray(r.json))
78
+ throw new Error(`dexscreener HTTP ${r.status}`);
79
+ const pairs = r.json.filter((p) => isObj(p) && str(p.pairAddress)).sort((a, b) => (fin(b.liquidity?.usd) ?? 0) - (fin(a.liquidity?.usd) ?? 0));
80
+ const p = pairs[0];
81
+ if (!p)
82
+ return null;
83
+ return { address: p.pairAddress, dex: str(p.dexId) ?? 'unknown', baseMint: str(p.baseToken?.address) ?? '', quoteMint: str(p.quoteToken?.address) ?? '', createdAt: fin(p.pairCreatedAt), liqUsd: fin(p.liquidity?.usd) };
84
+ }
85
+ /** The PumpSwap pool account + its LP mint (supply, largest holders' owners). null = not a PumpSwap account. */
86
+ async poolLp(pool, now = Date.now()) {
87
+ const hit = this.lpCache.get(pool);
88
+ if (hit && now - hit.t < 10 * 60e3 && hit.v !== 'error')
89
+ return hit.v;
90
+ let v;
91
+ try {
92
+ v = await this.readPoolLp(pool);
93
+ }
94
+ catch {
95
+ v = 'error';
96
+ }
97
+ this.lpCache.set(pool, { t: now, v });
98
+ return v;
99
+ }
100
+ async readPoolLp(pool) {
101
+ const acc = await this.rpc('getAccountInfo', [pool, { encoding: 'base64' }]);
102
+ const value = isObj(acc) ? acc.value : null;
103
+ if (!value)
104
+ return null;
105
+ if (value.owner !== PUMPSWAP_PROGRAM)
106
+ return null;
107
+ const data = Buffer.from(String(value.data[0] ?? ''), 'base64');
108
+ const lp = decodePumpSwapPool(data);
109
+ const sup = await this.rpc('getTokenSupply', [lp.lpMint]);
110
+ const lpSupply = BigInt(String((sup?.value?.amount) ?? '0'));
111
+ let holders = [];
112
+ if (lpSupply > 0n) {
113
+ const big = await this.rpc('getTokenLargestAccounts', [lp.lpMint]);
114
+ const rows = ((big?.value ?? []).filter(isObj)).slice(0, 5);
115
+ const accs = rows.length ? await this.rpc('getMultipleAccounts', [rows.map((x) => x.address), { encoding: 'jsonParsed' }]) : null;
116
+ const vals = accs?.value ?? [];
117
+ holders = rows.map((x, i) => {
118
+ const owner = str(vals[i]?.data?.parsed?.info?.owner) ?? 'unknown';
119
+ return { owner, amount: BigInt(String(x.amount ?? '0')) };
120
+ }).sort((a, b) => (b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0));
121
+ }
122
+ return { ...lp, lpSupply, holders };
123
+ }
124
+ /**
125
+ * `Withdraw` instructions among the pool's newest `n` successful transactions (and how many the creator signed).
126
+ * Fails closed: one transaction that cannot be read (after the rpc's rate-limit retries) could be the creator's
127
+ * Withdraw, so any unread one makes the whole scan `error` (the gate refuses, the vet looks again in 5 min).
128
+ */
129
+ async withdraws(pool, creator, n, now = Date.now()) {
130
+ const hit = this.scanCache.get(pool);
131
+ if (hit && now - hit.t < 10 * 60e3)
132
+ return hit.v;
133
+ const v = await this.scanWithdraws(pool, creator, n);
134
+ if (v !== 'error')
135
+ this.scanCache.set(pool, { t: now, v });
136
+ return v;
137
+ }
138
+ async scanWithdraws(pool, creator, n) {
139
+ try {
140
+ const sigs = await this.rpc('getSignaturesForAddress', [pool, { limit: Math.min(100, n * 2) }]);
141
+ const ok = (Array.isArray(sigs) ? sigs : []).filter((x) => isObj(x) && !x.err).slice(0, n);
142
+ let withdraws = 0;
143
+ let byCreator = 0;
144
+ let scanned = 0;
145
+ for (const s of ok) {
146
+ // maxSupportedTransactionVersion 1: Helius answers -32015 for a version-1 transaction under 0 (task-1394).
147
+ let tx = null;
148
+ try {
149
+ tx = await this.rpc('getTransaction', [s.signature, { encoding: 'jsonParsed', maxSupportedTransactionVersion: 1 }]);
150
+ }
151
+ catch {
152
+ return 'error';
153
+ }
154
+ if (!isObj(tx))
155
+ return 'error';
156
+ scanned++;
157
+ const logs = tx.meta?.logMessages ?? [];
158
+ if (!logs.some((l) => l === 'Program log: Instruction: Withdraw'))
159
+ continue;
160
+ withdraws++;
161
+ const keys = tx.transaction?.message?.accountKeys ?? [];
162
+ const signer = keys.map((k) => (isObj(k) ? k.pubkey : k))[0];
163
+ if (signer === creator)
164
+ byCreator++;
165
+ }
166
+ return { scanned, withdraws, byCreator };
167
+ }
168
+ catch {
169
+ return 'error';
170
+ }
171
+ }
172
+ async tokenDecimals(mint) {
173
+ const hit = this.decimals.get(mint);
174
+ if (hit !== undefined)
175
+ return hit;
176
+ try {
177
+ const sup = await this.rpc('getTokenSupply', [mint]);
178
+ const d = fin(sup?.value?.decimals);
179
+ if (d !== null) {
180
+ this.decimals.set(mint, d);
181
+ return d;
182
+ }
183
+ }
184
+ catch { /* below */ }
185
+ // pump.fun mints are 6 decimals by construction.
186
+ return mint.endsWith('pump') ? 6 : null;
187
+ }
188
+ /** Jupiter token -> SOL quote for `raw` units. HTTP 429 / a network failure is not a quote error. */
189
+ async route(mint, raw) {
190
+ if (raw <= 0n)
191
+ return { ok: false, kind: 'no-route', error: 'zero amount' };
192
+ let r = { status: 0, json: null };
193
+ for (let attempt = 0; attempt < 3; attempt++) {
194
+ try {
195
+ r = await this.d.fetchJson(`${this.d.jupiterBase ?? 'https://lite-api.jup.ag'}/swap/v1/quote?inputMint=${mint}&outputMint=${WSOL_MINT}&amount=${raw}&slippageBps=1000`);
196
+ }
197
+ catch (e) {
198
+ return { ok: false, kind: 'network', error: e.message.slice(0, 120) };
199
+ }
200
+ if (r.status !== 429 || attempt === 2)
201
+ break;
202
+ await sleep(this.backoffMs * (attempt + 1));
203
+ }
204
+ if (r.status === 429)
205
+ return { ok: false, kind: 'rate-limited', error: 'HTTP 429' };
206
+ const j = isObj(r.json) ? r.json : {};
207
+ if (str(j.outAmount) && !j.errorCode && !j.error)
208
+ return { ok: true, outLamports: BigInt(j.outAmount), impactPct: fin(Number(j.priceImpactPct)) === null ? null : Math.round(Number(j.priceImpactPct) * 10000) / 100 };
209
+ if (r.status >= 500 && !j.errorCode)
210
+ return { ok: false, kind: 'network', error: `HTTP ${r.status}` };
211
+ return { ok: false, kind: 'no-route', error: String(j.errorCode ?? j.error ?? `HTTP ${r.status}`).slice(0, 120) };
212
+ }
213
+ /** The pool's SOL vault balance (SOL). */
214
+ async vaultSol(vault) {
215
+ try {
216
+ const b = await this.rpc('getTokenAccountBalance', [vault]);
217
+ const v = b?.value ?? {};
218
+ return fin(v.uiAmount) ?? (str(v.amount) ? Number(v.amount) / 1e9 : null);
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ }
225
+ /** PumpSwap `Pool`: disc 8, bump u8, index u16, creator, base_mint, quote_mint, lp_mint, base vault, quote vault, lp_supply u64, coin_creator. */
226
+ export function decodePumpSwapPool(d) {
227
+ if (d.length < 211)
228
+ throw new Error(`PumpSwap pool account too short (${d.length} bytes)`);
229
+ const key = (o) => base58(d.subarray(o, o + 32));
230
+ return { creator: key(11), baseMint: key(43), quoteMint: key(75), lpMint: key(107), baseVault: key(139), quoteVault: key(171) };
231
+ }
232
+ // ───────────────────────────── the vet ─────────────────────────────
233
+ export const UNKNOWN_RETRY_MS = 5 * 60e3;
234
+ /**
235
+ * The strategy's rug-proof gates as a runner vet: free gates first (tick + history), then the pair, the pool account,
236
+ * the withdraw scan and the Jupiter route. The first refusal ends it; a time gate asks to be retried.
237
+ */
238
+ export function rugproofVet(r, o) {
239
+ return async (k, ctx) => {
240
+ const results = [];
241
+ const now = k.t;
242
+ const done = (x) => {
243
+ results.push(x);
244
+ if (x.pass)
245
+ return null;
246
+ o.log?.({ mint: k.mint, symbol: k.symbol, gate: x.gate, rule: x.rule, detail: x.detail, results: results.map((y) => `${y.gate}:${y.pass ? 'ok' : 'no'}`) });
247
+ // A read that failed (`-unknown`: a rate limit, an RPC outage) refuses now (fail closed) and looks again in 5 min,
248
+ // instead of skipping the coin for the rest of the run; a structural refusal is for good.
249
+ const retry = x.retryAfterMs ?? (x.rule.endsWith('-unknown') ? UNKNOWN_RETRY_MS : undefined);
250
+ return { ok: false, rule: x.rule, detail: x.detail, ...(retry !== undefined ? { retryAfterMs: retry } : {}) };
251
+ };
252
+ const history = ctx?.history ?? [];
253
+ const sizeUsd = ctx?.sizeUsd ?? 0;
254
+ const floor = ctx?.exitFloorUsd ?? 2000;
255
+ // 1-3: free.
256
+ let f = done(gatePoolAge(r, k, undefined, now));
257
+ if (f)
258
+ return f;
259
+ f = done(gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders));
260
+ if (f)
261
+ return f;
262
+ f = done(gateLiquidityRecheck(r, history, k, floor));
263
+ if (f)
264
+ return f;
265
+ // 4: the pool as listed.
266
+ let pair = null;
267
+ let pairErr = null;
268
+ try {
269
+ pair = await o.probe.pair(k.mint);
270
+ }
271
+ catch (e) {
272
+ pairErr = e.message.slice(0, 160);
273
+ }
274
+ results.shift(); // the token-age pass above is replaced by the pool's own age
275
+ if (pairErr !== null)
276
+ return done({ gate: 'pool-structure', pass: false, rule: 'rugproof:pool-structure-unknown', detail: `the pool could not be read: ${pairErr}` });
277
+ f = done(gatePoolAge(r, k, pair ? pair.createdAt : null, now));
278
+ if (f)
279
+ return f;
280
+ f = done(gatePoolStructure(r, k.mint, k.launchpad, pair));
281
+ if (f)
282
+ return f;
283
+ // 5-6: the chain.
284
+ let lp = null;
285
+ if (r.requireSafePool && pair) {
286
+ lp = o.probe.d.rpcUrl ? await o.probe.poolLp(pair.address, now) : 'error';
287
+ f = done(gateLpLocked(r, pair, lp, k.mint));
288
+ if (f)
289
+ return f;
290
+ const poolLp = lp;
291
+ const scan = poolLp.lpSupply === 0n && r.maxWithdraws === 0 ? null : await o.probe.withdraws(pair.address, poolLp.creator, r.withdrawScanTx);
292
+ f = done(gateWithdrawHistory(r, scan, poolLp));
293
+ if (f)
294
+ return f;
295
+ }
296
+ // 7: a sell route at routeX x the size.
297
+ if (r.routeX !== null) {
298
+ const dec = await o.probe.tokenDecimals(k.mint);
299
+ let route = null;
300
+ if (dec !== null && k.price) {
301
+ const tokens = (sizeUsd * r.routeX) / k.price;
302
+ route = await o.probe.route(k.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** dec))));
303
+ }
304
+ f = done(gateSellRoute(r, route, sizeUsd));
305
+ if (f)
306
+ return f;
307
+ }
308
+ const v = verdictOf(r, results, pair?.createdAt ? now - pair.createdAt : null);
309
+ const note = `rugproof ok: ${results.map((x) => `${x.gate}${x.partial ? '~' : ''}`).join(', ')}${v.sizeCapPct !== null ? `; size cap ${v.sizeCapPct}% (young or partly verified pool)` : ''}`;
310
+ o.log?.({ mint: k.mint, symbol: k.symbol, pass: true, sizeCapPct: v.sizeCapPct, results: results.map((x) => `${x.gate}:${x.partial ? 'partial' : 'ok'}`) });
311
+ return { ok: true, note, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}), ...(pair ? { pool: { address: pair.address, quoteVault: lp && lp !== 'error' ? lp.quoteVault : null } } : {}) };
312
+ };
313
+ }
314
+ /**
315
+ * Polls every open position's pool (SOL vault when the pool is known, plus a Jupiter quote for the held amount)
316
+ * every pollSec, outside the candle / tick cadence. The runner reads `verdict()` on every step and sells the whole
317
+ * position on an exit verdict. Timers are unref'd: they never keep a process alive.
318
+ */
319
+ export class DrainPoller {
320
+ w = new Map();
321
+ g;
322
+ probe;
323
+ log;
324
+ constructor(g, probe, log) { this.g = g; this.probe = probe; this.log = log; }
325
+ watch(p) {
326
+ if (this.w.has(p.positionId))
327
+ return;
328
+ const x = { pos: p, base: { reserveSol: null, liqUsd: p.entryLiqUsd }, timer: null, verdict: null, reads: 0, quoteErrors: 0, noRoute: 0, busy: false, decimals: null };
329
+ this.w.set(p.positionId, x);
330
+ void this.poll(x, true);
331
+ x.timer = setInterval(() => { void this.poll(x, false); }, Math.max(2, Math.min(10, this.g.pollSec)) * 1000);
332
+ x.timer.unref?.();
333
+ }
334
+ unwatch(positionId) {
335
+ const x = this.w.get(positionId);
336
+ if (x?.timer)
337
+ clearInterval(x.timer);
338
+ this.w.delete(positionId);
339
+ }
340
+ verdict(positionId) {
341
+ const v = this.w.get(positionId)?.verdict ?? null;
342
+ return v && v.exit ? v : null;
343
+ }
344
+ stop() { for (const id of [...this.w.keys()])
345
+ this.unwatch(id); }
346
+ /** One probe of one position (also called by tests). */
347
+ async poll(x, first) {
348
+ if (x.busy)
349
+ return;
350
+ x.busy = true;
351
+ try {
352
+ let vault = x.pos.quoteVault ?? null;
353
+ if (!vault && x.pos.poolAddress && this.probe.d.rpcUrl) {
354
+ const lp = await this.probe.poolLp(x.pos.poolAddress);
355
+ if (lp && lp !== 'error')
356
+ vault = x.pos.quoteVault = lp.quoteVault;
357
+ }
358
+ const reserveSol = vault && this.probe.d.rpcUrl ? await this.probe.vaultSol(vault) : null;
359
+ if (first || x.base.reserveSol === null) {
360
+ if (reserveSol !== null)
361
+ x.base.reserveSol = reserveSol;
362
+ }
363
+ x.decimals ??= await this.probe.tokenDecimals(x.pos.mint);
364
+ const tokens = x.pos.tokens();
365
+ const route = this.g.exitOnNoRoute && x.decimals !== null && tokens > 0 ? await this.probe.route(x.pos.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** x.decimals)))) : null;
366
+ x.reads++;
367
+ if (route && !route.ok && route.kind !== 'no-route')
368
+ x.quoteErrors++;
369
+ let v = drainVerdict(this.g, x.base, { t: Date.now(), reserveSol, liqUsd: null, route });
370
+ // A quote error alone needs two reads in a row (one pollSec apart): a single odd answer is not a pull. A reserve
371
+ // read decides at once. The poller keeps polling after an exit: a route that comes back lifts the verdict.
372
+ x.noRoute = v.exit && v.reason === 'drain-no-route' ? x.noRoute + 1 : 0;
373
+ if (v.exit && v.reason === 'drain-no-route' && x.noRoute < 2)
374
+ v = { exit: false, note: 'no route once: re-checking' };
375
+ const was = x.verdict?.exit ?? false;
376
+ x.verdict = v;
377
+ if (v.exit && !was)
378
+ this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, drain: v.reason, detail: v.detail, reads: x.reads });
379
+ }
380
+ catch (e) {
381
+ this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, probeError: e.message.slice(0, 160) });
382
+ }
383
+ finally {
384
+ x.busy = false;
385
+ }
386
+ }
387
+ stats() {
388
+ let reads = 0;
389
+ let quoteErrors = 0;
390
+ for (const x of this.w.values()) {
391
+ reads += x.reads;
392
+ quoteErrors += x.quoteErrors;
393
+ }
394
+ return { watched: this.w.size, reads, quoteErrors };
395
+ }
396
+ }
397
+ /** Run vets in order (the rug-proof gates first: they are the cheapest); the first refusal wins; notes and caps merge. */
398
+ export function chainVets(...vets) {
399
+ const vs = vets.filter((v) => !!v);
400
+ if (vs.length <= 1)
401
+ return vs[0];
402
+ return async (k, ctx) => {
403
+ const notes = [];
404
+ let out = { ok: true };
405
+ for (const v of vs) {
406
+ const r = await v(k, ctx);
407
+ if (!r.ok)
408
+ return r;
409
+ if (r.note)
410
+ notes.push(r.note);
411
+ out = { ...out, ...r, recheck: !!(out.ok && out.recheck) || !!r.recheck, ...(out.ok && out.sizeCapPct !== undefined && (r.sizeCapPct === undefined || out.sizeCapPct < r.sizeCapPct) ? { sizeCapPct: out.sizeCapPct } : {}) };
412
+ }
413
+ return notes.length ? { ...out, note: notes.join('; ') } : out;
414
+ };
415
+ }