dexterm-cli 0.0.0-stage → 0.1.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 (377) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +394 -0
  4. package/dist/packages/agents/src/executor.js +202 -0
  5. package/dist/packages/agents/src/gate.js +179 -0
  6. package/dist/packages/agents/src/identity.js +218 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +119 -0
  12. package/dist/packages/agents/src/tools.js +72 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +336 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
  28. package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
  29. package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
  30. package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
  31. package/dist/packages/cli/src/alerts/creds.js +83 -0
  32. package/dist/packages/cli/src/alerts/format.js +41 -0
  33. package/dist/packages/cli/src/alerts/index.js +10 -0
  34. package/dist/packages/cli/src/alerts/notifier.js +53 -0
  35. package/dist/packages/cli/src/alerts/route.js +19 -0
  36. package/dist/packages/cli/src/alerts/types.js +1 -0
  37. package/dist/packages/cli/src/alerts-cmd.js +212 -0
  38. package/dist/packages/cli/src/analyst/backend.js +526 -0
  39. package/dist/packages/cli/src/analyst/results.js +38 -0
  40. package/dist/packages/cli/src/analyst/session.js +319 -0
  41. package/dist/packages/cli/src/audit.js +51 -0
  42. package/dist/packages/cli/src/backtest-cmd.js +208 -0
  43. package/dist/packages/cli/src/boot-state.js +62 -0
  44. package/dist/packages/cli/src/coin-target.js +2 -0
  45. package/dist/packages/cli/src/custom.js +261 -0
  46. package/dist/packages/cli/src/display.js +191 -0
  47. package/dist/packages/cli/src/doctor.js +375 -0
  48. package/dist/packages/cli/src/export.js +235 -0
  49. package/dist/packages/cli/src/feed.js +634 -0
  50. package/dist/packages/cli/src/fixtures.js +275 -0
  51. package/dist/packages/cli/src/follow-cmd.js +221 -0
  52. package/dist/packages/cli/src/images-config.js +59 -0
  53. package/dist/packages/cli/src/index.js +31 -0
  54. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  55. package/dist/packages/cli/src/larp-record.js +21 -0
  56. package/dist/packages/cli/src/live/copy.js +80 -0
  57. package/dist/packages/cli/src/live/flow.js +233 -0
  58. package/dist/packages/cli/src/live/index.js +511 -0
  59. package/dist/packages/cli/src/live/intel.js +227 -0
  60. package/dist/packages/cli/src/live/newpairs.js +334 -0
  61. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  62. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  63. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  64. package/dist/packages/cli/src/live/token.js +585 -0
  65. package/dist/packages/cli/src/live/trade.js +929 -0
  66. package/dist/packages/cli/src/live/watchlist.js +178 -0
  67. package/dist/packages/cli/src/main.js +5785 -0
  68. package/dist/packages/cli/src/mcp-cmd.js +326 -0
  69. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  70. package/dist/packages/cli/src/pack-cmd.js +323 -0
  71. package/dist/packages/cli/src/plan-cmd.js +278 -0
  72. package/dist/packages/cli/src/popup.js +810 -0
  73. package/dist/packages/cli/src/rules-cmd.js +131 -0
  74. package/dist/packages/cli/src/rules.js +358 -0
  75. package/dist/packages/cli/src/scan/command.js +57 -0
  76. package/dist/packages/cli/src/scan/engine.js +419 -0
  77. package/dist/packages/cli/src/scan/observe.js +64 -0
  78. package/dist/packages/cli/src/scan/ui.js +98 -0
  79. package/dist/packages/cli/src/screener/filter.js +241 -0
  80. package/dist/packages/cli/src/screener/presets.js +80 -0
  81. package/dist/packages/cli/src/screener/rows.js +146 -0
  82. package/dist/packages/cli/src/screener/rug.js +50 -0
  83. package/dist/packages/cli/src/screener/sort.js +52 -0
  84. package/dist/packages/cli/src/screener/view.js +183 -0
  85. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  86. package/dist/packages/cli/src/screens/analyst.js +326 -0
  87. package/dist/packages/cli/src/screens/boot.js +74 -0
  88. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  89. package/dist/packages/cli/src/screens/coin.js +33 -0
  90. package/dist/packages/cli/src/screens/follows.js +201 -0
  91. package/dist/packages/cli/src/screens/kol.js +90 -0
  92. package/dist/packages/cli/src/screens/narrative.js +90 -0
  93. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  94. package/dist/packages/cli/src/screens/overlay.js +257 -0
  95. package/dist/packages/cli/src/screens/plans.js +126 -0
  96. package/dist/packages/cli/src/screens/positions.js +176 -0
  97. package/dist/packages/cli/src/screens/rules.js +170 -0
  98. package/dist/packages/cli/src/screens/screener.js +459 -0
  99. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  100. package/dist/packages/cli/src/screens/ticket.js +342 -0
  101. package/dist/packages/cli/src/screens/token.js +312 -0
  102. package/dist/packages/cli/src/screens/trending.js +181 -0
  103. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  104. package/dist/packages/cli/src/skill-eval-build.js +109 -0
  105. package/dist/packages/cli/src/social/demo.js +96 -0
  106. package/dist/packages/cli/src/social/view.js +250 -0
  107. package/dist/packages/cli/src/state.js +463 -0
  108. package/dist/packages/cli/src/theme-cmd.js +126 -0
  109. package/dist/packages/cli/src/trade.js +319 -0
  110. package/dist/packages/cli/src/update.js +294 -0
  111. package/dist/packages/cli/src/version.js +3 -0
  112. package/dist/packages/cli/src/voice-cmd.js +119 -0
  113. package/dist/packages/cli/src/voice-hud.js +42 -0
  114. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  115. package/dist/packages/cli/src/wallet-command.js +31 -0
  116. package/dist/packages/cli/src/watch-cmd.js +190 -0
  117. package/dist/packages/cli/src/widgets.js +208 -0
  118. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  119. package/dist/packages/cli/src/x-cmd.js +154 -0
  120. package/dist/packages/custom/src/config.js +213 -0
  121. package/dist/packages/custom/src/index.js +10 -0
  122. package/dist/packages/custom/src/keymap.js +107 -0
  123. package/dist/packages/custom/src/layout.js +235 -0
  124. package/dist/packages/custom/src/packs.js +633 -0
  125. package/dist/packages/custom/src/rules.js +704 -0
  126. package/dist/packages/custom/src/schema.js +538 -0
  127. package/dist/packages/custom/src/themes.js +176 -0
  128. package/dist/packages/custom/src/widget-html.js +222 -0
  129. package/dist/packages/custom/src/workspace.js +141 -0
  130. package/dist/packages/data/src/analyst-actions.js +133 -0
  131. package/dist/packages/data/src/budget.js +88 -0
  132. package/dist/packages/data/src/cache.js +142 -0
  133. package/dist/packages/data/src/candles.js +168 -0
  134. package/dist/packages/data/src/clock.js +64 -0
  135. package/dist/packages/data/src/coin-target.js +82 -0
  136. package/dist/packages/data/src/credits.js +137 -0
  137. package/dist/packages/data/src/detail.js +97 -0
  138. package/dist/packages/data/src/fast-lane.js +321 -0
  139. package/dist/packages/data/src/fixtures.js +135 -0
  140. package/dist/packages/data/src/health.js +245 -0
  141. package/dist/packages/data/src/http.js +277 -0
  142. package/dist/packages/data/src/images/cache.js +141 -0
  143. package/dist/packages/data/src/images/decode-worker.js +28 -0
  144. package/dist/packages/data/src/images/decode.js +81 -0
  145. package/dist/packages/data/src/images/fetch.js +162 -0
  146. package/dist/packages/data/src/images/gif.js +214 -0
  147. package/dist/packages/data/src/images/index.js +10 -0
  148. package/dist/packages/data/src/images/jpeg.js +544 -0
  149. package/dist/packages/data/src/images/png.js +310 -0
  150. package/dist/packages/data/src/images/service.js +334 -0
  151. package/dist/packages/data/src/images/types.js +27 -0
  152. package/dist/packages/data/src/index.js +211 -0
  153. package/dist/packages/data/src/intel/helius-history.js +70 -0
  154. package/dist/packages/data/src/intel/holder-class.js +125 -0
  155. package/dist/packages/data/src/intel/rules.js +421 -0
  156. package/dist/packages/data/src/intel/scan.js +170 -0
  157. package/dist/packages/data/src/larp/evidence.js +288 -0
  158. package/dist/packages/data/src/larp/link-baseline.js +196 -0
  159. package/dist/packages/data/src/larp/links.js +137 -0
  160. package/dist/packages/data/src/larp/robots.js +90 -0
  161. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  162. package/dist/packages/data/src/limiter.js +341 -0
  163. package/dist/packages/data/src/model.js +12 -0
  164. package/dist/packages/data/src/paper.js +331 -0
  165. package/dist/packages/data/src/rpc-config.js +126 -0
  166. package/dist/packages/data/src/rpc-pool.js +166 -0
  167. package/dist/packages/data/src/social/chain.js +83 -0
  168. package/dist/packages/data/src/social/dedupe.js +205 -0
  169. package/dist/packages/data/src/social/feed.js +462 -0
  170. package/dist/packages/data/src/social/firewall.js +110 -0
  171. package/dist/packages/data/src/social/index.js +11 -0
  172. package/dist/packages/data/src/social/kol.js +269 -0
  173. package/dist/packages/data/src/social/model.js +11 -0
  174. package/dist/packages/data/src/social/rank.js +184 -0
  175. package/dist/packages/data/src/social/rss.js +113 -0
  176. package/dist/packages/data/src/social/summary.js +104 -0
  177. package/dist/packages/data/src/social/venues.js +186 -0
  178. package/dist/packages/data/src/sources/dexscreener.js +184 -0
  179. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  180. package/dist/packages/data/src/sources/goplus.js +161 -0
  181. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  182. package/dist/packages/data/src/sources/jupiter.js +198 -0
  183. package/dist/packages/data/src/sources/pda.js +113 -0
  184. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  185. package/dist/packages/data/src/sources/pumpportal.js +252 -0
  186. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  187. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  188. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  189. package/dist/packages/data/src/store.js +289 -0
  190. package/dist/packages/data/src/trade/bridge.js +464 -0
  191. package/dist/packages/data/src/trade/caps.js +323 -0
  192. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  193. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  194. package/dist/packages/data/src/trade/copy/index.js +9 -0
  195. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  196. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  197. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  198. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  199. package/dist/packages/data/src/trade/copy/types.js +6 -0
  200. package/dist/packages/data/src/trade/executor.js +933 -0
  201. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  202. package/dist/packages/data/src/trade/link-store.js +52 -0
  203. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  204. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  205. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  206. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  207. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  208. package/dist/packages/data/src/trade/plans/index.js +12 -0
  209. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  210. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  211. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  212. package/dist/packages/data/src/trade/plans/service.js +168 -0
  213. package/dist/packages/data/src/trade/plans/types.js +6 -0
  214. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  215. package/dist/packages/data/src/trade/signer/client.js +197 -0
  216. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  217. package/dist/packages/data/src/trade/signer/host.js +97 -0
  218. package/dist/packages/data/src/trade/signer/main.js +121 -0
  219. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  220. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  221. package/dist/packages/data/src/trade/signer/service.js +400 -0
  222. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  223. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  224. package/dist/packages/data/src/trade/swap-build.js +108 -0
  225. package/dist/packages/data/src/trade/token-delta.js +201 -0
  226. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  227. package/dist/packages/data/src/util.js +24 -0
  228. package/dist/packages/data/src/watchlist.js +176 -0
  229. package/dist/packages/data/src/x/api.js +256 -0
  230. package/dist/packages/data/src/x/budget.js +66 -0
  231. package/dist/packages/data/src/x/config.js +49 -0
  232. package/dist/packages/data/src/x/index.js +10 -0
  233. package/dist/packages/data/src/x/mock-server.js +217 -0
  234. package/dist/packages/data/src/x/oauth.js +242 -0
  235. package/dist/packages/data/src/x/pkce.js +21 -0
  236. package/dist/packages/data/src/x/source.js +285 -0
  237. package/dist/packages/data/src/x/token-store.js +134 -0
  238. package/dist/packages/data/src/x/transport.js +71 -0
  239. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  240. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  241. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  242. package/dist/packages/mcp/assets/cases.json +1194 -0
  243. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  244. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  245. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  246. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  247. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  248. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  249. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  250. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  251. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  252. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  253. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  254. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  255. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  256. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  257. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  258. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  259. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  260. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  261. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  262. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  263. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  264. package/dist/packages/mcp/src/dossier.js +78 -0
  265. package/dist/packages/mcp/src/firewall.js +21 -0
  266. package/dist/packages/mcp/src/index.js +22 -0
  267. package/dist/packages/mcp/src/server.js +936 -0
  268. package/dist/packages/mcp/src/user-content.js +434 -0
  269. package/dist/packages/packs/src/backtest.js +183 -0
  270. package/dist/packages/packs/src/digest.js +95 -0
  271. package/dist/packages/packs/src/firewall.js +57 -0
  272. package/dist/packages/packs/src/index.js +20 -0
  273. package/dist/packages/packs/src/larp-eval.js +100 -0
  274. package/dist/packages/packs/src/narrative.js +309 -0
  275. package/dist/packages/packs/src/reason.js +135 -0
  276. package/dist/packages/packs/src/rug-model.js +137 -0
  277. package/dist/packages/packs/src/schema.js +54 -0
  278. package/dist/packages/packs/src/skill-backtest.js +300 -0
  279. package/dist/packages/packs/src/token-dossier.js +174 -0
  280. package/dist/packages/packs/src/verdicts.js +17 -0
  281. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  282. package/dist/packages/popup/src/config.js +71 -0
  283. package/dist/packages/popup/src/dock.js +271 -0
  284. package/dist/packages/popup/src/host.js +210 -0
  285. package/dist/packages/popup/src/index.js +12 -0
  286. package/dist/packages/popup/src/loopback.js +53 -0
  287. package/dist/packages/popup/src/open.js +183 -0
  288. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  289. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  290. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  291. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  292. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  293. package/dist/packages/popup/src/pages/chart.css +50 -0
  294. package/dist/packages/popup/src/pages/chart.html +56 -0
  295. package/dist/packages/popup/src/pages/chart.js +554 -0
  296. package/dist/packages/popup/src/pages/common.css +34 -0
  297. package/dist/packages/popup/src/pages/common.js +187 -0
  298. package/dist/packages/popup/src/pages/feeds.html +20 -0
  299. package/dist/packages/popup/src/pages/feeds.js +77 -0
  300. package/dist/packages/popup/src/pages/narrative.html +20 -0
  301. package/dist/packages/popup/src/pages/narrative.js +61 -0
  302. package/dist/packages/popup/src/pages/plans.css +30 -0
  303. package/dist/packages/popup/src/pages/plans.html +26 -0
  304. package/dist/packages/popup/src/pages/plans.js +82 -0
  305. package/dist/packages/popup/src/pages/roster.css +41 -0
  306. package/dist/packages/popup/src/pages/roster.html +25 -0
  307. package/dist/packages/popup/src/pages/roster.js +75 -0
  308. package/dist/packages/popup/src/pages/token.html +20 -0
  309. package/dist/packages/popup/src/pages/token.js +112 -0
  310. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  311. package/dist/packages/popup/src/pages/widget.css +5 -0
  312. package/dist/packages/popup/src/pages/widget.html +21 -0
  313. package/dist/packages/popup/src/pages/widget.js +85 -0
  314. package/dist/packages/popup/src/protocol.js +31 -0
  315. package/dist/packages/popup/src/selftest.js +156 -0
  316. package/dist/packages/popup/src/server.js +582 -0
  317. package/dist/packages/popup/src/sinkproxy.js +167 -0
  318. package/dist/packages/popup/src/widget-html.js +231 -0
  319. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  320. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  321. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  322. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  323. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  324. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  325. package/dist/packages/recorder/src/cli.js +113 -0
  326. package/dist/packages/recorder/src/export.js +71 -0
  327. package/dist/packages/recorder/src/index.js +20 -0
  328. package/dist/packages/recorder/src/labels.js +151 -0
  329. package/dist/packages/recorder/src/open.js +29 -0
  330. package/dist/packages/recorder/src/recorder.js +553 -0
  331. package/dist/packages/recorder/src/schedule.js +43 -0
  332. package/dist/packages/recorder/src/schema.js +135 -0
  333. package/dist/packages/recorder/src/snapshot.js +191 -0
  334. package/dist/packages/recorder/src/sweep.js +52 -0
  335. package/dist/packages/recorder/src/verdicts.js +52 -0
  336. package/dist/packages/tui/src/bubbles.js +415 -0
  337. package/dist/packages/tui/src/canvas.js +370 -0
  338. package/dist/packages/tui/src/chrome.js +85 -0
  339. package/dist/packages/tui/src/color.js +133 -0
  340. package/dist/packages/tui/src/flush.js +62 -0
  341. package/dist/packages/tui/src/fmt.js +188 -0
  342. package/dist/packages/tui/src/fx-trade.js +278 -0
  343. package/dist/packages/tui/src/fx.js +527 -0
  344. package/dist/packages/tui/src/images/blocks.js +141 -0
  345. package/dist/packages/tui/src/images/coin.js +22 -0
  346. package/dist/packages/tui/src/images/detect.js +140 -0
  347. package/dist/packages/tui/src/images/index.js +7 -0
  348. package/dist/packages/tui/src/images/layer.js +146 -0
  349. package/dist/packages/tui/src/images/pixels.js +157 -0
  350. package/dist/packages/tui/src/images/protocols.js +87 -0
  351. package/dist/packages/tui/src/index.js +18 -0
  352. package/dist/packages/tui/src/keymap.js +357 -0
  353. package/dist/packages/tui/src/layout.js +11 -0
  354. package/dist/packages/tui/src/raster-font.js +195 -0
  355. package/dist/packages/tui/src/raster.js +213 -0
  356. package/dist/packages/tui/src/sound.js +177 -0
  357. package/dist/packages/tui/src/theme.js +182 -0
  358. package/dist/packages/tui/src/widgets.js +598 -0
  359. package/dist/packages/voice/src/controller.js +407 -0
  360. package/dist/packages/voice/src/doctor.js +131 -0
  361. package/dist/packages/voice/src/download.js +184 -0
  362. package/dist/packages/voice/src/engines.js +84 -0
  363. package/dist/packages/voice/src/index.js +20 -0
  364. package/dist/packages/voice/src/router.js +502 -0
  365. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  366. package/dist/packages/voice/src/stt/apple.js +179 -0
  367. package/dist/packages/voice/src/stt/fake.js +34 -0
  368. package/dist/packages/voice/src/stt/whisper.js +112 -0
  369. package/dist/packages/voice/src/summary.js +44 -0
  370. package/dist/packages/voice/src/tts/espeak.js +131 -0
  371. package/dist/packages/voice/src/tts/fake.js +30 -0
  372. package/dist/packages/voice/src/tts/piper.js +206 -0
  373. package/dist/packages/voice/src/tts/say.js +132 -0
  374. package/dist/packages/voice/src/tts/voices.js +206 -0
  375. package/dist/packages/voice/src/types.js +3 -0
  376. package/package.json +38 -4
  377. package/README.md +0 -3
