opencandle 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/assets/logo.svg +187 -0
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +40 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +25 -0
  8. package/dist/config.js +72 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/infra/browser.d.ts +11 -3
  11. package/dist/infra/browser.js +2 -1
  12. package/dist/infra/browser.js.map +1 -1
  13. package/dist/infra/native-dependencies.d.ts +1 -0
  14. package/dist/infra/native-dependencies.js +10 -0
  15. package/dist/infra/native-dependencies.js.map +1 -0
  16. package/dist/infra/node-version.d.ts +2 -0
  17. package/dist/infra/node-version.js +23 -0
  18. package/dist/infra/node-version.js.map +1 -0
  19. package/dist/infra/rate-limiter.d.ts +4 -0
  20. package/dist/infra/rate-limiter.js +5 -1
  21. package/dist/infra/rate-limiter.js.map +1 -1
  22. package/dist/memory/index.d.ts +2 -0
  23. package/dist/memory/index.js +1 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/manager.d.ts +9 -0
  26. package/dist/memory/manager.js +28 -11
  27. package/dist/memory/manager.js.map +1 -1
  28. package/dist/memory/sqlite.js +42 -4
  29. package/dist/memory/sqlite.js.map +1 -1
  30. package/dist/memory/storage.d.ts +7 -0
  31. package/dist/memory/storage.js +3 -3
  32. package/dist/memory/storage.js.map +1 -1
  33. package/dist/memory/tool-defaults.d.ts +8 -0
  34. package/dist/memory/tool-defaults.js +59 -0
  35. package/dist/memory/tool-defaults.js.map +1 -0
  36. package/dist/memory/types.js +4 -0
  37. package/dist/memory/types.js.map +1 -1
  38. package/dist/onboarding/connect.d.ts +13 -1
  39. package/dist/onboarding/connect.js +21 -10
  40. package/dist/onboarding/connect.js.map +1 -1
  41. package/dist/onboarding/prompt-user.d.ts +1 -1
  42. package/dist/onboarding/providers.d.ts +7 -0
  43. package/dist/onboarding/providers.js +6 -3
  44. package/dist/onboarding/providers.js.map +1 -1
  45. package/dist/onboarding/tool-helpers.d.ts +1 -1
  46. package/dist/pi/opencandle-extension.d.ts +7 -1
  47. package/dist/pi/opencandle-extension.js +391 -21
  48. package/dist/pi/opencandle-extension.js.map +1 -1
  49. package/dist/pi/session-storage.d.ts +2 -0
  50. package/dist/pi/session-storage.js +5 -0
  51. package/dist/pi/session-storage.js.map +1 -0
  52. package/dist/pi/session.d.ts +4 -1
  53. package/dist/pi/session.js +25 -3
  54. package/dist/pi/session.js.map +1 -1
  55. package/dist/pi/setup.d.ts +1 -1
  56. package/dist/pi/setup.js +11 -1
  57. package/dist/pi/setup.js.map +1 -1
  58. package/dist/pi/tool-adapter.d.ts +2 -2
  59. package/dist/pi/tool-adapter.js +14 -1
  60. package/dist/pi/tool-adapter.js.map +1 -1
  61. package/dist/prompts/context-builder.d.ts +40 -3
  62. package/dist/prompts/context-builder.js +140 -19
  63. package/dist/prompts/context-builder.js.map +1 -1
  64. package/dist/prompts/disclaimer.d.ts +6 -0
  65. package/dist/prompts/disclaimer.js +9 -0
  66. package/dist/prompts/disclaimer.js.map +1 -0
  67. package/dist/prompts/policy-cards.d.ts +13 -0
  68. package/dist/prompts/policy-cards.js +197 -0
  69. package/dist/prompts/policy-cards.js.map +1 -0
  70. package/dist/prompts/sections.js +3 -3
  71. package/dist/prompts/sections.js.map +1 -1
  72. package/dist/prompts/workflow-prompts.d.ts +8 -0
  73. package/dist/prompts/workflow-prompts.js +208 -22
  74. package/dist/prompts/workflow-prompts.js.map +1 -1
  75. package/dist/providers/alpha-vantage.js +23 -1
  76. package/dist/providers/alpha-vantage.js.map +1 -1
  77. package/dist/providers/sec-edgar.d.ts +8 -1
  78. package/dist/providers/sec-edgar.js +172 -5
  79. package/dist/providers/sec-edgar.js.map +1 -1
  80. package/dist/providers/yahoo-finance.d.ts +2 -0
  81. package/dist/providers/yahoo-finance.js +203 -35
  82. package/dist/providers/yahoo-finance.js.map +1 -1
  83. package/dist/routing/classify-intent.d.ts +3 -0
  84. package/dist/routing/classify-intent.js +82 -3
  85. package/dist/routing/classify-intent.js.map +1 -1
  86. package/dist/routing/defaults.js +4 -4
  87. package/dist/routing/defaults.js.map +1 -1
  88. package/dist/routing/entity-extractor.d.ts +1 -0
  89. package/dist/routing/entity-extractor.js +158 -12
  90. package/dist/routing/entity-extractor.js.map +1 -1
  91. package/dist/routing/index.d.ts +10 -0
  92. package/dist/routing/index.js +7 -0
  93. package/dist/routing/index.js.map +1 -1
  94. package/dist/routing/legacy-rule-router.d.ts +9 -0
  95. package/dist/routing/legacy-rule-router.js +12 -0
  96. package/dist/routing/legacy-rule-router.js.map +1 -0
  97. package/dist/routing/planning.d.ts +54 -0
  98. package/dist/routing/planning.js +531 -0
  99. package/dist/routing/planning.js.map +1 -0
  100. package/dist/routing/route-manifest.d.ts +35 -0
  101. package/dist/routing/route-manifest.js +221 -0
  102. package/dist/routing/route-manifest.js.map +1 -0
  103. package/dist/routing/router-llm-client.d.ts +11 -0
  104. package/dist/routing/router-llm-client.js +42 -0
  105. package/dist/routing/router-llm-client.js.map +1 -0
  106. package/dist/routing/router-prompt.d.ts +2 -0
  107. package/dist/routing/router-prompt.js +141 -0
  108. package/dist/routing/router-prompt.js.map +1 -0
  109. package/dist/routing/router-types.d.ts +71 -0
  110. package/dist/routing/router-types.js +2 -0
  111. package/dist/routing/router-types.js.map +1 -0
  112. package/dist/routing/router.d.ts +11 -0
  113. package/dist/routing/router.js +638 -0
  114. package/dist/routing/router.js.map +1 -0
  115. package/dist/routing/slot-resolver.js +46 -6
  116. package/dist/routing/slot-resolver.js.map +1 -1
  117. package/dist/routing/turn-context.d.ts +44 -0
  118. package/dist/routing/turn-context.js +45 -0
  119. package/dist/routing/turn-context.js.map +1 -0
  120. package/dist/routing/types.d.ts +13 -1
  121. package/dist/runtime/answer-contracts.d.ts +82 -0
  122. package/dist/runtime/answer-contracts.js +414 -0
  123. package/dist/runtime/answer-contracts.js.map +1 -0
  124. package/dist/runtime/artifact-contracts.d.ts +14 -0
  125. package/dist/runtime/artifact-contracts.js +57 -0
  126. package/dist/runtime/artifact-contracts.js.map +1 -0
  127. package/dist/runtime/planning-evidence.d.ts +99 -0
  128. package/dist/runtime/planning-evidence.js +445 -0
  129. package/dist/runtime/planning-evidence.js.map +1 -0
  130. package/dist/runtime/session-coordinator.d.ts +81 -3
  131. package/dist/runtime/session-coordinator.js +201 -17
  132. package/dist/runtime/session-coordinator.js.map +1 -1
  133. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  134. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  135. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  136. package/dist/sentiment/store.js +5 -0
  137. package/dist/sentiment/store.js.map +1 -1
  138. package/dist/system-prompt.js +23 -12
  139. package/dist/system-prompt.js.map +1 -1
  140. package/dist/tool-kit.d.ts +4 -4
  141. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  142. package/dist/tools/fundamentals/company-overview.js +1 -1
  143. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  144. package/dist/tools/fundamentals/comps.d.ts +1 -1
  145. package/dist/tools/fundamentals/comps.js +1 -1
  146. package/dist/tools/fundamentals/comps.js.map +1 -1
  147. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  148. package/dist/tools/fundamentals/dcf.js +1 -1
  149. package/dist/tools/fundamentals/dcf.js.map +1 -1
  150. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  151. package/dist/tools/fundamentals/earnings.js +1 -1
  152. package/dist/tools/fundamentals/earnings.js.map +1 -1
  153. package/dist/tools/fundamentals/financials.d.ts +1 -1
  154. package/dist/tools/fundamentals/financials.js +1 -1
  155. package/dist/tools/fundamentals/financials.js.map +1 -1
  156. package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
  157. package/dist/tools/fundamentals/sec-filings.js +19 -2
  158. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  159. package/dist/tools/index.d.ts +29 -1
  160. package/dist/tools/index.js +30 -0
  161. package/dist/tools/index.js.map +1 -1
  162. package/dist/tools/interaction/ask-user.d.ts +1 -1
  163. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  164. package/dist/tools/macro/fear-greed.d.ts +1 -1
  165. package/dist/tools/macro/fear-greed.js +1 -1
  166. package/dist/tools/macro/fear-greed.js.map +1 -1
  167. package/dist/tools/macro/fred-data.d.ts +1 -1
  168. package/dist/tools/macro/fred-data.js +29 -5
  169. package/dist/tools/macro/fred-data.js.map +1 -1
  170. package/dist/tools/market/crypto-history.d.ts +1 -1
  171. package/dist/tools/market/crypto-history.js +18 -2
  172. package/dist/tools/market/crypto-history.js.map +1 -1
  173. package/dist/tools/market/crypto-price.d.ts +1 -1
  174. package/dist/tools/market/crypto-price.js +1 -1
  175. package/dist/tools/market/crypto-price.js.map +1 -1
  176. package/dist/tools/market/search-ticker.d.ts +1 -1
  177. package/dist/tools/market/search-ticker.js +1 -1
  178. package/dist/tools/market/search-ticker.js.map +1 -1
  179. package/dist/tools/market/stock-history.d.ts +1 -1
  180. package/dist/tools/market/stock-history.js +1 -1
  181. package/dist/tools/market/stock-history.js.map +1 -1
  182. package/dist/tools/market/stock-quote.d.ts +1 -1
  183. package/dist/tools/market/stock-quote.js +1 -1
  184. package/dist/tools/market/stock-quote.js.map +1 -1
  185. package/dist/tools/options/greeks.js +0 -1
  186. package/dist/tools/options/greeks.js.map +1 -1
  187. package/dist/tools/options/option-chain.d.ts +1 -1
  188. package/dist/tools/options/option-chain.js +13 -5
  189. package/dist/tools/options/option-chain.js.map +1 -1
  190. package/dist/tools/portfolio/correlation.d.ts +1 -1
  191. package/dist/tools/portfolio/correlation.js +1 -1
  192. package/dist/tools/portfolio/correlation.js.map +1 -1
  193. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  194. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  195. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  196. package/dist/tools/portfolio/predictions.d.ts +1 -1
  197. package/dist/tools/portfolio/predictions.js +1 -1
  198. package/dist/tools/portfolio/predictions.js.map +1 -1
  199. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  200. package/dist/tools/portfolio/risk-analysis.js +1 -1
  201. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  202. package/dist/tools/portfolio/tracker.d.ts +1 -1
  203. package/dist/tools/portfolio/tracker.js +1 -1
  204. package/dist/tools/portfolio/tracker.js.map +1 -1
  205. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  206. package/dist/tools/portfolio/watchlist.js +12 -4
  207. package/dist/tools/portfolio/watchlist.js.map +1 -1
  208. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  209. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  210. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  211. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  212. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  213. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  214. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  215. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  216. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  217. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  218. package/dist/tools/sentiment/web-search.d.ts +1 -1
  219. package/dist/tools/sentiment/web-search.js +32 -3
  220. package/dist/tools/sentiment/web-search.js.map +1 -1
  221. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  222. package/dist/tools/sentiment/web-sentiment.js +1 -1
  223. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  224. package/dist/tools/technical/backtest.d.ts +3 -3
  225. package/dist/tools/technical/backtest.js +41 -27
  226. package/dist/tools/technical/backtest.js.map +1 -1
  227. package/dist/tools/technical/indicators.d.ts +1 -1
  228. package/dist/tools/technical/indicators.js +8 -4
  229. package/dist/tools/technical/indicators.js.map +1 -1
  230. package/dist/types/options.d.ts +10 -0
  231. package/dist/types/portfolio.d.ts +27 -0
  232. package/dist/workflows/compare-assets.js +38 -2
  233. package/dist/workflows/compare-assets.js.map +1 -1
  234. package/dist/workflows/options-screener.js +94 -8
  235. package/dist/workflows/options-screener.js.map +1 -1
  236. package/dist/workflows/portfolio-builder.js +9 -5
  237. package/dist/workflows/portfolio-builder.js.map +1 -1
  238. package/gui/server/ask-user-bridge.ts +82 -0
  239. package/gui/server/background-quotes.ts +31 -0
  240. package/gui/server/chat-event-adapter.ts +142 -0
  241. package/gui/server/gui-session-manager.ts +5 -0
  242. package/gui/server/invoke-tool.ts +89 -0
  243. package/gui/server/live-chat-event-adapter.ts +181 -0
  244. package/gui/server/model-setup.ts +100 -0
  245. package/gui/server/package.json +5 -0
  246. package/gui/server/projector.ts +254 -0
  247. package/gui/server/prompt-observation.ts +61 -0
  248. package/gui/server/server.ts +703 -0
  249. package/gui/server/session-actions.ts +31 -0
  250. package/gui/server/session-entry-wait.ts +81 -0
  251. package/gui/server/tool-metadata.ts +88 -0
  252. package/gui/server/websocket.ts +128 -0
  253. package/gui/server/writer-lock.ts +118 -0
  254. package/gui/shared/chat-events.ts +118 -0
  255. package/gui/shared/event-reducer.ts +186 -0
  256. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +1 -0
  257. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  258. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  259. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  260. package/gui/web/dist/index.html +17 -0
  261. package/package.json +50 -18
  262. package/src/analysts/contracts.ts +189 -0
  263. package/src/analysts/orchestrator.ts +300 -0
  264. package/src/cli.ts +206 -0
  265. package/src/config.ts +245 -0
  266. package/src/index.ts +5 -0
  267. package/src/infra/browser.ts +111 -0
  268. package/src/infra/cache.ts +103 -0
  269. package/src/infra/http-client.ts +68 -0
  270. package/src/infra/index.ts +18 -0
  271. package/src/infra/native-dependencies.ts +12 -0
  272. package/src/infra/node-version.ts +24 -0
  273. package/src/infra/open-url.ts +28 -0
  274. package/src/infra/opencandle-paths.ts +64 -0
  275. package/src/infra/rate-limiter.ts +73 -0
  276. package/src/memory/index.ts +10 -0
  277. package/src/memory/manager.ts +192 -0
  278. package/src/memory/preference-extractor.ts +106 -0
  279. package/src/memory/retrieval.ts +70 -0
  280. package/src/memory/sqlite.ts +172 -0
  281. package/src/memory/storage.ts +205 -0
  282. package/src/memory/tool-defaults.ts +87 -0
  283. package/src/memory/types.ts +71 -0
  284. package/src/onboarding/connect.ts +184 -0
  285. package/src/onboarding/credential-interceptor.ts +134 -0
  286. package/src/onboarding/degradation-accumulator.ts +79 -0
  287. package/src/onboarding/prompt-user.ts +85 -0
  288. package/src/onboarding/providers.ts +315 -0
  289. package/src/onboarding/state.ts +218 -0
  290. package/src/onboarding/tool-helpers.ts +111 -0
  291. package/src/onboarding/tool-tags.ts +201 -0
  292. package/src/onboarding/validation.ts +158 -0
  293. package/src/pi/opencandle-extension.ts +955 -0
  294. package/src/pi/session-storage.ts +5 -0
  295. package/src/pi/session.ts +81 -0
  296. package/src/pi/setup.ts +381 -0
  297. package/src/pi/tool-adapter.ts +36 -0
  298. package/src/prompts/context-builder.ts +315 -0
  299. package/src/prompts/disclaimer.ts +9 -0
  300. package/src/prompts/policy-cards.ts +220 -0
  301. package/src/prompts/sections.ts +46 -0
  302. package/src/prompts/workflow-prompts.ts +433 -0
  303. package/src/providers/alpha-vantage.ts +315 -0
  304. package/src/providers/coingecko.ts +96 -0
  305. package/src/providers/exa-search.ts +373 -0
  306. package/src/providers/fear-greed.ts +45 -0
  307. package/src/providers/finnhub.ts +124 -0
  308. package/src/providers/fred.ts +83 -0
  309. package/src/providers/index.ts +9 -0
  310. package/src/providers/provider-credential-error.ts +23 -0
  311. package/src/providers/reddit.ts +151 -0
  312. package/src/providers/sec-edgar.ts +312 -0
  313. package/src/providers/twitter.ts +173 -0
  314. package/src/providers/web-search.ts +293 -0
  315. package/src/providers/with-fallback.ts +41 -0
  316. package/src/providers/wrap-provider.ts +64 -0
  317. package/src/providers/yahoo-finance.ts +534 -0
  318. package/src/routing/classify-intent.ts +285 -0
  319. package/src/routing/defaults.ts +29 -0
  320. package/src/routing/entity-extractor.ts +291 -0
  321. package/src/routing/index.ts +70 -0
  322. package/src/routing/legacy-rule-router.ts +13 -0
  323. package/src/routing/planning.ts +732 -0
  324. package/src/routing/route-manifest.ts +287 -0
  325. package/src/routing/router-llm-client.ts +51 -0
  326. package/src/routing/router-prompt.ts +163 -0
  327. package/src/routing/router-types.ts +87 -0
  328. package/src/routing/router.ts +712 -0
  329. package/src/routing/slot-resolver.ts +190 -0
  330. package/src/routing/turn-context.ts +111 -0
  331. package/src/routing/types.ts +75 -0
  332. package/src/runtime/answer-contracts.ts +633 -0
  333. package/src/runtime/artifact-contracts.ts +76 -0
  334. package/src/runtime/evidence.ts +77 -0
  335. package/src/runtime/planning-evidence.ts +591 -0
  336. package/src/runtime/prompt-step.ts +75 -0
  337. package/src/runtime/provider-tracker.ts +40 -0
  338. package/src/runtime/run-context.ts +22 -0
  339. package/src/runtime/session-coordinator.ts +472 -0
  340. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  341. package/src/runtime/validation.ts +214 -0
  342. package/src/runtime/workflow-events.ts +75 -0
  343. package/src/runtime/workflow-runner.ts +188 -0
  344. package/src/runtime/workflow-types.ts +102 -0
  345. package/src/sentiment/adapters/finnhub.ts +44 -0
  346. package/src/sentiment/adapters/reddit.ts +65 -0
  347. package/src/sentiment/adapters/twitter.ts +36 -0
  348. package/src/sentiment/adapters/web.ts +44 -0
  349. package/src/sentiment/index.ts +58 -0
  350. package/src/sentiment/keywords.ts +9 -0
  351. package/src/sentiment/pipeline.ts +68 -0
  352. package/src/sentiment/scorer.ts +78 -0
  353. package/src/sentiment/store.ts +260 -0
  354. package/src/sentiment/trends.ts +90 -0
  355. package/src/sentiment/types.ts +108 -0
  356. package/src/system-prompt.ts +118 -0
  357. package/src/tool-kit.ts +68 -0
  358. package/src/tools/AGENTS.md +36 -0
  359. package/src/tools/fundamentals/company-overview.ts +54 -0
  360. package/src/tools/fundamentals/comps.ts +156 -0
  361. package/src/tools/fundamentals/dcf.ts +267 -0
  362. package/src/tools/fundamentals/earnings.ts +47 -0
  363. package/src/tools/fundamentals/financials.ts +54 -0
  364. package/src/tools/fundamentals/sec-filings.ts +84 -0
  365. package/src/tools/index.ts +91 -0
  366. package/src/tools/interaction/ask-user.ts +81 -0
  367. package/src/tools/interaction/twitter-login.ts +93 -0
  368. package/src/tools/macro/fear-greed.ts +41 -0
  369. package/src/tools/macro/fred-data.ts +80 -0
  370. package/src/tools/market/crypto-history.ts +67 -0
  371. package/src/tools/market/crypto-price.ts +53 -0
  372. package/src/tools/market/search-ticker.ts +53 -0
  373. package/src/tools/market/stock-history.ts +79 -0
  374. package/src/tools/market/stock-quote.ts +64 -0
  375. package/src/tools/options/greeks.ts +81 -0
  376. package/src/tools/options/option-chain.ts +96 -0
  377. package/src/tools/portfolio/correlation.ts +162 -0
  378. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  379. package/src/tools/portfolio/predictions.ts +253 -0
  380. package/src/tools/portfolio/risk-analysis.ts +134 -0
  381. package/src/tools/portfolio/tracker.ts +147 -0
  382. package/src/tools/portfolio/watchlist.ts +159 -0
  383. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  384. package/src/tools/sentiment/sentiment-summary.ts +316 -0
  385. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  386. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  387. package/src/tools/sentiment/web-search.ts +183 -0
  388. package/src/tools/sentiment/web-sentiment.ts +76 -0
  389. package/src/tools/technical/backtest.ts +267 -0
  390. package/src/tools/technical/indicators.ts +256 -0
  391. package/src/types/fundamentals.ts +46 -0
  392. package/src/types/index.ts +20 -0
  393. package/src/types/macro.ts +27 -0
  394. package/src/types/market.ts +43 -0
  395. package/src/types/options.ts +52 -0
  396. package/src/types/portfolio.ts +73 -0
  397. package/src/types/sentiment.ts +70 -0
  398. package/src/workflows/compare-assets.ts +75 -0
  399. package/src/workflows/index.ts +4 -0
  400. package/src/workflows/options-screener.ts +127 -0
  401. package/src/workflows/portfolio-builder.ts +56 -0
  402. package/src/workflows/types.ts +4 -0
  403. package/dist/runtime/index.d.ts +0 -16
  404. package/dist/runtime/index.js +0 -10
  405. package/dist/runtime/index.js.map +0 -1
  406. package/dist/runtime/provider-ids.d.ts +0 -14
  407. package/dist/runtime/provider-ids.js +0 -14
  408. package/dist/runtime/provider-ids.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ /** Source of a value flowing through the runtime. */