@@ -0,0 +1,784 @@
1
+ // task-1355 — the PLAN ENGINE: local trigger engine + signer-side policy + state machine + ledger
2
+ // (SPEC §6.13.3/§6.13.5-§6.13.11). Ported from the task-1292 reference simulation
3
+ // (research/task-1292/sim/engine.mjs), typed. Behaviour is intended to be identical to the sim; the
4
+ // ported test suite (packages/data/test/plans-*.test.ts) is the equivalence check.
5
+ //
6
+ // Event-sourced: every mutation is `emit(type, fields)` -> journal.append -> reduce(event). The in-memory
7
+ // state is a pure function of the journal, so crash recovery is `PlanEngine.recover(journal)` followed by
8
+ // settling in-flight sends by signature lookup (never by re-sending). This file holds no keys and does no
9
+ // I/O except through the injected journal and executor.
10
+ //
11
+ // Plan states (card): proposed -> armed -> triggered -> filled -> exiting -> done | failed | cancelled.
12
+ import { createHash } from 'node:crypto';
13
+ import { proposePlan, amendPlan, readBack, planHash, PlanError } from "./plan.js";
14
+ import { draftMandate, armMandate as armMandateObj, mandateAlive, capsHash, TIERS, HARD } from "./mandate.js";
15
+ import { gestureTier } from "./confirm.js";
16
+ import { RpcOutage } from "./executor.js";
17
+ export const TERMINAL = new Set(['done', 'failed', 'cancelled']);
18
+ const LEG_MAX_ATTEMPTS = 3;
19
+ const SEND_UNKNOWN_SETTLE_MS = 90_000; // blockhash lifetime: after this an unfound signature is dead
20
+ export class PolicyRefusal extends Error {
21
+ reason;
22
+ constructor(reason, message) {
23
+ super(message);
24
+ this.name = 'PolicyRefusal';
25
+ this.reason = reason;
26
+ }
27
+ }
28
+ const big = (s) => (typeof s === 'bigint' ? s : BigInt(s));
29
+ const str = (b) => (typeof b === 'bigint' ? b.toString() : b);
30
+ export class PlanEngine {
31
+ clock;
32
+ journal;
33
+ ex;
34
+ gate;
35
+ safety;
36
+ cfg;
37
+ mandates = new Map();
38
+ recs = new Map();
39
+ ticks = new Map();
40
+ batches = new Map();
41
+ panicked = null;
42
+ proofs = new Map();
43
+ #issued = new WeakMap();
44
+ #consumed = new WeakSet();
45
+ #remember(proof) {
46
+ // A caller must not install accessors that change the gesture/subject between validation and use.
47
+ for (const key of Object.keys(proof))
48
+ Object.defineProperty(proof, key, {
49
+ writable: key === 'consumed', configurable: false,
50
+ });
51
+ Object.preventExtensions(proof);
52
+ this.#issued.set(proof, JSON.stringify({ ...proof, consumed: false }));
53
+ return proof;
54
+ }
55
+ #validateProof(proof) {
56
+ if (this.#consumed.has(proof) || proof.consumed)
57
+ throw new PolicyRefusal('proof-consumed', 'proof already used');
58
+ if (this.#issued.get(proof) !== JSON.stringify({ ...proof, consumed: false }))
59
+ throw new PolicyRefusal('unissued-proof', 'proof was not issued unchanged by this engine');
60
+ }
61
+ #consume(proof) { this.#consumed.add(proof); proof.consumed = true; }
62
+ constructor({ clock, journal, executor, gate, safety = () => ({ ok: true }), priceMaxAgeMs = 3_000, debounceTicks = 2, confirmBandPct = 2, sourceDisagreePct = 5 }) {
63
+ this.clock = clock;
64
+ this.journal = journal;
65
+ this.ex = executor;
66
+ this.gate = gate;
67
+ this.safety = safety;
68
+ this.cfg = { priceMaxAgeMs, debounceTicks, confirmBandPct, sourceDisagreePct };
69
+ }
70
+ /** task-1363: the fire path (paper ⇄ live). Gated by PlanService.useLive/usePaper (keypress, no open plans). */
71
+ replaceExecutor(ex) { this.ex = ex; }
72
+ get executor() { return this.ex; }
73
+ /** Plans that are armed, triggered, filled or exiting (anything a fire-path switch would strand). */
74
+ openPlans() { return [...this.recs.values()].filter((r) => !TERMINAL.has(r.status) && r.status !== 'proposed').map((r) => r.plan.id); }
75
+ // ───────────────────────── event sourcing ─────────────────────────
76
+ emit(type, fields = {}) {
77
+ const e = this.journal.append(type, fields);
78
+ this.reduce(e);
79
+ return e;
80
+ }
81
+ reduce(e) {
82
+ const d = e.data ?? {};
83
+ switch (e.type) {
84
+ case 'mandate.draft': {
85
+ const m = structuredClone(d.mandate);
86
+ Object.freeze(m.budget);
87
+ Object.freeze(m.limits);
88
+ Object.freeze(m.fees);
89
+ Object.freeze(m.allowed);
90
+ this.mandates.set(m.id, { m, ledger: { reservedUsd: 0, spentUsd: 0, realizedUsd: 0, lossUsd: 0, feeLamports: 0, sendTs: [] } });
91
+ break;
92
+ }
93
+ case 'mandate.armed': {
94
+ const x = this.mandates.get(e.mandateId);
95
+ x.m.status = 'armed';
96
+ x.m.armedAt = d.armedAt;
97
+ x.m.armedBy = d.armedBy;
98
+ break;
99
+ }
100
+ case 'mandate.revoked': {
101
+ const x = this.mandates.get(e.mandateId);
102
+ x.m.status = d.status ?? 'revoked';
103
+ x.m.revokedAt = e.ts;
104
+ x.m.revokedReason = d.reason;
105
+ break;
106
+ }
107
+ case 'plan.proposed': {
108
+ const plan = Object.freeze(structuredClone(d.plan));
109
+ this.recs.set(plan.id, { plan, status: 'proposed', mandateId: null, tier: null, reservedUsd: 0, legs: {}, pos: { tokenRaw: 0n, entryTokenRaw: 0n, costUsd: 0, soldUsd: 0, realizedUsd: 0 }, paused: null, reason: null, exitIdx: 0 });
110
+ break;
111
+ }
112
+ case 'plan.armed': {
113
+ const r = this.recs.get(e.planId);
114
+ r.status = 'armed';
115
+ r.mandateId = e.mandateId;
116
+ r.tier = d.tier;
117
+ r.reservedUsd = d.reservedUsd;
118
+ this.mandates.get(e.mandateId).ledger.reservedUsd += d.reservedUsd;
119
+ break;
120
+ }
121
+ case 'plan.status': {
122
+ const r = this.recs.get(e.planId);
123
+ r.status = d.status;
124
+ if (d.reason)
125
+ r.reason = d.reason;
126
+ if (d.release && r.reservedUsd) {
127
+ this.mandates.get(r.mandateId).ledger.reservedUsd -= r.reservedUsd;
128
+ r.reservedUsd = 0;
129
+ }
130
+ break;
131
+ }
132
+ case 'plan.paused': {
133
+ this.recs.get(e.planId).paused = d;
134
+ break;
135
+ }
136
+ case 'plan.resumed': {
137
+ const r = this.recs.get(e.planId);
138
+ r.paused = null;
139
+ if (typeof d.supply === 'number')
140
+ r.ackSupply = d.supply;
141
+ break;
142
+ }
143
+ case 'leg.sent': {
144
+ const r = this.recs.get(e.planId);
145
+ r.legs[d.legKey] = { ...(r.legs[d.legKey] ?? { attempts: 0, state: 'pending' }), state: 'in-flight', side: d.side, signature: d.signature, usd: d.usd, tokenRaw: d.tokenRaw == null ? null : big(d.tokenRaw), priceUsd: d.priceUsd, feeLamports: d.feeLamports, sentAt: e.ts, attempts: (r.legs[d.legKey]?.attempts ?? 0) + 1 };
146
+ const L = this.mandates.get(r.mandateId).ledger;
147
+ L.feeLamports += d.feeLamports;
148
+ L.sendTs.push(e.ts);
149
+ break;
150
+ }
151
+ case 'leg.filled': {
152
+ const r = this.recs.get(e.planId);
153
+ const leg = r.legs[d.legKey];
154
+ leg.state = 'filled';
155
+ leg.fill = d.fill;
156
+ const L = this.mandates.get(r.mandateId).ledger;
157
+ const tok = big(d.fill.tokenDelta);
158
+ if (d.side === 'buy') {
159
+ r.pos.tokenRaw += tok;
160
+ r.pos.entryTokenRaw += tok;
161
+ r.pos.costUsd += d.fill.usd;
162
+ L.spentUsd += d.fill.usd;
163
+ L.reservedUsd -= Math.min(r.reservedUsd, d.fill.usd);
164
+ r.reservedUsd = Math.max(0, r.reservedUsd - d.fill.usd);
165
+ }
166
+ else {
167
+ const sold = -tok;
168
+ const costShare = r.pos.entryTokenRaw > 0n ? r.pos.costUsd * Number(sold) / Number(r.pos.entryTokenRaw) : 0;
169
+ r.pos.tokenRaw -= sold;
170
+ r.pos.soldUsd += d.fill.usd;
171
+ const pnl = d.fill.usd - costShare;
172
+ r.pos.realizedUsd += pnl;
173
+ L.realizedUsd += pnl;
174
+ if (pnl < 0)
175
+ L.lossUsd += -pnl;
176
+ }
177
+ break;
178
+ }
179
+ case 'leg.failed': {
180
+ const r = this.recs.get(e.planId);
181
+ const leg = r.legs[d.legKey] ?? (r.legs[d.legKey] = { attempts: 0, state: 'pending' });
182
+ leg.state = d.final ? 'failed' : 'retry';
183
+ leg.reason = d.reason;
184
+ if (d.refundFeeLamports)
185
+ this.mandates.get(r.mandateId).ledger.feeLamports -= d.refundFeeLamports;
186
+ break;
187
+ }
188
+ case 'leg.unknown': {
189
+ const r = this.recs.get(e.planId);
190
+ r.legs[d.legKey].state = 'unknown';
191
+ r.legs[d.legKey].unknownSince = e.ts;
192
+ break;
193
+ }
194
+ case 'panic': {
195
+ this.panicked = { at: e.ts, reason: d.reason };
196
+ break;
197
+ }
198
+ default: break; // audit-only events: confirm.*, settings.*, price.*, policy.refused, trigger.fired, batch.*
199
+ }
200
+ }
201
+ // ───────────────────────── mandates ─────────────────────────
202
+ draftMandate(input) {
203
+ const m = draftMandate(input, { clock: this.clock });
204
+ this.emit('mandate.draft', { mandateId: m.id, data: { mandate: m, capsHash: capsHash(m) } });
205
+ return this.mandates.get(m.id).m;
206
+ }
207
+ /** The signer side observed a physical gesture on the mandate card (TTY key read by the signer process / Phantom approval). */
208
+ issueMandateProof(mandateId, gesture) {
209
+ const x = this.mandates.get(mandateId);
210
+ if (!x)
211
+ throw new PolicyRefusal('unknown-mandate', 'no such mandate');
212
+ const p = { id: `mproof_${this.proofs.size + 1}`, kind: 'gesture', signerIssued: true, gesture, subject: `mandate:${mandateId}:${capsHash(x.m)}`, at: this.clock.now(), consumed: false };
213
+ this.proofs.set(p.id, p);
214
+ return this.#remember(p);
215
+ }
216
+ armMandate(mandateId, proof) {
217
+ const x = this.mandates.get(mandateId);
218
+ if (!x)
219
+ throw new PolicyRefusal('unknown-mandate', 'no such mandate');
220
+ if (this.panicked)
221
+ throw new PolicyRefusal('panicked', 'panic engaged; resume needs the TTY path');
222
+ this.#validateProof(proof);
223
+ armMandateObj(x.m, proof, { clock: this.clock }); // throws MandateError on a voice proof etc.
224
+ this.#consume(proof);
225
+ this.emit('mandate.armed', { mandateId, data: { armedAt: x.m.armedAt, armedBy: x.m.armedBy, capsHash: capsHash(x.m) } });
226
+ return x.m;
227
+ }
228
+ revokeMandate(mandateId, reason, status = 'revoked') {
229
+ const x = this.mandates.get(mandateId);
230
+ if (!x || x.m.status !== 'armed')
231
+ return;
232
+ this.emit('mandate.revoked', { mandateId, data: { reason, status } });
233
+ for (const r of this.recs.values())
234
+ if (r.mandateId === mandateId && !TERMINAL.has(r.status))
235
+ this.cancelPlan(r.plan.id, `mandate ${status}: ${reason}`);
236
+ }
237
+ // ───────────────────────── plans ─────────────────────────
238
+ propose(input, ctx) {
239
+ const plan = proposePlan(input, { ...ctx, clock: this.clock });
240
+ this.emit('plan.proposed', { planId: plan.id, data: { plan, by: plan.source.by, ref: plan.source.ref } });
241
+ return this.recs.get(plan.id).plan;
242
+ }
243
+ /** A mutated plan is a NEW plan: new id and hash; the old proposal is superseded (any challenge for it is void). */
244
+ amend(planId, patch, ctx = {}) {
245
+ const r = this.rec(planId);
246
+ if (r.status !== 'proposed')
247
+ throw new PolicyRefusal('plan-not-proposed', `plan is ${r.status}; cancel it and propose again`);
248
+ const next = amendPlan(r.plan, patch, { clock: this.clock, ...ctx });
249
+ this.emit('plan.status', { planId, data: { status: 'cancelled', reason: `superseded by ${next.id}` } });
250
+ this.emit('plan.proposed', { planId: next.id, data: { plan: next, by: next.source.by, ref: next.source.ref, supersedes: planId } });
251
+ return this.recs.get(next.id).plan;
252
+ }
253
+ rec(planId) { const r = this.recs.get(planId); if (!r)
254
+ throw new PolicyRefusal('unknown-plan', 'no such plan'); return r; }
255
+ plan(planId) { return this.recs.get(planId)?.plan ?? null; }
256
+ status(planId) { return this.recs.get(planId)?.status ?? null; }
257
+ challenge(planId, tier) { const r = this.rec(planId); return this.gate.issue(r.plan, tier, readBack(r.plan)); }
258
+ /** The gate sees the plan AS THE ENGINE HOLDS IT NOW (current hash + current status), never the proposer's copy. */
259
+ confirm(challengeId, evidence) {
260
+ const c = this.gate.challenges.get(challengeId);
261
+ const r = c ? this.recs.get(c.planId) : null;
262
+ return this.#remember(this.gate.confirm({ challengeId, evidence }, r ? { id: r.plan.id, hash: r.plan.hash, status: r.status } : null));
263
+ }
264
+ /** Trusted host only, at the physical copy opt-in. The returned capability stays inside CopyAuto.
265
+ * It creates NEW copy buys, never adopts an existing proposal or exposes an automation proof.
266
+ * A fresh mandate proof is consumed here; an already-armed mandate id grants no authority.
267
+ */
268
+ authorizeCopy(mandateId, proof, wallet) {
269
+ if (!this.gate.handsFreeAllowed())
270
+ throw new PolicyRefusal('gate-on', 'copy automation needs the less-safe opt-in');
271
+ this.armMandate(mandateId, proof);
272
+ return (input, supply, ref) => {
273
+ const check = () => {
274
+ if (!this.gate.handsFreeAllowed())
275
+ throw new PolicyRefusal('gate-on', 'copy automation opt-in is inactive');
276
+ if (this.panicked)
277
+ throw new PolicyRefusal('panicked', 'panic engaged');
278
+ const m = this.mandates.get(mandateId)?.m;
279
+ if (!m || !mandateAlive(m, this.clock.now()).ok)
280
+ throw new PolicyRefusal('dead-mandate', 'copy mandate is not alive');
281
+ };
282
+ check();
283
+ if (input.side !== 'buy')
284
+ throw new PolicyRefusal('action-not-allowed', 'copy automation is buy-only');
285
+ const plan = this.propose(input, { supply, by: 'copy', ref: `copy-auto:${wallet}:${ref}` });
286
+ const issued = this.#remember({ id: `copy_${plan.id}`, kind: 'gesture', signerIssued: true,
287
+ gesture: 'copy-auto', subject: `plan:${plan.id}:${plan.hash}`, at: this.clock.now(), consumed: false });
288
+ return { plan, arm: () => { check(); return this.armPlan(plan.id, issued, mandateId); } };
289
+ };
290
+ }
291
+ /** Arm a plan INTO a mandate with a proof. The signer-side admission check runs here; a refusal is journaled. */
292
+ armPlan(planId, proof, mandateId) {
293
+ const r = this.rec(planId);
294
+ const refuse = (reason, message) => { this.emit('policy.refused', { planId, mandateId, data: { stage: 'arm', reason, message } }); throw new PolicyRefusal(reason, message); };
295
+ if (this.panicked)
296
+ refuse('panicked', 'panic engaged');
297
+ if (r.status !== 'proposed')
298
+ refuse('plan-not-proposed', `plan is ${r.status}`);
299
+ if (!proof || proof.kind !== 'gesture' || !proof.signerIssued)
300
+ refuse('no-gesture', 'arming needs a signer-issued gesture proof');
301
+ if (proof.consumed)
302
+ refuse('proof-consumed', 'proof already used');
303
+ try {
304
+ this.#validateProof(proof);
305
+ }
306
+ catch (e) {
307
+ refuse(e.reason, e.message);
308
+ }
309
+ if (proof.subject !== `plan:${planId}:${r.plan.hash}`)
310
+ refuse('plan-hash-mismatch', 'the proof is bound to a different plan hash');
311
+ const tier = gestureTier(proof.gesture);
312
+ const x = this.mandates.get(mandateId);
313
+ if (!x)
314
+ refuse('unknown-mandate', 'no such mandate');
315
+ const alive = mandateAlive(x.m, this.clock.now());
316
+ if (!alive.ok)
317
+ refuse(alive.reason, 'mandate is not alive');
318
+ if (TIERS[tier].rank > TIERS[x.m.maxTier].rank)
319
+ refuse('tier-not-allowed', `this mandate accepts up to ${x.m.maxTier}; ${tier} is weaker`);
320
+ const v = this.admit(r, x, tier);
321
+ if (!v.ok)
322
+ refuse(v.reason, v.message);
323
+ this.#consume(proof);
324
+ this.emit('plan.armed', { planId, mandateId, data: { tier, reservedUsd: r.plan.sizeUsd, proofId: proof.id, planHash: r.plan.hash } });
325
+ return this.recs.get(planId);
326
+ }
327
+ /** Arming-time admission (budget is RESERVED here so the total cap is a guarantee, not a race). */
328
+ admit(r, x, tier) {
329
+ const p = r.plan, m = x.m, L = x.ledger, now = this.clock.now();
330
+ const no = (reason, message) => ({ ok: false, reason, message });
331
+ if (now > p.expiresAt)
332
+ return no('plan-expired', 'the plan expired before arming');
333
+ if (!m.allowed.actions.includes(p.side))
334
+ return no('action-not-allowed', `${p.side} is not in the mandate`);
335
+ const safe = this.safety(p.mint);
336
+ if (!safe.ok)
337
+ return no('mint-not-allowed', `mint fails the safety filter: ${safe.why}`);
338
+ if (m.allowed.mints && !m.allowed.mints.includes(p.mint))
339
+ return no('mint-not-allowed', 'mint not in the mandate allow-list');
340
+ if (p.slippageBps > m.limits.maxSlippageBps)
341
+ return no('slippage', `slippage ${p.slippageBps} > mandate max ${m.limits.maxSlippageBps}`);
342
+ const armed = [...this.recs.values()].filter((q) => q.mandateId === m.id && !TERMINAL.has(q.status) && q.status !== 'proposed');
343
+ if (armed.length >= m.limits.maxArmedPlans)
344
+ return no('concurrent-cap', `mandate already has ${armed.length} armed plans`);
345
+ if (p.side === 'buy') {
346
+ const perTrade = m.budget.perTradeUsd * TIERS[tier].perTradeMultiplier;
347
+ if (p.sizeUsd > perTrade + 1e-9)
348
+ return no('per-trade-cap', `$${p.sizeUsd} exceeds the ${tier} per-trade cap $${perTrade.toFixed(2)}`);
349
+ const coin = armed.filter((q) => q.plan.mint === p.mint).reduce((s, q) => s + q.reservedUsd + q.pos.costUsd, 0);
350
+ if (coin + p.sizeUsd > m.budget.perCoinUsd + 1e-9)
351
+ return no('per-coin-cap', `$${(coin + p.sizeUsd).toFixed(2)} on this coin exceeds $${m.budget.perCoinUsd}`);
352
+ if (L.reservedUsd + L.spentUsd + p.sizeUsd > m.budget.totalUsd + 1e-9)
353
+ return no('total-cap', `budget: reserved $${L.reservedUsd.toFixed(2)} + spent $${L.spentUsd.toFixed(2)} + $${p.sizeUsd} > $${m.budget.totalUsd}`);
354
+ }
355
+ return { ok: true };
356
+ }
357
+ /** A batch = many plans proposed together; ONE keypress arms them all (per-plan admission still runs). */
358
+ proposeBatch(inputs, ctx) {
359
+ const batchId = ctx.batchId ?? `batch_${this.batches.size + 1}`;
360
+ const plans = inputs.map((inp, i) => this.propose(inp, { ...ctx, batchId, id: ctx.idPrefix ? `${ctx.idPrefix}${i}` : undefined }));
361
+ const batchHash = createHash('sha256').update(plans.map((p) => p.hash).join('|')).digest('hex');
362
+ const b = { id: batchId, planIds: plans.map((p) => p.id), hash: batchHash, totalUsd: plans.reduce((s, p) => s + p.sizeUsd, 0) };
363
+ this.batches.set(batchId, b);
364
+ this.emit('batch.proposed', { data: { batchId, count: plans.length, totalUsd: b.totalUsd, batchHash } });
365
+ return b;
366
+ }
367
+ issueBatchProof(batchId, gesture) {
368
+ const b = this.batches.get(batchId);
369
+ if (!b)
370
+ throw new PolicyRefusal('unknown-batch', 'no such batch');
371
+ const p = { id: `bproof_${this.proofs.size + 1}`, kind: 'gesture', signerIssued: true, gesture, subject: `batch:${batchId}:${b.hash}`, at: this.clock.now(), consumed: false };
372
+ this.proofs.set(p.id, p);
373
+ return this.#remember(p);
374
+ }
375
+ armBatch(batchId, proof, mandateId) {
376
+ const b = this.batches.get(batchId);
377
+ if (!b)
378
+ throw new PolicyRefusal('unknown-batch', 'no such batch');
379
+ const tier = gestureTier(proof?.gesture ?? '');
380
+ if (!proof || proof.consumed || proof.subject !== `batch:${batchId}:${b.hash}`)
381
+ throw new PolicyRefusal('proof-mismatch', 'proof is not for this batch');
382
+ this.#validateProof(proof);
383
+ if (!TIERS[tier].canArmBatch)
384
+ throw new PolicyRefusal('tier-not-allowed', 'a batch can only be armed by keypress or Phantom approval');
385
+ this.#consume(proof);
386
+ const out = { armed: [], refused: [] };
387
+ for (const id of b.planIds) {
388
+ const r = this.recs.get(id);
389
+ const per = { id: `bproof_${batchId}_${id}`, kind: 'gesture', signerIssued: true, gesture: proof.gesture, subject: `plan:${id}:${r.plan.hash}`, consumed: false };
390
+ this.#remember(per);
391
+ try {
392
+ this.armPlan(id, per, mandateId);
393
+ out.armed.push(id);
394
+ }
395
+ catch (e) {
396
+ out.refused.push({ id, reason: e.reason });
397
+ }
398
+ }
399
+ this.emit('batch.armed', { data: { batchId, armed: out.armed.length, refused: out.refused } });
400
+ return out;
401
+ }
402
+ cancelPlan(planId, reason) {
403
+ const r = this.rec(planId);
404
+ if (TERMINAL.has(r.status))
405
+ return r.status;
406
+ const holding = r.pos.tokenRaw > 0n;
407
+ this.emit('plan.status', { planId, data: { status: 'cancelled', reason, release: true, positionHeld: holding ? str(r.pos.tokenRaw) : null } });
408
+ return 'cancelled';
409
+ }
410
+ /** PANIC: one-way. Revokes every mandate, cancels every non-terminal plan, refuses every send. Positions are NOT sold. */
411
+ panic(reason = 'panic key') {
412
+ this.emit('panic', { data: { reason } });
413
+ for (const x of this.mandates.values())
414
+ if (x.m.status === 'armed')
415
+ this.revokeMandate(x.m.id, reason, 'revoked');
416
+ for (const r of this.recs.values())
417
+ if (!TERMINAL.has(r.status))
418
+ this.cancelPlan(r.plan.id, reason);
419
+ }
420
+ resumePlan(planId, proof) {
421
+ const r = this.rec(planId);
422
+ if (!r.paused)
423
+ return;
424
+ if (proof?.gesture !== 'keypress' && proof?.gesture !== 'phantom')
425
+ throw new PolicyRefusal('no-gesture', 'resuming a paused plan needs a keypress');
426
+ // the keypress acknowledges the NEW supply (the mc trigger now means a different price; the card showed it)
427
+ this.emit('plan.resumed', { planId, data: { by: proof.gesture, supply: r.paused.now } });
428
+ }
429
+ // ───────────────────────── ticks & triggers ─────────────────────────
430
+ /** A price tick for one mint: {mint, priceUsd, supply, ts, source, crossPriceUsd?}. Drives everything. */
431
+ tick(t) {
432
+ const now = this.clock.now();
433
+ const prev = this.ticks.get(t.mint)?.[1] ?? null;
434
+ this.ticks.set(t.mint, [prev, t]);
435
+ this.sweep(now);
436
+ for (const r of this.recs.values()) {
437
+ if (r.plan.mint !== t.mint || TERMINAL.has(r.status) || r.status === 'proposed')
438
+ continue;
439
+ this.settleUnknown(r, now);
440
+ if (this.hasInFlight(r))
441
+ continue;
442
+ const fresh = this.freshness(t, prev, now);
443
+ if (!fresh.ok) {
444
+ this.emit('price.unusable', { planId: r.plan.id, data: { reason: fresh.reason, ageMs: now - t.ts, source: t.source } });
445
+ continue;
446
+ }
447
+ if (r.paused)
448
+ continue;
449
+ const knownSupply = r.ackSupply ?? r.plan.supplyAtProposal;
450
+ if (Math.abs(t.supply - knownSupply) / knownSupply > 0.01) {
451
+ this.emit('plan.paused', { planId: r.plan.id, data: { reason: 'supply-changed', was: knownSupply, now: t.supply, newTriggerPriceUsd: r.plan.entry.kind !== 'immediate' ? r.plan.entry.value / t.supply : null } });
452
+ continue;
453
+ }
454
+ const mc = (tk) => tk.priceUsd * tk.supply;
455
+ if (r.status === 'armed' || r.status === 'triggered') { // triggered = an entry chunk is waiting to retry
456
+ if (this.cond(r.plan.entry, t, prev, mc))
457
+ this.fire(r, 'entry', 'buy', t);
458
+ }
459
+ else if (r.status === 'filled') {
460
+ if (r.plan.stop && this.cond(r.plan.stop, t, prev, mc)) {
461
+ this.fire(r, 'stop', 'sell', t);
462
+ continue;
463
+ }
464
+ const leg = r.plan.exits[r.exitIdx];
465
+ if (leg && this.cond(leg.at, t, prev, mc))
466
+ this.fire(r, `exit#${r.exitIdx}`, 'sell', t);
467
+ }
468
+ }
469
+ }
470
+ freshness(t, prev, now) {
471
+ if (now - t.ts > this.cfg.priceMaxAgeMs)
472
+ return { ok: false, reason: 'stale' };
473
+ if (typeof t.crossPriceUsd === 'number' && Math.abs(t.crossPriceUsd - t.priceUsd) / t.priceUsd * 100 > this.cfg.sourceDisagreePct)
474
+ return { ok: false, reason: 'sources-disagree' };
475
+ if (this.cfg.debounceTicks > 1 && (!prev || now - prev.ts > this.cfg.priceMaxAgeMs * 2))
476
+ return { ok: false, reason: 'need-consecutive-tick' };
477
+ return { ok: true };
478
+ }
479
+ cond(trig, t, prev, mc) {
480
+ const test = (tk) => {
481
+ switch (trig.kind) {
482
+ case 'immediate': return true;
483
+ case 'mc-below': return mc(tk) <= trig.value;
484
+ case 'mc-above': return mc(tk) >= trig.value;
485
+ case 'price-below': return tk.priceUsd <= trig.value;
486
+ case 'price-above': return tk.priceUsd >= trig.value;
487
+ default: return false;
488
+ }
489
+ };
490
+ if (trig.kind === 'immediate')
491
+ return true;
492
+ return test(t) && (this.cfg.debounceTicks < 2 || (prev !== null && test(prev)));
493
+ }
494
+ hasInFlight(r) { return Object.values(r.legs).some((l) => l.state === 'in-flight' || l.state === 'unknown'); }
495
+ sweep(now) {
496
+ for (const x of this.mandates.values())
497
+ if (x.m.status === 'armed' && now > x.m.expiresAt)
498
+ this.revokeMandate(x.m.id, 'ttl', 'expired');
499
+ for (const r of this.recs.values()) {
500
+ if (TERMINAL.has(r.status) || now <= r.plan.expiresAt)
501
+ continue;
502
+ if (r.status === 'proposed' || r.status === 'armed')
503
+ this.emit('plan.status', { planId: r.plan.id, data: { status: 'cancelled', reason: 'expired', release: true } });
504
+ else if (r.status === 'filled')
505
+ this.emit('plan.status', { planId: r.plan.id, data: { status: 'done', reason: 'expired-holding', release: true } });
506
+ }
507
+ }
508
+ /** Fire one leg: quote -> re-confirm the trigger against the quote -> signer policy -> journal signature -> send -> settle. */
509
+ fire(r, legKey, side, t) {
510
+ const p = r.plan, now = this.clock.now();
511
+ // a chunked entry is N independent legs (entry#i), each with its own signature, attempts and outcome
512
+ const chunks = side === 'buy' && p.chunkUsd ? Math.ceil(p.sizeUsd / p.chunkUsd) : 1;
513
+ if (chunks > 1 && legKey === 'entry') {
514
+ for (let i = 0; i < chunks; i++)
515
+ r.legs[`entry#${i}`] ??= { attempts: 0, state: 'pending' }; // all chunks exist before any settles
516
+ for (let i = 0; i < chunks; i++)
517
+ this.fire(r, `entry#${i}`, 'buy', t);
518
+ return;
519
+ }
520
+ const leg = r.legs[legKey] ?? (r.legs[legKey] = { attempts: 0, state: 'pending' });
521
+ if (leg.state === 'filled' || leg.state === 'failed')
522
+ return;
523
+ if (leg.attempts >= LEG_MAX_ATTEMPTS) {
524
+ this.legFinalFail(r, legKey, side, 'max-attempts');
525
+ return;
526
+ }
527
+ // sizes
528
+ let usd = null, tokenRaw = null;
529
+ if (side === 'buy') {
530
+ const idx = /^entry#(\d+)$/.exec(legKey)?.[1];
531
+ usd = idx == null ? p.sizeUsd : Math.min(p.chunkUsd, p.sizeUsd - Number(idx) * p.chunkUsd);
532
+ }
533
+ else {
534
+ const remaining = r.pos.tokenRaw;
535
+ if (legKey === 'stop')
536
+ tokenRaw = remaining;
537
+ else {
538
+ const frac = p.exits[r.exitIdx].fraction;
539
+ let want = BigInt(Math.round(Number(r.pos.entryTokenRaw) * frac));
540
+ if (want > remaining)
541
+ want = remaining;
542
+ if (remaining - want < r.pos.entryTokenRaw / 100n)
543
+ want = remaining;
544
+ tokenRaw = want;
545
+ }
546
+ if (tokenRaw <= 0n) {
547
+ this.legFinalFail(r, legKey, side, 'nothing-to-sell');
548
+ return;
549
+ }
550
+ }
551
+ this.emit('trigger.fired', { planId: p.id, mandateId: r.mandateId, data: { legKey, side, priceUsd: t.priceUsd, mcUsd: t.priceUsd * t.supply, source: t.source, ageMs: now - t.ts } });
552
+ let q;
553
+ try {
554
+ q = this.ex.quote({ plan: p, legKey, side, usd, tokenRaw, priceUsd: t.priceUsd });
555
+ }
556
+ catch (e) {
557
+ if (e instanceof RpcOutage) {
558
+ this.emit('price.unusable', { planId: p.id, data: { reason: 'rpc-outage', stage: 'quote' } });
559
+ return;
560
+ }
561
+ throw e;
562
+ }
563
+ const v = this.policy(r, this.mandates.get(r.mandateId), legKey, side, q, usd, tokenRaw, t);
564
+ if (!v.ok) {
565
+ this.emit('policy.refused', { planId: p.id, mandateId: r.mandateId, data: { stage: 'fire', legKey, reason: v.reason, message: v.message } });
566
+ if (v.transient)
567
+ return; // queued: re-evaluated on the next tick, no attempt consumed
568
+ leg.attempts += 1;
569
+ if (leg.attempts >= LEG_MAX_ATTEMPTS)
570
+ this.legFinalFail(r, legKey, side, v.reason);
571
+ return;
572
+ }
573
+ if (legKey.startsWith('entry') && r.status === 'armed')
574
+ this.emit('plan.status', { planId: p.id, data: { status: 'triggered' } });
575
+ if (side === 'sell')
576
+ this.emit('plan.status', { planId: p.id, data: { status: 'exiting', legKey } });
577
+ const signature = this.ex.nextSignature();
578
+ // CL-03: the signature is journaled BEFORE the send so a crash here is recoverable by lookup, never by resend.
579
+ this.emit('leg.sent', { planId: p.id, mandateId: r.mandateId, data: { legKey, side, signature, usd, tokenRaw: str(tokenRaw), priceUsd: q.priceUsd, feeLamports: q.feeLamports + q.tipLamports, impactPct: q.impactPct } });
580
+ let out;
581
+ try {
582
+ out = this.ex.submit({ plan: p, legKey, quote: q, signature, usd, tokenRaw });
583
+ }
584
+ catch (e) {
585
+ if (e instanceof RpcOutage) {
586
+ this.emit('leg.unknown', { planId: p.id, data: { legKey, signature, why: 'rpc-outage after send' } });
587
+ return;
588
+ }
589
+ throw e;
590
+ }
591
+ this.settle(r, legKey, side, signature, out);
592
+ }
593
+ /** The signer-side policy at fire time. `transient` refusals are queue-and-retry (rate/in-flight caps), not attempts. */
594
+ policy(r, x, legKey, side, q, usd, _tokenRaw, t) {
595
+ const p = r.plan, m = x.m, L = x.ledger, now = this.clock.now();
596
+ const no = (reason, message, transient = false) => ({ ok: false, reason, message, transient });
597
+ if (this.panicked)
598
+ return no('panicked', 'panic engaged');
599
+ const alive = mandateAlive(m, now);
600
+ if (!alive.ok)
601
+ return no(alive.reason, 'mandate not alive');
602
+ if (now > p.expiresAt)
603
+ return no('plan-expired', 'plan expired');
604
+ if (!m.allowed.actions.includes(side))
605
+ return no('action-not-allowed', `${side} not allowed`);
606
+ // task-1363: a live executor carries the SIGNER's own refusal reason (caps, policy, no fast wallet…) so the
607
+ // journal says why; a plain failed simulation stays 'simulation-failed'.
608
+ if (!q.simOk)
609
+ return no(q.refusal?.reason ?? 'simulation-failed', q.refusal?.message ?? q.simError ?? 'simulation failed');
610
+ const impactCap = legKey === 'stop' ? (m.limits.stopMaxPriceImpactPct ?? 10) : Math.min(p.maxPriceImpactPct, m.limits.maxPriceImpactPct);
611
+ if (q.impactPct > impactCap)
612
+ return no('price-impact', `impact ${q.impactPct}% > ${impactCap}%`);
613
+ if (p.slippageBps > m.limits.maxSlippageBps)
614
+ return no('slippage', 'slippage above mandate max');
615
+ // the quote must still satisfy the trigger (the tick is a hint; the route's implied price is the truth)
616
+ const band = this.cfg.confirmBandPct / 100, impliedMc = q.priceUsd * t.supply;
617
+ const trig = legKey.startsWith('entry') ? p.entry : legKey === 'stop' ? p.stop : p.exits[r.exitIdx].at;
618
+ const val = trig.kind.startsWith('mc') ? impliedMc : q.priceUsd;
619
+ const trigValue = trig.kind === 'immediate' ? 0 : trig.value;
620
+ if ((trig.kind.endsWith('below') && val > trigValue * (1 + band)) || (trig.kind.endsWith('above') && val < trigValue * (1 - band)))
621
+ return no('trigger-not-confirmed', `quote-implied ${trig.kind} ${val.toFixed(0)} is outside the ${this.cfg.confirmBandPct}% band of ${trigValue}`, true);
622
+ if (side === 'buy') {
623
+ const perTrade = m.budget.perTradeUsd * TIERS[r.tier].perTradeMultiplier;
624
+ if (usd > perTrade + 1e-9)
625
+ return no('per-trade-cap', `$${usd} > $${perTrade}`);
626
+ if (usd > r.reservedUsd + 1e-9)
627
+ return no('total-cap', 'more than the reserved budget');
628
+ const open = [...this.recs.values()].filter((z) => z.mandateId === m.id && (z.status === 'filled' || z.status === 'exiting')).length;
629
+ if (open >= m.limits.maxOpenPositions && r.status === 'armed')
630
+ return no('open-positions-cap', `${open} open positions`, true);
631
+ }
632
+ const fee = q.feeLamports + q.tipLamports;
633
+ if (q.tipLamports > m.fees.maxTipLamportsPerTx)
634
+ return no('tip-cap', 'tip above per-tx ceiling');
635
+ if (q.feeLamports > m.fees.maxPriorityFeeLamportsPerTx + 5_000)
636
+ return no('priority-fee-cap', 'priority fee above per-tx ceiling');
637
+ if (L.feeLamports + fee > m.fees.tipBudgetLamports)
638
+ return no('fee-budget', `fee budget exhausted (${L.feeLamports} + ${fee} > ${m.fees.tipBudgetLamports} lamports)`);
639
+ const inflight = [...this.recs.values()].filter((z) => z.mandateId === m.id).reduce((s, z) => s + Object.values(z.legs).filter((l) => l.state === 'in-flight' || l.state === 'unknown').length, 0);
640
+ if (inflight >= m.limits.maxInFlight)
641
+ return no('in-flight-cap', `${inflight} sends in flight`, true);
642
+ const recent = L.sendTs.filter((ts) => now - ts < 1000).length;
643
+ if (recent >= m.limits.maxSendsPerSec)
644
+ return no('rate-limit', `${recent} sends in the last second`, true);
645
+ return { ok: true };
646
+ }
647
+ settle(r, legKey, side, signature, out) {
648
+ const p = r.plan;
649
+ switch (out.kind) {
650
+ case 'filled':
651
+ this.emit('leg.filled', { planId: p.id, mandateId: r.mandateId, data: { legKey, side, signature, fill: { ...out.fill, tokenDelta: str(out.fill.tokenDelta), solDelta: str(out.fill.solDelta) }, feeLamports: out.feeLamports, roundTripMs: out.roundTripMs } });
652
+ this.advance(r);
653
+ this.lossCheck(r);
654
+ return;
655
+ case 'send-failed': {
656
+ const leg = r.legs[legKey];
657
+ const final = leg.attempts >= LEG_MAX_ATTEMPTS;
658
+ this.emit('leg.failed', { planId: p.id, data: { legKey, side, signature, reason: 'send-failed', error: out.error, refundFeeLamports: leg.feeLamports, final } });
659
+ break;
660
+ }
661
+ case 'send-unknown':
662
+ this.emit('leg.unknown', { planId: p.id, data: { legKey, signature, why: out.error } });
663
+ return;
664
+ default: throw new Error(`bad outcome ${out.kind}`);
665
+ }
666
+ this.advance(r);
667
+ }
668
+ /** Daily loss limit: realized losses inside the mandate reaching the limit auto-revoke it (positions held). */
669
+ lossCheck(r) {
670
+ const x = this.mandates.get(r.mandateId);
671
+ if (x.m.status === 'armed' && x.ledger.lossUsd >= x.m.budget.dailyLossUsd)
672
+ this.revokeMandate(x.m.id, `daily-loss-limit: $${x.ledger.lossUsd.toFixed(2)} >= $${x.m.budget.dailyLossUsd}`);
673
+ }
674
+ legFinalFail(r, legKey, side, reason) {
675
+ const leg = r.legs[legKey] ?? (r.legs[legKey] = { attempts: 0, state: 'pending' });
676
+ if (leg.state !== 'failed')
677
+ this.emit('leg.failed', { planId: r.plan.id, data: { legKey, side, signature: null, reason, final: true } });
678
+ this.advance(r);
679
+ }
680
+ /** Derive the plan status from its legs after a leg settles. */
681
+ advance(r) {
682
+ const p = r.plan, legs = r.legs;
683
+ const entryLegs = Object.entries(legs).filter(([k]) => k.startsWith('entry'));
684
+ const allDone = (ls) => ls.every(([, l]) => l.state === 'filled' || l.state === 'failed');
685
+ if (r.status === 'triggered' || (r.status === 'armed' && entryLegs.length)) { // armed + legs = refused before anything was sent
686
+ if (!allDone(entryLegs))
687
+ return;
688
+ const filled = entryLegs.filter(([, l]) => l.state === 'filled').length;
689
+ if (filled === 0) {
690
+ const why = [...new Set(entryLegs.map(([, l]) => l.reason).filter(Boolean))].join(',');
691
+ this.emit('plan.status', { planId: p.id, data: { status: 'failed', reason: `entry-failed: ${why}`, release: true } });
692
+ return;
693
+ }
694
+ const fraction = Math.round(r.pos.costUsd / p.sizeUsd * 1000) / 1000;
695
+ this.emit('plan.status', { planId: p.id, data: { status: 'filled', reason: fraction < 1 ? `partial ${fraction}` : null, release: true, filledFraction: fraction, entryTokenRaw: str(r.pos.entryTokenRaw) } });
696
+ return;
697
+ }
698
+ if (r.status === 'exiting' || r.status === 'filled') {
699
+ const stop = legs.stop;
700
+ if (stop?.state === 'filled') {
701
+ this.emit('plan.status', { planId: p.id, data: { status: 'done', reason: 'stopped', realizedUsd: r.pos.realizedUsd } });
702
+ return;
703
+ }
704
+ // a stop the signer will not let through (rug: impact over every ceiling) = the position is unrealizable within policy
705
+ if (stop?.state === 'failed') {
706
+ this.emit('plan.status', { planId: p.id, data: { status: 'failed', reason: `unrealizable: stop ${stop.reason}; position held`, positionHeld: str(r.pos.tokenRaw) } });
707
+ return;
708
+ }
709
+ const key = `exit#${r.exitIdx}`, leg = legs[key];
710
+ if (leg?.state === 'filled') {
711
+ r.exitIdx += 1;
712
+ if (r.exitIdx >= p.exits.length || r.pos.tokenRaw <= 0n)
713
+ this.emit('plan.status', { planId: p.id, data: { status: 'done', reason: 'ladder-complete', moonBagTokenRaw: str(r.pos.tokenRaw), moonBagFraction: p.moonBagFraction, realizedUsd: r.pos.realizedUsd } });
714
+ else
715
+ this.emit('plan.status', { planId: p.id, data: { status: 'filled', reason: `leg ${r.exitIdx} of ${p.exits.length} done` } });
716
+ return;
717
+ }
718
+ if (r.status !== 'exiting')
719
+ return; // nothing in flight changed
720
+ if (leg?.state === 'failed') {
721
+ this.emit('plan.status', { planId: p.id, data: { status: 'filled', reason: `exit ${key} failed (${leg.reason}); holding, alert` } });
722
+ return;
723
+ }
724
+ if (leg?.state === 'retry') {
725
+ this.emit('plan.status', { planId: p.id, data: { status: 'filled', reason: `exit ${key} will retry` } });
726
+ return;
727
+ }
728
+ }
729
+ }
730
+ /** A leg in 'unknown' (sent, outcome unknown): look the signature up; never resend. */
731
+ settleUnknown(r, now) {
732
+ for (const [legKey, leg] of Object.entries(r.legs)) {
733
+ if (leg.state !== 'unknown')
734
+ continue;
735
+ let res;
736
+ try {
737
+ res = this.ex.lookup(leg.signature);
738
+ }
739
+ catch (e) {
740
+ if (e instanceof RpcOutage)
741
+ return;
742
+ throw e;
743
+ }
744
+ const confirmed = res === 'confirmed' || (typeof res === 'object' && res.status === 'confirmed');
745
+ const failed = res === 'failed' || (typeof res === 'object' && res.status === 'failed');
746
+ if (confirmed) {
747
+ const fill = (typeof res === 'object' && res.status === 'confirmed' ? res.fill : null) ?? this.ex.chainFill(leg.signature);
748
+ this.emit('leg.filled', { planId: r.plan.id, mandateId: r.mandateId, data: { legKey, side: leg.side, signature: leg.signature, fill: { ...fill, tokenDelta: str(fill.tokenDelta), solDelta: str(fill.solDelta) }, feeLamports: leg.feeLamports, recovered: true } });
749
+ this.advance(r);
750
+ this.lossCheck(r);
751
+ }
752
+ else if (failed || now - leg.unknownSince > SEND_UNKNOWN_SETTLE_MS) {
753
+ const final = leg.attempts >= LEG_MAX_ATTEMPTS;
754
+ this.emit('leg.failed', { planId: r.plan.id, data: { legKey, side: leg.side, signature: leg.signature, reason: failed ? 'failed-on-chain' : 'expired-unseen', final, refundFeeLamports: 0 } });
755
+ this.advance(r);
756
+ }
757
+ }
758
+ }
759
+ // ───────────────────────── recovery ─────────────────────────
760
+ /** Rebuild from a journal, then settle anything in flight by signature lookup. */
761
+ static recover(deps) {
762
+ const eng = new PlanEngine(deps);
763
+ const v = deps.journal.verify();
764
+ if (!v.ok)
765
+ throw new Error(`journal chain broken at seq ${v.atSeq}: ${v.why}`);
766
+ for (const e of deps.journal.events)
767
+ eng.reduce(e);
768
+ eng.emit('recovered', { data: { events: deps.journal.events.length - 1, plans: eng.recs.size, mandates: eng.mandates.size } });
769
+ const now = eng.clock.now();
770
+ for (const r of eng.recs.values()) {
771
+ for (const [legKey, leg] of Object.entries(r.legs))
772
+ if (leg.state === 'in-flight') {
773
+ eng.emit('leg.unknown', { planId: r.plan.id, data: { legKey, signature: leg.signature, why: 'in flight at crash' } });
774
+ }
775
+ eng.settleUnknown(r, now);
776
+ }
777
+ return eng;
778
+ }
779
+ // ───────────────────────── views ─────────────────────────
780
+ ledger(mandateId) { const x = this.mandates.get(mandateId); return { reservedUsd: x.ledger.reservedUsd, spentUsd: x.ledger.spentUsd, realizedUsd: x.ledger.realizedUsd, lossUsd: x.ledger.lossUsd, feeLamports: x.ledger.feeLamports, sendTs: undefined, mandateStatus: x.m.status }; }
781
+ position(planId) { const r = this.rec(planId); return { tokenRaw: r.pos.tokenRaw, entryTokenRaw: r.pos.entryTokenRaw, costUsd: r.pos.costUsd, soldUsd: r.pos.soldUsd, realizedUsd: r.pos.realizedUsd }; }
782
+ summary(planId) { const r = this.rec(planId); return { id: planId, status: r.status, reason: r.reason, tier: r.tier, legs: Object.fromEntries(Object.entries(r.legs).map(([k, l]) => [k, l.state])), position: this.position(planId) }; }
783
+ }
784
+ export { PlanError, HARD };