2
+ export type ProvenanceSource =
3
+ | "user"
4
+ | "preference"
5
+ | "default"
6
+ | "fetched"
7
+ | "computed"
8
+ | "unavailable"
9
+ | "stale_cache";
10
+
11
+ /** Tracks where a value came from, when, and with what confidence. */
12
+ export interface Provenance {
13
+ source: ProvenanceSource;
14
+ timestamp?: string;
15
+ provider?: string;
16
+ confidence?: number;
17
+ reason?: string;
18
+ }
19
+
20
+ /** A labeled data point with its provenance. */
21
+ export interface EvidenceRecord {
22
+ label: string;
23
+ value: unknown;
24
+ provenance: Provenance;
25
+ }
26
+
27
+ /** Successful provider result. */
28
+ export interface ProviderResultOk<T> {
29
+ status: "ok";
30
+ data: T;
31
+ timestamp: string;
32
+ stale?: boolean;
33
+ }
34
+
35
+ /** Failed/unavailable provider result. */
36
+ export interface ProviderResultUnavailable {
37
+ status: "unavailable";
38
+ reason: string;
39
+ provider: string;
40
+ }
41
+
42
+ /** Union of provider outcomes — every provider call returns one of these. */
43
+ export type ProviderResult<T> = ProviderResultOk<T> | ProviderResultUnavailable;
44
+
45
+ /** Type guard for successful provider results. */
46
+ export function isProviderOk<T>(result: ProviderResult<T>): result is ProviderResultOk<T> {
47
+ return result.status === "ok";
48
+ }
49
+
50
+ /** Convert a ProviderResult into an EvidenceRecord. */
51
+ export function toEvidenceRecord<T>(
52
+ label: string,
53
+ result: ProviderResult<T>,
54
+ providerId?: string,
55
+ ): EvidenceRecord {
56
+ if (isProviderOk(result)) {
57
+ return {
58
+ label,
59
+ value: result.data,
60
+ provenance: {
61
+ source: result.stale ? "stale_cache" : "fetched",
62
+ timestamp: result.timestamp,
63
+ provider: providerId,
64
+ confidence: result.stale ? 0.5 : undefined,
65
+ },
66
+ };
67
+ }
68
+ return {
69
+ label,
70
+ value: null,
71
+ provenance: {
72
+ source: "unavailable",
73
+ reason: result.reason,
74
+ provider: result.provider,
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,591 @@
1
+ import { parseToolTag } from "../onboarding/tool-tags.js";
2
+ import type { ParsedTag } from "../onboarding/tool-tags.js";
3
+ import type { CapabilityGapId, EvidencePlanId, TaskFamily } from "../routing/planning.js";
4
+ import type { ProviderResult } from "./evidence.js";
5
+
6
+ export type PlanningEvidenceType =
7
+ | "market_status"
8
+ | "ticker_disambiguation"
9
+ | "portfolio_exposure_map"
10
+ | "tool_result"
11
+ | "provider_gap";
12
+
13
+ export type PlanningProviderStatus =
14
+ | "available"
15
+ | "unavailable"
16
+ | "credential_required"
17
+ | "soft_degraded"
18
+ | "skipped";
19
+
20
+ export interface EvidenceSource {
21
+ toolName?: string;
22
+ provider?: string;
23
+ }
24
+
25
+ export interface EntityScope {
26
+ symbols?: string[];
27
+ query?: string;
28
+ }
29
+
30
+ export interface RawTracePointer {
31
+ traceId?: string;
32
+ turnIndex?: number;
33
+ toolCallIndex?: number;
34
+ toolCallId?: string;
35
+ toolName?: string;
36
+ customEntryType?: string;
37
+ }
38
+
39
+ export interface EvidenceGap {
40
+ kind: "provider_status" | "capability_gap" | "roadmap_placeholder";
41
+ providerStatus?: Exclude<PlanningProviderStatus, "available">;
42
+ provider?: string;
43
+ capabilityGapId?: CapabilityGapId;
44
+ reason: string;
45
+ remediation?: string;
46
+ silenced?: boolean;
47
+ rawTag?: string;
48
+ }
49
+
50
+ export interface PlanningEvidenceRecord {
51
+ id: string;
52
+ evidenceType: PlanningEvidenceType;
53
+ source: EvidenceSource;
54
+ entityScope: EntityScope;
55
+ observedAt: string;
56
+ providerStatus: PlanningProviderStatus;
57
+ normalizedFacts: Record<string, unknown>;
58
+ rawTracePointer?: RawTracePointer;
59
+ gaps: EvidenceGap[];
60
+ caveats: string[];
61
+ }
62
+
63
+ export interface EvidencePlanDefinition {
64
+ id: EvidencePlanId;
65
+ taskFamilies: TaskFamily[];
66
+ implemented: boolean;
67
+ requiredEvidence: PlanningEvidenceType[];
68
+ optionalEvidence: PlanningEvidenceType[];
69
+ capabilityGapIds: CapabilityGapId[];
70
+ roadmapPlaceholder?: string;
71
+ }
72
+
73
+ export interface MarketStatusInput {
74
+ text: string;
75
+ now?: Date;
76
+ timezone?: string;
77
+ traceId?: string;
78
+ }
79
+
80
+ export interface TickerDisambiguationInput {
81
+ text: string;
82
+ symbols?: string[];
83
+ traceId?: string;
84
+ }
85
+
86
+ export interface PortfolioExposureMapInput {
87
+ text: string;
88
+ now?: Date;
89
+ traceId?: string;
90
+ }
91
+
92
+ export interface PortfolioExposureSleeve {
93
+ label: string;
94
+ normalizedSleeve: string;
95
+ percent: number;
96
+ }
97
+
98
+ export interface ToolCallEvidenceInput {
99
+ name: string;
100
+ args: Record<string, unknown>;
101
+ result: unknown;
102
+ isError?: boolean;
103
+ }
104
+
105
+ export interface ToolTraceOptions {
106
+ traceId?: string;
107
+ turnIndex?: number;
108
+ toolCallIndex?: number;
109
+ toolCallId?: string;
110
+ }
111
+
112
+ export interface NormalizedProviderGap {
113
+ providerStatus: Exclude<PlanningProviderStatus, "available">;
114
+ provider: string;
115
+ reason: string;
116
+ remediation?: string;
117
+ silenced?: boolean;
118
+ rawTag: string;
119
+ }
120
+
121
+ export const EVIDENCE_PLAN_REGISTRY: Record<EvidencePlanId, EvidencePlanDefinition> = {
122
+ market_status: {
123
+ id: "market_status",
124
+ taskFamilies: ["current_event_explanation", "macro_allocation_review"],
125
+ implemented: true,
126
+ requiredEvidence: ["market_status"],
127
+ optionalEvidence: ["tool_result"],
128
+ capabilityGapIds: ["market_calendar"],
129
+ },
130
+ ticker_disambiguation: {
131
+ id: "ticker_disambiguation",
132
+ taskFamilies: ["ticker_disambiguation"],
133
+ implemented: true,
134
+ requiredEvidence: ["ticker_disambiguation"],
135
+ optionalEvidence: ["tool_result"],
136
+ capabilityGapIds: ["earnings_event_risk"],
137
+ },
138
+ placeholder_single_asset_decision: placeholderPlan("placeholder_single_asset_decision", ["single_asset_decision"], []),
139
+ placeholder_asset_compare: placeholderPlan("placeholder_asset_compare", ["asset_compare"], ["etf_holdings_overlap"]),
140
+ placeholder_portfolio_build: placeholderPlan("placeholder_portfolio_build", ["portfolio_build"], []),
141
+ placeholder_portfolio_review: placeholderPlan("placeholder_portfolio_review", ["portfolio_review"], []),
142
+ placeholder_options_strategy: placeholderPlan("placeholder_options_strategy", ["options_strategy"], []),
143
+ placeholder_current_event_explanation: placeholderPlan("placeholder_current_event_explanation", ["current_event_explanation"], ["market_calendar"]),
144
+ placeholder_sentiment_snapshot: placeholderPlan("placeholder_sentiment_snapshot", ["sentiment_snapshot"], ["sentiment_sample_depth"]),
145
+ placeholder_filing_thesis_review: placeholderPlan("placeholder_filing_thesis_review", ["filing_thesis_review"], []),
146
+ placeholder_retail_finance_tradeoff: placeholderPlan("placeholder_retail_finance_tradeoff", ["retail_finance_tradeoff"], [
147
+ "brokerage_comparison",
148
+ "cash_yield_products",
149
+ "fund_tax_efficiency",
150
+ ]),
151
+ placeholder_concept_explainer: placeholderPlan("placeholder_concept_explainer", ["concept_explainer"], []),
152
+ placeholder_backtest_review: placeholderPlan("placeholder_backtest_review", ["backtest_review"], []),
153
+ placeholder_stateful_tracking_update: placeholderPlan("placeholder_stateful_tracking_update", ["stateful_tracking_update"], []),
154
+ placeholder_general_fallback: placeholderPlan("placeholder_general_fallback", ["general_fallback"], []),
155
+ };
156
+
157
+ const KNOWN_US_MARKET_HOLIDAYS: Record<string, string> = {
158
+ "2026-01-01": "New Year's Day",
159
+ "2026-01-19": "Martin Luther King Jr. Day",
160
+ "2026-02-16": "Washington's Birthday",
161
+ "2026-04-03": "Good Friday",
162
+ "2026-05-25": "Memorial Day",
163
+ "2026-06-19": "Juneteenth",
164
+ "2026-07-03": "Independence Day observed",
165
+ "2026-09-07": "Labor Day",
166
+ "2026-11-26": "Thanksgiving Day",
167
+ "2026-12-25": "Christmas Day",
168
+ };
169
+
170
+ function placeholderPlan(
171
+ id: EvidencePlanId,
172
+ taskFamilies: TaskFamily[],
173
+ capabilityGapIds: CapabilityGapId[],
174
+ ): EvidencePlanDefinition {
175
+ return {
176
+ id,
177
+ taskFamilies,
178
+ implemented: false,
179
+ requiredEvidence: [],
180
+ optionalEvidence: ["tool_result"],
181
+ capabilityGapIds,
182
+ roadmapPlaceholder: "openspec/changes/prompt-to-policy-agent-planning/future-roadmap.md",
183
+ };
184
+ }
185
+
186
+ export function buildMarketStatusEvidence(input: MarketStatusInput): PlanningEvidenceRecord {
187
+ const timezone = input.timezone ?? "America/New_York";
188
+ const now = input.now ?? new Date();
189
+ const local = localDateTimeParts(now, timezone);
190
+ const temporalReferences = temporalReferencesFor(input.text);
191
+ const holidayName = KNOWN_US_MARKET_HOLIDAYS[local.date];
192
+ const isWeekend = local.weekday === "Sat" || local.weekday === "Sun";
193
+ const isMarketHoliday = holidayName !== undefined;
194
+ const marketStatus = classifyMarketStatus(local, isWeekend, isMarketHoliday, temporalReferences);
195
+ const lastTradingDate = marketStatus === "open" ? local.date : lastTradingDay(local.date);
196
+ const gaps: EvidenceGap[] = [{
197
+ kind: "capability_gap",
198
+ capabilityGapId: "market_calendar",
199
+ reason: "V1 uses deterministic weekday and known-holiday grounding, not a live exchange-calendar provider.",
200
+ }];
201
+
202
+ return {
203
+ id: "market_status:deterministic",
204
+ evidenceType: "market_status",
205
+ source: { toolName: "deterministic_market_status" },
206
+ entityScope: { query: input.text },
207
+ observedAt: now.toISOString(),
208
+ providerStatus: "available",
209
+ normalizedFacts: {
210
+ timezone,
211
+ localDate: local.date,
212
+ localTime: local.time,
213
+ weekday: local.weekday,
214
+ temporalReferences,
215
+ isWeekend,
216
+ isMarketHoliday,
217
+ holidayName,
218
+ marketStatus,
219
+ lastTradingDay: lastTradingDate,
220
+ quoteAsOfCaveat: quoteAsOfCaveat(marketStatus),
221
+ },
222
+ rawTracePointer: input.traceId ? { traceId: input.traceId, toolName: "deterministic_market_status" } : undefined,
223
+ gaps,
224
+ caveats: ["Market status is deterministic V1 grounding and should be verified against an exchange calendar for edge cases."],
225
+ };
226
+ }
227
+
228
+ export function buildTickerDisambiguationEvidence(
229
+ input: TickerDisambiguationInput,
230
+ ): PlanningEvidenceRecord {
231
+ const gaps: EvidenceGap[] = [{
232
+ kind: "capability_gap",
233
+ capabilityGapId: "earnings_event_risk",
234
+ reason: "Ticker disambiguation can preserve symbol-verification gaps, but V1 does not add richer earnings-event data.",
235
+ }];
236
+
237
+ return {
238
+ id: "ticker_disambiguation:selected_slice",
239
+ evidenceType: "ticker_disambiguation",
240
+ source: { toolName: "planning_ticker_disambiguation" },
241
+ entityScope: { symbols: [...(input.symbols ?? [])], query: input.text },
242
+ observedAt: new Date(0).toISOString(),
243
+ providerStatus: "available",
244
+ normalizedFacts: {
245
+ selectedMigrationSlice: "ticker_disambiguation",
246
+ requestedSymbols: [...(input.symbols ?? [])],
247
+ requiresSymbolVerification: true,
248
+ legacyBehaviorRemainsActive: true,
249
+ },
250
+ rawTracePointer: input.traceId ? { traceId: input.traceId, toolName: "planning_ticker_disambiguation" } : undefined,
251
+ gaps,
252
+ caveats: input.symbols?.length ? [] : ["No symbol was verified by this evidence plan; active tools remain responsible for lookup."],
253
+ };
254
+ }
255
+
256
+ export function buildPortfolioExposureMapEvidence(
257
+ input: PortfolioExposureMapInput,
258
+ ): PlanningEvidenceRecord {
259
+ const sleeves = extractPortfolioSleeves(input.text);
260
+ const normalizedSleeves = new Set(sleeves.map((sleeve) => sleeve.normalizedSleeve));
261
+ const hasBroadIndex = normalizedSleeves.has("broad_us_index") || normalizedSleeves.has("broad_equity");
262
+ const hasSectorSleeve = sleeves.some((sleeve) => sleeve.normalizedSleeve.endsWith("_sector"));
263
+ const broadIndexOverlapCaveat = hasBroadIndex && hasSectorSleeve;
264
+
265
+ return {
266
+ id: "portfolio_exposure_map:deterministic",
267
+ evidenceType: "portfolio_exposure_map",
268
+ source: { toolName: "deterministic_portfolio_exposure_map" },
269
+ entityScope: { query: input.text },
270
+ observedAt: (input.now ?? new Date()).toISOString(),
271
+ providerStatus: "available",
272
+ normalizedFacts: {
273
+ directSleeves: sleeves,
274
+ directExposureTotalPercent: roundPercent(sleeves.reduce((sum, sleeve) => sum + sleeve.percent, 0)),
275
+ broadIndexOverlapCaveat,
276
+ exactHoldingsOverlapAvailable: false,
277
+ targetBandGuidanceNeeded: /\b(?:rebalance|target\s+bands?|drift|overweight|underweight|concentration|diversif)/i.test(input.text),
278
+ },
279
+ rawTracePointer: input.traceId ? {
280
+ traceId: input.traceId,
281
+ toolName: "deterministic_portfolio_exposure_map",
282
+ } : undefined,
283
+ gaps: [{
284
+ kind: "capability_gap",
285
+ capabilityGapId: "etf_holdings_overlap",
286
+ reason: "V1 records user-stated sleeves and overlap caveats but does not fetch exact ETF/index holdings or issuer weights.",
287
+ }],
288
+ caveats: [
289
+ "Exact ETF/index holdings overlap requires a provider-backed holdings source.",
290
+ ...(broadIndexOverlapCaveat
291
+ ? ["Broad index exposure may already include sector exposure; V1 does not estimate exact constituent weights."]
292
+ : []),
293
+ ],
294
+ };
295
+ }
296
+
297
+ export function captureEvidenceFromToolCall(
298
+ toolCall: ToolCallEvidenceInput,
299
+ options: ToolTraceOptions = {},
300
+ ): PlanningEvidenceRecord {
301
+ const text = toolResultText(toolCall.result);
302
+ const providerGap = text ? normalizeProviderGapFromToolText(text) : undefined;
303
+ const providerStatus = providerGap?.providerStatus ?? (toolCall.isError ? "unavailable" : "available");
304
+ const gaps = providerGap ? [providerGapToEvidenceGap(providerGap)] : [];
305
+ if (toolCall.isError && !providerGap) {
306
+ gaps.push({
307
+ kind: "provider_status",
308
+ providerStatus: "unavailable",
309
+ reason: "Tool call returned an error.",
310
+ });
311
+ }
312
+
313
+ return {
314
+ id: `tool_result:${toolCall.name}`,
315
+ evidenceType: providerGap ? "provider_gap" : "tool_result",
316
+ source: {
317
+ toolName: toolCall.name,
318
+ provider: providerGap?.provider,
319
+ },
320
+ entityScope: symbolsFromArgs(toolCall.args),
321
+ observedAt: new Date(0).toISOString(),
322
+ providerStatus,
323
+ normalizedFacts: {
324
+ toolName: toolCall.name,
325
+ args: { ...toolCall.args },
326
+ isError: toolCall.isError === true,
327
+ providerStatus,
328
+ },
329
+ rawTracePointer: {
330
+ ...options,
331
+ toolName: toolCall.name,
332
+ },
333
+ gaps,
334
+ caveats: [],
335
+ };
336
+ }
337
+
338
+ export function normalizeProviderGapFromToolText(text: string): NormalizedProviderGap | undefined {
339
+ const parsed = parseToolTag(text);
340
+ if (!parsed || parsed.kind === "connected") return undefined;
341
+
342
+ return {
343
+ providerStatus: providerStatusFromTag(parsed),
344
+ provider: parsed.provider,
345
+ reason: reasonFromTag(parsed),
346
+ remediation: remediationFromTag(parsed),
347
+ silenced: parsed.kind === "skipped" ? parsed.silenced : undefined,
348
+ rawTag: text,
349
+ };
350
+ }
351
+
352
+ export function providerResultToPlanningEvidence<T>(
353
+ label: string,
354
+ result: ProviderResult<T>,
355
+ rawTracePointer?: RawTracePointer,
356
+ ): PlanningEvidenceRecord {
357
+ if (result.status === "ok") {
358
+ return {
359
+ id: `provider_result:${label}`,
360
+ evidenceType: "tool_result",
361
+ source: {},
362
+ entityScope: {},
363
+ observedAt: result.timestamp,
364
+ providerStatus: "available",
365
+ normalizedFacts: { label, data: result.data, stale: result.stale === true },
366
+ rawTracePointer,
367
+ gaps: [],
368
+ caveats: result.stale ? ["Provider result came from stale cache."] : [],
369
+ };
370
+ }
371
+
372
+ return {
373
+ id: `provider_result:${label}`,
374
+ evidenceType: "provider_gap",
375
+ source: { provider: result.provider },
376
+ entityScope: {},
377
+ observedAt: new Date(0).toISOString(),
378
+ providerStatus: "unavailable",
379
+ normalizedFacts: { label },
380
+ rawTracePointer,
381
+ gaps: [{
382
+ kind: "provider_status",
383
+ providerStatus: "unavailable",
384
+ provider: result.provider,
385
+ reason: result.reason,
386
+ }],
387
+ caveats: [],
388
+ };
389
+ }
390
+
391
+ function providerGapToEvidenceGap(gap: NormalizedProviderGap): EvidenceGap {
392
+ return {
393
+ kind: "provider_status",
394
+ providerStatus: gap.providerStatus,
395
+ provider: gap.provider,
396
+ reason: gap.reason,
397
+ remediation: gap.remediation,
398
+ silenced: gap.silenced,
399
+ rawTag: gap.rawTag,
400
+ };
401
+ }
402
+
403
+ function providerStatusFromTag(tag: Exclude<ParsedTag, { kind: "connected" }>): Exclude<PlanningProviderStatus, "available"> {
404
+ if (tag.kind === "credential_required") return "credential_required";
405
+ if (tag.kind === "soft_degraded") return "soft_degraded";
406
+ return "skipped";
407
+ }
408
+
409
+ function reasonFromTag(tag: Exclude<ParsedTag, { kind: "connected" }>): string {
410
+ if (tag.kind === "credential_required") {
411
+ return tag.reason === "stale" ? "credential_stale" : "credential_missing";
412
+ }
413
+ if (tag.kind === "soft_degraded") {
414
+ return `soft_degraded_to_${tag.fallback}`;
415
+ }
416
+ return tag.reason;
417
+ }
418
+
419
+ function remediationFromTag(tag: Exclude<ParsedTag, { kind: "connected" }>): string | undefined {
420
+ if (tag.kind === "soft_degraded") return tag.remediation;
421
+ if (tag.kind === "skipped") return tag.remediation;
422
+ return "run /connect";
423
+ }
424
+
425
+ function toolResultText(result: unknown): string | undefined {
426
+ if (typeof result === "string") return result;
427
+ if (!isRecord(result)) return undefined;
428
+ const content = result.content;
429
+ if (typeof content === "string") return content;
430
+ if (Array.isArray(content)) {
431
+ return content.map((item) => {
432
+ if (typeof item === "string") return item;
433
+ if (isRecord(item) && typeof item.text === "string") return item.text;
434
+ return "";
435
+ }).filter(Boolean).join("\n");
436
+ }
437
+ return undefined;
438
+ }
439
+
440
+ function symbolsFromArgs(args: Record<string, unknown>): EntityScope {
441
+ const symbol = args.symbol;
442
+ const symbols = args.symbols;
443
+ if (typeof symbol === "string") return { symbols: [symbol] };
444
+ if (Array.isArray(symbols) && symbols.every((item) => typeof item === "string")) {
445
+ return { symbols: [...symbols] };
446
+ }
447
+ return {};
448
+ }
449
+
450
+ function extractPortfolioSleeves(text: string): PortfolioExposureSleeve[] {
451
+ const matches: PortfolioExposureSleeve[] = [];
452
+ const pattern = /(\d+(?:\.\d+)?)\s*%\s*(?:in|to|of|toward|towards|allocated\s+to)?\s*([^,;.\n?]+)/gi;
453
+ for (const match of text.matchAll(pattern)) {
454
+ const percent = Number(match[1]);
455
+ if (!Number.isFinite(percent)) continue;
456
+ const label = cleanSleeveLabel(match[2] ?? "");
457
+ if (!label) continue;
458
+ matches.push({
459
+ label,
460
+ normalizedSleeve: normalizePortfolioSleeve(label),
461
+ percent: roundPercent(percent),
462
+ });
463
+ }
464
+ return matches;
465
+ }
466
+
467
+ function cleanSleeveLabel(label: string): string {
468
+ return label
469
+ .replace(/^(?:and|plus|with)\s+/i, "")
470
+ .replace(/\b(?:should|can|could|would|do|does|rebalance|diversify|target|bands?).*$/i, "")
471
+ .trim();
472
+ }
473
+
474
+ function normalizePortfolioSleeve(label: string): string {
475
+ const lower = label.toLowerCase();
476
+ if (/\b(?:s&p\s*500|sp\s*500|index|voo|spy|ivv)\b/.test(lower)) return "broad_us_index";
477
+ if (/\b(?:tech|technology|software|semis?|semiconductors?)\b/.test(lower)) return "technology_sector";
478
+ if (/\b(?:bond|bonds|fixed\s+income|treasur(?:y|ies))\b/.test(lower)) return "bonds";
479
+ if (/\b(?:cash|savings?|money\s*market|t[-\s]?bills?)\b/.test(lower)) return "cash";
480
+ if (/\b(?:international|ex[-\s]?us|foreign|global)\b/.test(lower)) return "international_equity";
481
+ if (/\b(?:stock|stocks|equity|equities)\b/.test(lower)) return "broad_equity";
482
+ return lower
483
+ .replace(/[^a-z0-9]+/g, "_")
484
+ .replace(/^_+|_+$/g, "")
485
+ .slice(0, 48) || "unknown_sleeve";
486
+ }
487
+
488
+ function roundPercent(value: number): number {
489
+ return Math.round(value * 100) / 100;
490
+ }
491
+
492
+ function temporalReferencesFor(text: string): string[] {
493
+ const lower = text.toLowerCase();
494
+ const refs: string[] = [];
495
+ if (/\btoday\b/.test(lower)) refs.push("today");
496
+ if (/\bright now\b/.test(lower)) refs.push("right_now");
497
+ if (/\bthis morning\b/.test(lower)) refs.push("this_morning");
498
+ if (/\bafter close\b|\bafter the close\b/.test(lower)) refs.push("after_close");
499
+ if (/\bmarket(?: is|'s)? closed\b|\bmarket-closed\b/.test(lower)) refs.push("market_closed");
500
+ if (/\bweekend\b/.test(lower)) refs.push("weekend");
501
+ if (/\bholiday\b/.test(lower)) refs.push("holiday");
502
+ return refs;
503
+ }
504
+
505
+ function classifyMarketStatus(
506
+ local: LocalDateTimeParts,
507
+ isWeekend: boolean,
508
+ isMarketHoliday: boolean,
509
+ temporalReferences: string[],
510
+ ): string {
511
+ if (isWeekend) return "closed_weekend";
512
+ if (isMarketHoliday) return "closed_holiday";
513
+ if (local.minutesSinceMidnight < 9 * 60 + 30) return "pre_market";
514
+ if (local.minutesSinceMidnight < 16 * 60) return "open";
515
+ if (temporalReferences.includes("after_close")) return "after_close";
516
+ return "closed_after_hours";
517
+ }
518
+
519
+ function quoteAsOfCaveat(marketStatus: string): string {
520
+ if (marketStatus === "open") return "Quote evidence may be intraday or delayed depending on provider.";
521
+ if (marketStatus === "pre_market") return "Quote evidence may reflect the previous regular session until premarket data is fetched.";
522
+ return "Quote evidence should distinguish the current calendar date from the most recent trading session.";
523
+ }
524
+
525
+ interface LocalDateTimeParts {
526
+ date: string;
527
+ time: string;
528
+ weekday: string;
529
+ minutesSinceMidnight: number;
530
+ }
531
+
532
+ function localDateTimeParts(date: Date, timezone: string): LocalDateTimeParts {
533
+ const formatter = new Intl.DateTimeFormat("en-US", {
534
+ timeZone: timezone,
535
+ year: "numeric",
536
+ month: "2-digit",
537
+ day: "2-digit",
538
+ hour: "2-digit",
539
+ minute: "2-digit",
540
+ hour12: false,
541
+ weekday: "short",
542
+ });
543
+ const parts = formatter.formatToParts(date).reduce<Record<string, string>>((acc, part) => {
544
+ acc[part.type] = part.value;
545
+ return acc;
546
+ }, {});
547
+ const hour = Number(parts.hour);
548
+ const minute = Number(parts.minute);
549
+ return {
550
+ date: `${parts.year}-${parts.month}-${parts.day}`,
551
+ time: `${parts.hour}:${parts.minute}`,
552
+ weekday: parts.weekday ?? "",
553
+ minutesSinceMidnight: hour * 60 + minute,
554
+ };
555
+ }
556
+
557
+ function lastTradingDay(localDate: string): string {
558
+ let cursor = dateFromKey(localDate);
559
+ do {
560
+ cursor = addDays(cursor, -1);
561
+ } while (isWeekendOrKnownHoliday(dateKey(cursor)));
562
+ return dateKey(cursor);
563
+ }
564
+
565
+ function isWeekendOrKnownHoliday(key: string): boolean {
566
+ const date = dateFromKey(key);
567
+ const day = date.getUTCDay();
568
+ return day === 0 || day === 6 || KNOWN_US_MARKET_HOLIDAYS[key] !== undefined;
569
+ }
570
+
571
+ function dateFromKey(key: string): Date {
572
+ const [year, month, day] = key.split("-").map(Number);
573
+ return new Date(Date.UTC(year, month - 1, day));
574
+ }
575
+
576
+ function addDays(date: Date, days: number): Date {
577
+ const next = new Date(date);
578
+ next.setUTCDate(next.getUTCDate() + days);
579
+ return next;
580
+ }
581
+
582
+ function dateKey(date: Date): string {
583
+ const year = date.getUTCFullYear();
584
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
585
+ const day = String(date.getUTCDate()).padStart(2, "0");
586
+ return `${year}-${month}-${day}`;
587
+ }
588
+
589
+ function isRecord(value: unknown): value is Record<string, unknown> {
590
+ return typeof value === "object" && value !== null && !Array.isArray(value);
591
+ }