Compare commits

...
Sign in to create a new pull request.

20 commits

Author SHA1 Message Date
cc5a662d1a remove readme mirror notice
Some checks are pending
CI / Validate (push) Waiting to run
2026-06-06 06:05:17 -04:00
b26ea54eda move mirror notice above logo
Some checks are pending
CI / Validate (push) Waiting to run
2026-06-06 06:04:22 -04:00
73b3d530d1 add mirror notice to readme
Some checks are pending
CI / Validate (push) Waiting to run
2026-06-06 05:32:08 -04:00
1357ddaaee bd: sync server issues
All checks were successful
CI / Validate (push) Successful in 1m20s
2026-06-01 13:53:02 -04:00
d7a34f36b6 bd: update sync.remote
All checks were successful
CI / Validate (push) Successful in 1m20s
2026-06-01 09:58:47 -04:00
1133f0e705 bd: clear sync.remote 2026-06-01 09:58:47 -04:00
8ee7cbecb4 bd: update sync.remote 2026-06-01 09:58:47 -04:00
aae904f334 bd: clear sync.remote 2026-06-01 09:58:47 -04:00
ab35533289 trim repo turn doc guidance
All checks were successful
CI / Validate (push) Successful in 1m12s
2026-05-31 09:29:53 -04:00
44431c4e66 expand ci quality gates
All checks were successful
CI / Validate (push) Successful in 1m13s
2026-05-30 02:34:28 -04:00
65139bf8d0 close forgejo ci terminal issue
All checks were successful
CI / Validate (push) Successful in 1m11s
2026-05-30 02:00:49 -04:00
01c7ca0b2f fix terminal pathname import for forgejo
All checks were successful
CI / Validate (push) Successful in 1m16s
2026-05-30 01:58:37 -04:00
92d5db44e9 fix forgejo terminal test module resolution
Some checks failed
CI / Validate (push) Failing after 1m2s
2026-05-30 01:54:15 -04:00
f9682ca9ea fix terminal test navigation alias
Some checks failed
CI / Validate (push) Failing after 1m4s
2026-05-30 01:49:11 -04:00
4ae32c4f3b stabilize forgejo ci bun path and mocks
Some checks failed
CI / Validate (push) Failing after 1m7s
2026-05-30 01:44:45 -04:00
e5867e6f73 fix forgejo bun path for ci scripts
Some checks failed
CI / Validate (push) Failing after 1m3s
2026-05-30 01:37:43 -04:00
c80d88bc5f fix ci typecheck bun path
Some checks failed
CI / Validate (push) Failing after 20s
2026-05-30 01:35:08 -04:00
7607571c80 fix electron node-gyp resolution for ci installs
Some checks failed
CI / Validate (push) Failing after 26s
2026-05-29 23:24:35 -04:00
fdef33db0d Merge pull request 'clarify turn doc diff rendering' (#14) from lavender/clarify-diffs-ssr-docs into main
Some checks failed
CI / Validate (push) Failing after 21s
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/14
2026-05-30 02:04:02 +00:00
31f72ecd38 clarify turn doc diff rendering
Some checks failed
CI / Validate (pull_request) Failing after 47s
2026-05-29 22:02:27 -04:00
76 changed files with 2815 additions and 1301 deletions

View file

@ -53,7 +53,4 @@
# - github.org
# - github.repo
sync:
remote: git+https://git.deltaisland.io/dirtydishes/islandflow.git
sync.remote: "git+https://github.com/dirtydishes/islandflow.git"
sync.remote: "http://dolt.deltaisland.io/islandflow"

View file

@ -1,3 +1,5 @@
{"_type":"issue","id":"islandflow-9ur","title":"address forgejo issue 15 tmp cve","description":"Track remediation for Forgejo issue #15: update tmp from vulnerable 0.2.5 to patched 0.2.6+ via root override and refreshed Bun lockfile, then validate with audit/tests.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-01T17:32:18Z","created_by":"dirtydishes","updated_at":"2026-06-01T17:36:01Z","started_at":"2026-06-01T17:32:23Z","closed_at":"2026-06-01T17:36:01Z","close_reason":"Resolved Forgejo issue #15 by bumping the tmp override to ^0.2.6, refreshing bun.lock to tmp@0.2.7, and validating with bun audit, bun why tmp, and bun test.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-m3d","title":"fix docs mirroring to github pages","description":"The repository docs folder is supposed to mirror to dirtydishes.github.io for GitHub Pages, but the mirroring is not working. Investigate the docs publishing workflow and repair the configuration or scripts so docs can be published reliably.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-31T22:05:48Z","created_by":"dirtydishes","updated_at":"2026-05-31T22:12:26Z","started_at":"2026-05-31T22:05:56Z","closed_at":"2026-05-31T22:12:26Z","close_reason":"Updated docs Pages workflow to publish into dirtydishes/dirtydishes.github.io under islandflow/docs, tightened docs index generation, regenerated docs index, and documented validation/limitations.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-2op","title":"[bug] Desktop app unclickable and no live data in hosted shell","description":"## Summary\\nDesktop Electron shell appears fully non-interactive (clicks do not work) and no live market data reaches the UI.\\n\\n## Why this matters\\nDesktop wrapper is currently unusable for core workflow and blocks users from validating market streams outside browser.\\n\\n## Scope\\nReproduce issue locally, identify root cause(s) in Electron shell and frontend integration, implement fix, and validate interactivity + data flow end-to-end.\\n\\n## Acceptance Criteria\\n- Desktop app responds to pointer interactions (navigation/actions clickable)\\n- Live data stream connects and updates UI in desktop mode\\n- Regression coverage or guardrails added where practical\\n- Findings and validation documented","status":"in_progress","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-24T04:23:55Z","created_by":"dirtydishes","updated_at":"2026-05-24T04:23:57Z","started_at":"2026-05-24T04:23:57Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jad","title":"Sync docs pages workflow fix to github mirror","description":"GitHub is still running an older docs Pages workflow with configure-pages because github/main is behind forgejo/main. Push the already-fixed workflow commit to the GitHub mirror so Actions runs the gh-pages branch deployment flow instead.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T22:27:46Z","created_by":"dirtydishes","updated_at":"2026-05-23T22:28:24Z","started_at":"2026-05-23T22:28:10Z","closed_at":"2026-05-23T22:28:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-bc7","title":"Fix docs Pages workflow configure-pages failure","description":"Replace the current docs Pages deployment flow so workflow runs succeed even when configure-pages cannot read or enable the site. Keep published docs target behavior for dirtydishes.github.io/islandflow/docs.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T22:23:28Z","created_by":"dirtydishes","updated_at":"2026-05-23T22:25:19Z","started_at":"2026-05-23T22:23:31Z","closed_at":"2026-05-23T22:25:19Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -9,6 +11,7 @@
{"_type":"issue","id":"islandflow-yza","title":"Persist historical flow packets for alert detail replay","description":"## Why\nAlert details can show a missing persisted flow packet when the packet is no longer present in the Redis hot cache, even though the associated historical alert and evidence were loaded from ClickHouse.\n\n## What needs to be done\nTrace the API path that resolves alert detail flow packets, compare Redis hot-cache lookups with ClickHouse historical fetches, and ensure historical flow packet payloads are treated as first-class persisted data with context preserved when replaying or loading older alerts.\n\n## Acceptance Criteria\n- Alert detail flow packets load for historical alerts even when the packet is absent from Redis hot cache\n- Historical ClickHouse-backed flow packet responses preserve the context required by the UI\n- Relevant automated tests cover the regression or the gap is explicitly documented","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T06:52:04Z","created_by":"dirtydishes","updated_at":"2026-05-20T06:59:26Z","started_at":"2026-05-20T06:52:09Z","closed_at":"2026-05-20T06:59:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jor","title":"Support Forgejo pull request status in desktop git panel","description":"The desktop app currently reports pull request status unavailable when a repository only has a Forgejo remote. Add native Forgejo/Gitea-style remote detection and pull request status lookup so Forgejo-only repositories can show PR state in the Codex app git panel.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T20:55:15Z","created_by":"dirtydishes","updated_at":"2026-05-19T20:59:46Z","started_at":"2026-05-19T20:55:25Z","closed_at":"2026-05-19T20:59:46Z","close_reason":"Patched the installed Codex desktop app bundle with a Forgejo PR status fallback and documented the local change.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-g3a","title":"Reconcile PR merge conflicts","description":"Resolve the current pull request conflicts for the nextjs-upgrade branch, validate the result, document the turn, and push the reconciled branch.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:44:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:47:35Z","started_at":"2026-05-19T18:44:56Z","closed_at":"2026-05-19T18:47:35Z","close_reason":"Merged forgejo/main into nextjs-upgrade, resolved README and Beads conflicts, updated JetStream retention tests, validated deploy help, Docker workspace sync, API/bus tests, and web build, and added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9rc","title":"Implement native fast iterative deploy plan","description":"Implement the checked-in plan at plans/2026-05-18-native-fast-iterative-deploy-plan.md. Cover deploy-phase timing instrumentation, native deployment operational assets, deploy guardrails, validation/cutover documentation, and any required live VPS remediation that is safely actionable from this session. Track follow-up items separately if anything cannot be completed in-repo or on the live host.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:15:19Z","created_by":"dirtydishes","updated_at":"2026-05-18T07:34:03Z","started_at":"2026-05-18T07:15:25Z","closed_at":"2026-05-18T07:34:03Z","close_reason":"Implemented the native fast iterative deploy plan with deploy timing summaries, worker-only native fast mode, edge-cutover guardrails, local-on-server execution support, checked-in native ops assets, live audit findings, and turn documentation. Remaining cutover work is tracked in islandflow-vvw.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8kj","title":"Configure persistent beads Dolt remote on deltaisland server","description":"Install the beads and Dolt CLIs on the server, configure a persistent Dolt sync remote backed by the server-hosted Forgejo repository, verify refs/dolt/data publication, and document Nginx Proxy Manager / firewall considerations.","status":"closed","priority":1,"issue_type":"task","assignee":"delta","created_at":"2026-05-17T10:31:31Z","created_by":"delta","updated_at":"2026-05-17T10:37:47Z","started_at":"2026-05-17T10:32:16Z","closed_at":"2026-05-17T10:37:47Z","close_reason":"Installed bd and dolt on the server, configured the Forgejo-backed Dolt remote, published refs/dolt/data, and documented the setup.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-200","title":"Implement durable options tape history","description":"Implement the plan from docs/plans/2026-05-16-1711-durable-options-tape-history.html: durable ClickHouse-backed options history, signal/all prints view selection, preserved execution context, stale semantics limited to live health, reset runbook, tests, and turn documentation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-16T21:21:30Z","created_by":"dirtydishes","updated_at":"2026-05-16T21:26:51Z","started_at":"2026-05-16T21:21:33Z","closed_at":"2026-05-16T21:26:51Z","close_reason":"Implemented durable options tape history, signal/raw view selection, reset runbook, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -24,6 +27,15 @@
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xmi","title":"Resolve conflicts in PR 45","description":"Resolve the merge conflicts blocking Forgejo PR 45, validate the affected code paths, and push the reconciled branch back to Forgejo.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-06T03:33:52Z","created_by":"dirtydishes","updated_at":"2026-06-06T03:35:16Z","started_at":"2026-06-06T03:33:58Z","closed_at":"2026-06-06T03:35:16Z","close_reason":"Resolved the PR 45 merge conflict in .beads/issues.jsonl and validated the reconciled tracker file.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8a6","title":"verify github pages token for docs mirror","description":"The docs mirror workflow now publishes islandflow/docs into dirtydishes/dirtydishes.github.io, but the GitHub Actions secret DOCS_PAGES_TOKEN must exist and have permission to push to that Pages repository. Verify the secret is configured and manually run the Publish Docs workflow after the mirror branch lands.","notes":"Direct manual publish to dirtydishes/dirtydishes.github.io succeeded on 2026-06-01 and https://dirtydishes.github.io/islandflow/docs/ returned HTTP 200. Remaining work is to verify DOCS_PAGES_TOKEN so the islandflow docs mirror workflow can publish future updates automatically.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-31T22:12:27Z","created_by":"dirtydishes","updated_at":"2026-06-01T13:45:34Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-5jt","title":"Add anatomy reference page","description":"Create a standalone docs/anatomy.html reference explaining how prints move through ingest, tape, flow packets, smart-money events, classifier hits, and alerts, including diagrams.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-31T21:20:34Z","created_by":"dirtydishes","updated_at":"2026-05-31T21:25:54Z","started_at":"2026-05-31T21:20:44Z","closed_at":"2026-05-31T21:25:54Z","close_reason":"Added the standalone anatomy reference page and linked it from the docs index.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-cig","title":"Expand CI quality gates","description":"Add a more robust CI workflow for the Bun/TypeScript monorepo, including formatting, linting, type checking, builds, and tests where appropriate.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T06:29:33Z","created_by":"dirtydishes","updated_at":"2026-05-30T06:34:11Z","started_at":"2026-05-30T06:29:41Z","closed_at":"2026-05-30T06:34:11Z","close_reason":"Expanded CI quality gates with Biome formatting/linting, public API route checks, Docker snapshot validation, tests, typecheck, and web build validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-3l6","title":"fix ci typecheck bun path resolution","description":"Forgejo CI fails in scripts/typecheck.ts because the script shells out to bunx, which expects bun on PATH. The runner installs Bun by absolute path, so the typecheck helper should use the current Bun executable instead of PATH lookup.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T05:34:55Z","created_by":"dirtydishes","updated_at":"2026-05-30T06:00:31Z","started_at":"2026-05-30T05:35:02Z","closed_at":"2026-05-30T06:00:31Z","close_reason":"Fixed the Forgejo CI terminal import mismatch by switching the terminal client component to a namespace import; verified locally and on Forgejo run #56.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-wtg","title":"Harden drawer dialog focus behavior","description":"Fix terminal drawers so they expose modal dialog semantics, trap keyboard focus while open, and restore focus to the invoking control after close.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:55:25Z","created_by":"dirtydishes","updated_at":"2026-05-29T23:09:45Z","started_at":"2026-05-29T22:56:22Z","closed_at":"2026-05-29T23:09:45Z","close_reason":"Implemented modal dialog semantics, focus trapping, Escape dismissal, focus restoration, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-833","title":"Improve narrow options table responsiveness","description":"Adapt the Options route for narrow screens so dense tape tables remain contained in their panes, preserve row identity while horizontally panning, and keep the mobile ticker/filter controls readable.","acceptance_criteria":"Options tape panes have bounded heights on narrow screens; table body scrolls internally; first table column remains visible while panning; mobile topbar and filter controls have adequate spacing; web production build passes.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:34:05Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:36:20Z","started_at":"2026-05-29T22:34:24Z","closed_at":"2026-05-29T22:36:20Z","close_reason":"Implemented narrow-screen options pane containment, sticky row context, touch-scroll affordances, and mobile control spacing. Validated with web build and in-browser narrow viewport checks.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-aq9","title":"Harden terminal UI error and overflow states","description":"Harden the web terminal against oversized API errors, non-JSON synthetic admin failures, and long status text so live trading panes remain stable under bad network/backend responses.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:10:16Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:13:37Z","closed_at":"2026-05-29T22:13:37Z","close_reason":"Hardened terminal UI error rendering, synthetic admin failure parsing, long-message wrapping, and added focused tests.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-ggm","title":"Harden web terminal UI states","description":"Improve the web terminal surface so it handles loading, empty data, API failures, overflow, and accessible live-status behavior more robustly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T21:59:45Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:05:45Z","started_at":"2026-05-29T21:59:59Z","closed_at":"2026-05-29T22:05:45Z","close_reason":"Hardened web terminal status announcements, empty states, table semantics, clipped-cell fallbacks, tests, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-dk5","title":"Remove frontend cooker route","description":"Remove the experimental /frontend-cooker page and update repository references that still list it as an available public route.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T13:50:38Z","created_by":"dirtydishes","updated_at":"2026-05-29T13:53:05Z","started_at":"2026-05-29T13:50:48Z","closed_at":"2026-05-29T13:53:05Z","close_reason":"Removed the /frontend-cooker Next.js route, cleaned route/scanner references, documented the work, and validated the web build.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-ep2","title":"Configure Impeccable live mode","description":"Initialize the repository's Impeccable live-mode configuration so future design iteration can start without first-time setup.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T08:03:47Z","created_by":"dirtydishes","updated_at":"2026-05-29T08:05:01Z","started_at":"2026-05-29T08:03:52Z","closed_at":"2026-05-29T08:05:01Z","close_reason":"Configured Impeccable live mode and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9en","title":"Install Impeccable skill for Codex","description":"Install the Impeccable skill in the Codex-compatible project locations after the upstream installer selected unused harness folders.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T07:59:10Z","created_by":"dirtydishes","updated_at":"2026-05-29T07:59:22Z","started_at":"2026-05-29T07:59:18Z","closed_at":"2026-05-29T07:59:22Z","close_reason":"Installed Impeccable into .agents and mirrored it into .codex/skills for Codex use.","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -38,6 +50,8 @@
{"_type":"issue","id":"islandflow-kgu","title":"Reconcile PR #8 branch with current main","description":"Why this issue exists and what needs to be done: user requested reconciliation for PR #8. Identify the PR #8 branch, merge/rebase with current main, resolve conflicts, validate, and push the updated branch so the PR can merge cleanly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T20:14:36Z","created_by":"dirtydishes","updated_at":"2026-05-23T20:24:29Z","started_at":"2026-05-23T20:14:39Z","closed_at":"2026-05-23T20:24:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-l9h","title":"stop persisting non-signal option prints in clickhouse","description":"Why: non-signal option prints are storage noise and should not be persisted by default.\\n\\nWhat: add OPTIONS_PERSIST_SIGNAL_ONLY env flag (default true), gate option_print inserts in ingest-options, add tests for persistence behavior, update env examples, and document one-off cleanup SQL for existing non-signal rows.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T03:02:32Z","created_by":"dirtydishes","updated_at":"2026-05-23T03:06:34Z","started_at":"2026-05-23T03:02:35Z","closed_at":"2026-05-23T03:06:34Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-2cj","title":"Add Forgejo-first agent workflow guidance to AGENTS.md","description":"Why this issue exists and what needs to be done:\\n- The repositorys canonical home is Forgejo at git.deltaisland.io, but AGENTS.md does not currently direct agents to prefer Forgejo-specific workflows.\\n- Update AGENTS.md so agents treat Forgejo as primary and use the fj CLI for pull request workflows.\\n- Keep existing Beads and completion instructions intact while clarifying remote preference and command usage.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-23T02:51:31Z","created_by":"dirtydishes","updated_at":"2026-05-23T02:55:42Z","closed_at":"2026-05-23T02:55:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-6ub","title":"Fix LiveStateManager default hot-head test expectation after recent API changes","description":"bun test v1.3.13 (bf2e2cec) currently fails on the case after the latest pulled changes. The failure appears unrelated to the server-load tuning work and should be investigated separately so targeted validation can pass cleanly again.","status":"open","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-05-22T06:09:44Z","created_by":"dirtydishes","updated_at":"2026-05-22T06:09:44Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-qke","title":"Tune healthchecks and Redis flush cadence to reduce server load","description":"Containerd and dockerd are consuming significant CPU due to frequent Docker healthcheck exec churn across multiple stacks, and the host Islandflow Redis instance is hot from aggressive live-cache rewrite behavior. Tune external stack healthcheck intervals and Islandflow Redis flush cadence to reduce steady-state load while preserving service behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-22T06:06:58Z","created_by":"dirtydishes","updated_at":"2026-05-22T06:11:40Z","started_at":"2026-05-22T06:07:03Z","closed_at":"2026-05-22T06:11:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xc5","title":"One-time bidirectional git remote backfill between github and forgejo","description":"Perform a one-time sync so github and forgejo contain the same branch/tag refs and historical commits, including pre-transition github history and newer forgejo commits. Document exact commands and validation results.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-21T01:25:05Z","created_by":"dirtydishes","updated_at":"2026-05-21T01:26:19Z","started_at":"2026-05-21T01:25:16Z","closed_at":"2026-05-21T01:26:19Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-y7b","title":"Fix false browser fallback in Electron renderer","description":"Why this issue exists and what needs to be done:\\nElectron sessions can briefly or permanently render browser-only fallback copy when runtime detection depends on async desktop AI state loading.\\n\\nImplement a runtime snapshot that is resolved synchronously on the client (shell marker + bridge presence) and kept independent from bridge.ai state fetch/subscribe behavior. Add bounded runtime resync/retry and lifecycle-triggered resync on focus/pageshow so late bridge exposure flips to desktop mode.\\n\\nUpdate desktop-ai tests to cover: runtime marker present before AI state resolves, bridge present with pending/rejected getState, and late runtime availability. Keep preload/IPC contract unchanged unless a verified failure requires it.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-21T00:06:52Z","created_by":"dirtydishes","updated_at":"2026-05-21T00:11:21Z","started_at":"2026-05-21T00:06:55Z","closed_at":"2026-05-21T00:11:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xtg","title":"implement ai alert copilot ux refinements","description":"Implement the AI alert Copilot UX plan: markdown result rendering, reusable task result states, in-session result caching with regenerate, task cancellation through the desktop bridge, tests, and required turn documentation.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T23:30:50Z","created_by":"dirtydishes","updated_at":"2026-05-20T23:37:58Z","started_at":"2026-05-20T23:30:58Z","closed_at":"2026-05-20T23:37:58Z","close_reason":"Implemented markdown Copilot rendering, session result caching, regenerate controls, task cancellation plumbing, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -54,12 +68,16 @@
{"_type":"issue","id":"islandflow-lm6","title":"Clarify repo turn documentation scope","description":"Update AGENTS.md so repository turn documentation clearly uses repo-local docs/turns and impeccable styling, without inheriting global non-repo computer-task styling.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T12:05:07Z","created_by":"dirtydishes","updated_at":"2026-05-19T12:06:12Z","started_at":"2026-05-19T12:05:14Z","closed_at":"2026-05-19T12:06:12Z","close_reason":"Verified AGENTS.md now scopes repo turn docs to docs/turns and makes impeccable the styling authority; added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-6iq","title":"Update README for current project state","description":"Resolve README merge conflicts and document the current project state, including the smart money classification taxonomy, Next.js update, and deployment workflow changes.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:37:24Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:40:01Z","started_at":"2026-05-19T11:37:31Z","closed_at":"2026-05-19T11:40:01Z","close_reason":"README conflict resolved and current project state documented, including smart-money taxonomy, Next.js update, and deployment workflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lib","title":"Upgrade apps/web to Next.js 16.2.6","description":"Upgrade the web app dependency stack to Next.js 16.2.6 with React 19, refresh Bun and mirrored Docker workspace lockfiles, keep runtime behavior unchanged, fix any focused web test fallout, validate the web build and targeted route tests, and document the completed work.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:04:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:31:23Z","started_at":"2026-05-19T11:04:57Z","closed_at":"2026-05-19T11:31:23Z","close_reason":"Upgraded apps/web to Next.js 16.2.6 with React 19, refreshed Bun lockfiles including the Docker workspace mirror, fixed the React 19 nullable ref type issue, and validated the web build, focused tests, Docker workspace sync, and route smoke checks.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-fl5","title":"Decide final public posture for api.flow.deltaisland.io after native cutover","description":"Why this issue exists and what needs to be done:\\n- Native cutover now works end-to-end through Nginx Proxy Manager and the public API hostname now resolves directly to the VPS\\n- The API hostname was left DNS-only in Cloudflare during incident resolution, while the web hostname still uses the Cloudflare proxy\\n- We need to decide whether api.flow.deltaisland.io should remain direct-to-origin or be re-proxied through Cloudflare, then validate TLS, websocket, and operational behavior for the chosen posture","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-18T23:51:21Z","created_by":"dirtydishes","updated_at":"2026-05-18T23:51:21Z","dependencies":[{"issue_id":"islandflow-fl5","depends_on_id":"islandflow-vvw","type":"discovered-from","created_at":"2026-05-18T19:52:32Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8fn","title":"implement alpaca-backed news wire view","description":"Why this issue exists and what needs to be done:\\nAdd an Alpaca-powered live news pipeline, API, storage, and web experience, including a dedicated /news route, Home preview, live fanout, history pagination, ticker resolution, and replay-mode live-only empty states.\\n\\nAcceptance criteria:\\n- normalized NewsStory contract and live channel exist\\n- ingest-news service backfills and streams Alpaca news\\n- API persists, serves, and fans out news\\n- web app exposes /news plus Home preview and drawer\\n- tests cover types, storage, API, and key UI behaviors\\n- turn documentation is added\\n\\nDesign:\\nReuse Islandflow drawer, chips, panes, and terminal styling; keep news live-only in v1 replay mode.\\n\\nNotes:\\nImplement client-side ticker filtering in v1 and expose latest revision only per provider+story_id.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T20:37:13Z","created_by":"dirtydishes","updated_at":"2026-05-18T20:55:11Z","started_at":"2026-05-18T20:37:20Z","closed_at":"2026-05-18T20:55:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-k8i","title":"Fix duplicate alert context import in API entrypoint","description":"Recent alert-context work introduced a duplicate fetchAlertContextByTraceId import in services/api/src/index.ts, which risks breaking TypeScript compilation and API startup. Remove the duplicate import and validate the affected API/web tests.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T13:01:58Z","created_by":"dirtydishes","updated_at":"2026-05-18T13:03:40Z","started_at":"2026-05-18T13:02:02Z","closed_at":"2026-05-18T13:03:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lk9","title":"Fix PR creation workflow after Forgejo migration","description":"## Why\\nCreating pull requests with fails after the repository moved primary collaboration from GitHub to Forgejo. The current workflow still assumes GitHub GraphQL PR creation semantics, which do not work against the Forgejo remote.\\n\\n## What\\nInvestigate the current PR creation path, identify remaining GitHub-specific assumptions, and update the repo workflow/scripts/docs so contributors can reliably publish branches and open PRs in the Forgejo-based setup.\\n\\n## Acceptance Criteria\\n- The repo no longer instructs contributors to use a broken GitHub-specific PR creation path for Forgejo branches\\n- There is a documented and preferably scripted way to create the equivalent review request against Forgejo\\n- Validation demonstrates the new workflow behaves correctly or clearly documents any remaining platform limitation","status":"in_progress","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T10:26:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T10:26:53Z","started_at":"2026-05-18T10:26:53Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-vvw","title":"Stage native public-edge cutover after worker soak","description":"Why this issue exists and what needs to be done:\\n- The native deploy path is now provisioned for worker-first iteration, with checked-in user units, rollback helpers, and edge guardrails\\n- Remaining work is to enable and soak native worker units, validate duplicate-processing behavior, then deliberately cut over the public web/api edge if warranted\\n- Final acceptance should include deciding whether Docker or native becomes the default runtime after operational evidence","notes":"2026-05-18: native infra, native app services, NPM public-edge retargeting, Docker rollback helpers, and Cloudflare/DNS API hostname recovery were implemented and verified. Public checks now pass for flow.deltaisland.io and api.flow.deltaisland.io. Remaining follow-up: decide whether api.flow.deltaisland.io should remain DNS-only or be re-proxied through Cloudflare under islandflow-fl5.","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:32:35Z","created_by":"dirtydishes","updated_at":"2026-05-18T23:52:32Z","started_at":"2026-05-18T23:51:20Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-bsg","title":"Fix public /replay/options proxy regression","description":"Restore correct public routing for GET /replay/options on flow.deltaisland.io. The app currently serves HTML for that API path, which indicates edge/proxy routing drift. Update the live proxy topology or deployment assets as needed, then validate with bun run scripts/check-public-api-routes.ts.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:15:19Z","created_by":"dirtydishes","updated_at":"2026-05-18T07:32:51Z","started_at":"2026-05-18T07:15:24Z","closed_at":"2026-05-18T07:32:51Z","close_reason":"Audited the live VPS and reverse proxy on 2026-05-18: public /replay/options now returns JSON, bun run scripts/check-public-api-routes.ts passes, and the active Nginx Proxy Manager config includes /replay in the API route matcher. No in-repo app code change was required.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-1ei","title":"Make deploy helper remote-aware for Forgejo","description":"Why: scripts/deploy.ts hardcodes git remote name origin for fetch/pull/push and branch verification, but this repository now uses forgejo/github remotes and may not have an origin remote. What: update deploy.ts to resolve the deploy git remote robustly (Forgejo-aware), use it across local prechecks, branch publish, and remote rollout git operations, and keep behavior explicit in output.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T03:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-18T03:22:39Z","started_at":"2026-05-18T03:20:16Z","closed_at":"2026-05-18T03:22:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xod","title":"Add --fast mode to deploy helper","description":"Why: full main deploys rebuild all images and run full verification, which is slow for routine rollouts. What: add a --fast flag to scripts/deploy.ts with explicit behavior that short-circuits slow steps while preserving basic safety checks; update help text/docs for discoverability.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T02:50:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T02:53:41Z","started_at":"2026-05-18T02:50:50Z","closed_at":"2026-05-18T02:53:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-cif","title":"hydrate alert evidence context from clickhouse","description":"Implement alert detail hydration from ClickHouse with a new context endpoint and frontend drawer evidence resolution. Includes storage lookup by alert trace_id/evidence refs, unresolved refs diagnostics, API route GET /flow/alerts/:trace_id/context, terminal evidence hydration + loading states/copy updates, and tests across storage/api/web.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T00:15:55Z","created_by":"dirtydishes","updated_at":"2026-05-18T00:17:38Z","started_at":"2026-05-18T00:16:00Z","closed_at":"2026-05-18T00:17:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9j5","title":"Prepare PR for deploy allowlist cleanup","description":"Why this issue exists and what needs to be done:\\n- Package current deploy allowlist cleanup into a reviewable PR with multiple commits\\n- Add required turn documentation in docs/turns\\n- Run validation and push all artifacts","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T15:44:12Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:53:55Z","started_at":"2026-05-17T15:44:22Z","closed_at":"2026-05-17T15:53:55Z","close_reason":"Packaged deploy allowlist cleanup into multi-commit PR branch with required turn documentation and push workflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-4e9","title":"Polish terminal view","description":"Improve the Islandflow web terminal view with a focused UI polish pass aligned to the product design system.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T15:18:18Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:25:02Z","started_at":"2026-05-17T15:18:21Z","closed_at":"2026-05-17T15:25:02Z","close_reason":"Polished terminal shell styling, responsive Tape actions, and documented the turn.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lyt","title":"Summarize 2026-05-16 git activity for standup","description":"Create a grounded standup summary for yesterday's git activity, anchored to commits, changed files, and any linked PR context if present. Produce the required HTML document in docs/general and complete the beads + git handoff workflow.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:02:57Z","created_by":"dirtydishes","updated_at":"2026-05-17T14:05:37Z","started_at":"2026-05-17T14:03:09Z","closed_at":"2026-05-17T14:05:37Z","close_reason":"Created docs/general standup summary for 2026-05-16 git activity, grounded to commits and changed files, and prepared the repo handoff workflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-sz8","title":"Fix public /replay/options proxy regression","description":"## Summary\nThe new deploy-time public route checker added in commit 1424a27 (\"fix durable options history routing\") currently fails against https://flow.deltaisland.io because GET /replay/options returns HTML instead of JSON.\n\n## Evidence\n- `bun run scripts/check-public-api-routes.ts https://flow.deltaisland.io` fails on `/replay/options?view=signal\u0026after_ts=0\u0026after_seq=0\u0026limit=1` with `returned non-JSON content (text/html; charset=UTF-8)`\n- `services/api/src/index.ts` implements `GET /replay/options`, so the HTML response indicates the request is landing on the web app instead of the API service\n- `deployment/docker/README.md` documents that same-origin proxy mode must include `/replay/*` in the API route matcher\n\n## Minimal Fix\nUpdate the live reverse proxy / edge route matcher for flow.deltaisland.io so `/replay/*` is forwarded to the API host, then rerun `bun run check:public-api-routes`.\n\n## Notes\nThis looks like a production proxy configuration regression rather than an in-repo application bug.","status":"open","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-05-17T13:06:11Z","created_by":"dirtydishes","updated_at":"2026-05-17T13:06:11Z","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -90,6 +108,9 @@
{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"auto-import","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-5rt","title":"Summarize June 2 git activity for standup","description":"Create the daily standup summary in docs/general for 2026-06-02 activity, anchored to yesterday's commits and touched files.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-03T16:30:03Z","created_by":"dirtydishes","updated_at":"2026-06-03T16:31:33Z","started_at":"2026-06-03T16:31:26Z","closed_at":"2026-06-03T16:31:33Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-3f4","title":"Publish May 31 standup git summary","description":"## Summary\nCreate the daily standup HTML summary for 2026-05-31 git activity in docs/general and regenerate any supporting docs index entries.\n\n## Why this matters\nThe team needs a grounded, commit-anchored standup artifact for May 31 repository activity.\n\n## Scope\nInspect May 31 git history, write the summary document in docs/general, update related generated docs metadata if needed, and close out the task.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-01T13:02:21Z","created_by":"dirtydishes","updated_at":"2026-06-01T13:04:45Z","started_at":"2026-06-01T13:02:29Z","closed_at":"2026-06-01T13:04:45Z","close_reason":"Added docs/general standup summary for 2026-05-31 and verified docs index discovery.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-6ak","title":"Clarify turn doc diff rendering instructions","description":"Make AGENTS.md explicit that turn documents should render diffs with the @pierre/diffs/ssr library import instead of attempting to run @pierre/diffs through bunx.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T02:01:59Z","created_by":"dirtydishes","updated_at":"2026-05-30T02:02:27Z","started_at":"2026-05-30T02:02:00Z","closed_at":"2026-05-30T02:02:27Z","close_reason":"Updated AGENTS.md to require @pierre/diffs/ssr rendering, forbid bunx @pierre/diffs attempts, and include a known-good preloadPatchDiff recipe.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-3kn","title":"Summarize 2026-05-28 git activity","description":"Prepare the standup-ready summary of yesterday's git activity, grounded in commits, PRs, and touched files, and store the HTML report in docs/general.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T13:02:25Z","created_by":"dirtydishes","updated_at":"2026-05-29T13:04:23Z","started_at":"2026-05-29T13:02:33Z","closed_at":"2026-05-29T13:04:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-3ys","title":"Expand Forgejo CI beyond the fast validate path","description":"Add follow-on Forgejo CI jobs after the initial baseline is stable. This should cover deferred work such as Docker image builds for deployment/docker, service-container integration tests for NATS/Redis/ClickHouse paths, and any later deploy or release automation that should not block the first fast PR gate.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-24T00:34:09Z","created_by":"dirtydishes","updated_at":"2026-05-24T00:34:09Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-cwr","title":"polish terminal navigation drawer motion","description":"The shared terminal navigation drawer opens and closes abruptly because it mounts only while open and unmounts immediately on dismiss. Add calm, reduced-motion-safe drawer and backdrop transitions so the mobile navigation feels intentional without slowing task flow. Include validation for open and dismiss behavior if the existing drawer interaction coverage is touched.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T23:58:06Z","created_by":"dirtydishes","updated_at":"2026-05-24T00:05:16Z","started_at":"2026-05-23T23:58:17Z","closed_at":"2026-05-24T00:05:16Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -106,4 +127,6 @@
{"_type":"issue","id":"islandflow-zsy","title":"Expose Forgejo SSH on a direct DNS hostname","description":"git.deltaisland.io currently resolves through Cloudflare's proxy, so SSH on port 2222 does not complete even though the Forgejo container is listening on the host. If SSH-based git/beads workflows are desired, add a DNS-only hostname (or adjust the existing record) that points directly at the server for Forgejo SSH.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-17T10:34:06Z","created_by":"delta","updated_at":"2026-05-17T10:34:06Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-38p","title":"Add native deployment unit templates and rollback helpers","description":"The deploy helper now supports --runtime native, but the repo still relies on operator-managed systemd units and manual rollback. Add checked-in native deployment templates or provisioning guidance for the expected units, and consider lightweight rollback/smoke-test helpers once the host-native path is exercised on the real VPS.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:46:42Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-a1m","title":"Publish June 3 standup summary","description":"Why this issue exists and what needs to be done:\\n- Produce the daily standup summary for git activity on 2026-06-03.\\n- Ground every statement in commits and touched files only.\\n- Save the HTML artifact under docs/general and complete the automation handoff workflow.","status":"closed","priority":4,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-04T13:02:04Z","created_by":"dirtydishes","updated_at":"2026-06-04T13:03:43Z","started_at":"2026-06-04T13:03:34Z","closed_at":"2026-06-04T13:03:43Z","close_reason":"Created docs/general/2026-06-04-standup-summary-2026-06-03.html with a commit-grounded summary of June 3 git activity.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-0jb","title":"Publish June 1 standup summary","description":"Why this issue exists and what needs to be done:\\n- Produce the daily standup summary for git activity on 2026-06-01.\\n- Ground every statement in commits and touched files only.\\n- Save the HTML artifact under docs/general and complete the automation handoff workflow.","status":"closed","priority":4,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-02T13:03:01Z","created_by":"dirtydishes","updated_at":"2026-06-02T13:05:51Z","started_at":"2026-06-02T13:03:16Z","closed_at":"2026-06-02T13:05:51Z","close_reason":"Created docs/general/2026-06-02-standup-summary-2026-06-01.html with a commit-grounded June 1 standup summary.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-1tu","title":"Publish 2026-05-24 standup summary","description":"Why this issue exists and what needs to be done\n\nCreate the daily standup summary for git activity on 2026-05-24, grounded in commits and touched files, then store the HTML report in docs/general.","status":"closed","priority":4,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-25T13:02:56Z","created_by":"dirtydishes","updated_at":"2026-05-25T13:04:31Z","closed_at":"2026-05-25T13:04:31Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -30,17 +30,27 @@ jobs:
apt-get install --yes --no-install-recommends curl unzip
rm -rf /var/lib/apt/lists/*
curl -fsSL https://bun.sh/install | bash
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
~/.bun/bin/bun --version
- name: Install dependencies
run: ~/.bun/bin/bun install --frozen-lockfile
- name: Check formatting
run: ~/.bun/bin/bun run fmt:check
- name: Run lint
run: ~/.bun/bin/bun run lint
- name: Run typecheck
run: ~/.bun/bin/bun run typecheck
- name: Run tests
run: ~/.bun/bin/bun test
- name: Check public API routes
run: ~/.bun/bin/bun run check:public-api-routes
- name: Check Docker workspace snapshot
run: ~/.bun/bin/bun run check:docker-workspace

120
AGENTS.md
View file

@ -86,22 +86,15 @@ Agent expectations:
## Required Turn Documentation
At the end of every completed implementation task, before final handoff, create a user-readable HTML document describing the work.
Follow the global turn-documentation rules in `~/.codex/AGENTS.md` for repository implementation tasks, plan documents, and non-repo computer tasks.
This documentation is mandatory whenever code, configuration, tests, or project files were changed.
For this repository, the repo-specific requirements are:
### Precedence and classification
Use this decision order before creating a turn document:
1. Check the minor/trivial exemption checklist below first.
2. If the task clearly matches an exemption, do not create a turn document.
3. If the task is a clearly substantive implementation change, create a turn document.
4. If classification is ambiguous or mixed, ask the user before creating a turn document.
The minor/trivial exemptions override the general mandatory turn-document rule.
For diff content in turn documentation (including "Code diffs" and "Relevant Diff Snippets"), use `@pierre/diffs` output by default. If `@pierre/diffs` is unavailable because of a real tool or blocking error, use a clearly labeled plain diff/code block fallback and note why.
- Save repository implementation turn documents in `docs/turns/`.
- Use the `impeccable` skill to structure and style repository implementation turn documents when available.
- Render "Relevant Diff Snippets" with `@pierre/diffs/ssr`; use https://diffs.com/docs as the SSR reference.
- For minor updates to a previous change, update the existing turn document instead of creating a new one.
- The minor/trivial exemptions below override the general documentation requirement for this repository.
### No turn document for minor/trivial checklist matches
@ -116,69 +109,9 @@ Do not create a turn document when the change is minor/trivial and cleanly match
If a change does not cleanly fit either exempt or substantive buckets, ask the user before creating a turn document.
### When making a minor update to a previous change, update the existing documentation instead of creating a new file. Use the following format:
**"New Changes as of {time and date at which the change was made}"**
- **Summary of changes**
- **Why this change was made**
- **Code diffs** (use `@pierre/diffs` output by default; if unavailable, include a clearly labeled plain diff/code block and note why)
- **Related issues or PRs**
Additionally, add a note to each section explaining why the changes were made.
### Location
Save the document in:
```text
docs/turns/
```
Use a clear timestamped filename:
```text
docs/turns/YYYY-MM-DD-short-task-name.html
```
Example:
```text
docs/turns/2026-05-14-add-market-replay-controls.html
```
### Format
Use the `impeccable` skill to structure and style the document as clean, readable HTML.
For this repository, `impeccable` is the styling and layout authority for turn documents when available. Do not apply global non-repo computer-task house styling to repository turn documents.
If the `impeccable` skill is unavailable or blocked by an actual tool/file error, still create a well-structured standalone HTML file with:
- A concise summary at the top
- A detailed explanation of what changed
- Relevant context or background
- Specific code snippets or examples when helpful
- Issues, limitations, tradeoffs, or mitigations
- Validation performed, including tests, builds, linters, or manual checks
- Any remaining follow-up work, with corresponding Beads issue IDs when applicable
### Required Sections
Each turn document must include these sections:
1. **Summary**
2. **Changes Made**
3. **Context**
4. **Important Implementation Details**
5. **Relevant Diff Snippets** (render with `@pierre/diffs` output by default; if unavailable, include a clearly labeled plain diff/code block and note why)
6. **Expected Impact for End-Users**
7. **Validation**
8. **Issues, Limitations, and Mitigations**
9. **Follow-up Work**
### Completion Rule
A task that requires a turn document is not complete until:
For repository implementation tasks that require turn documentation, the task is not complete until:
1. The Beads workflow is updated
2. The turn document is created in `docs/turns`
@ -187,40 +120,3 @@ A task that requires a turn document is not complete until:
5. `bd dolt push` succeeds
6. `git push forgejo <branch>` succeeds
7. `git status` shows the branch is up to date with `forgejo/<branch>`
For tasks that do require turn documentation, the document may be brief when scope is small, but it must clearly explain what changed and how it was validated.
## Plan Mode Documentation
When working in plan mode, do not modify implementation files.
At the end of plan mode, provide a concise summary of the plan and ask the user whether they want to proceed with implementation.
If the user asks to save the plan, create a user-readable HTML plan document in:
```text
docs/plans/
```
Use a clear timestamped filename:
```text
docs/plans/YYYY-MM-DD-short-plan-name.html
```
The plan document should be labeled clearly as a plan and should include:
1. **Plan Summary**
2. **Goals**
3. **Proposed Changes**
4. **Relevant Context**
5. **Implementation Steps**
6. **Risks, Limitations, and Mitigations**
7. **Open Questions**
Always do the following when you finish a task, finish the beads workflow and and make a commit:
- Document the changes in a user-readable format
- Use the impeccable skill to structure the document as HTML
- Create a clear, concise summary of the changes at the top, followed by a detailed description of the changes, including any relevant context or background as well as specific code snippets or examples.
- Note any relevant issues or limitations that were addressed or mitigated by the changes.
- The HTML file should be stored in the `docs/turns` directory. It should include the current date and time, as well as a brief explanation of changes. e.g. docs/turns/YYYY-MM-DD-{description}.html

View file

@ -9,11 +9,8 @@ export async function GET(): Promise<Response> {
}
export async function PUT(req: Request): Promise<Response> {
return proxySyntheticAdminRequest(
"/admin/synthetic/control",
{
return proxySyntheticAdminRequest("/admin/synthetic/control", {
method: "PUT",
body: await req.text()
}
);
});
}

View file

@ -1,8 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import {
getSyntheticAdminProxyConfig,
isSyntheticAdminFeatureEnabled
} from "./shared";
import { getSyntheticAdminProxyConfig, isSyntheticAdminFeatureEnabled } from "./shared";
const originalFetch = globalThis.fetch;

View file

@ -18,25 +18,29 @@ const variants: Record<
> = {
mock1: {
title: "Command Deck",
premise: "Closest to the reference: left navigation, ticker ribbon, dense evidence panes, replay rail.",
premise:
"Closest to the reference: left navigation, ticker ribbon, dense evidence panes, replay rail.",
mode: "Dense ops",
layout: "classic"
},
mock2: {
title: "Investigation Stack",
premise: "A calmer analyst layout with the selected symbol story in the center and context wrapped around it.",
premise:
"A calmer analyst layout with the selected symbol story in the center and context wrapped around it.",
mode: "Forensic",
layout: "focus"
},
mock3: {
title: "Signal Wall",
premise: "Prioritizes alert triage and cross-symbol scanning before a user drills into price action.",
premise:
"Prioritizes alert triage and cross-symbol scanning before a user drills into price action.",
mode: "Triage",
layout: "signals"
},
mock4: {
title: "Replay Lab",
premise: "A replay-first structure with timeline, event tape, and causality context always visible.",
premise:
"A replay-first structure with timeline, event tape, and causality context always visible.",
mode: "Replay",
layout: "replay"
}
@ -93,7 +97,10 @@ export function DashboardMock({ variant }: DashboardMockProps) {
const config = variants[variant];
return (
<section className={`mock-terminal mock-terminal-${config.layout}`} aria-labelledby="mock-title">
<section
className={`mock-terminal mock-terminal-${config.layout}`}
aria-labelledby="mock-title"
>
<MockHeader config={config} active={variant} />
<TickerRail />
{variant === "mock1" ? <ClassicLayout /> : null}
@ -277,7 +284,11 @@ function OptionTape({ condensed = false }: { condensed?: boolean }) {
function ChartPanel({ compact = false }: { compact?: boolean }) {
return (
<Panel title="AAPL | Price & Flow" meta="1m / 5m / 15m" className={compact ? "mock-chart is-compact" : "mock-chart"}>
<Panel
title="AAPL | Price & Flow"
meta="1m / 5m / 15m"
className={compact ? "mock-chart is-compact" : "mock-chart"}
>
<div className="mock-chart-meta">
<strong>194.88</strong>
<span className="mock-move is-up">+2.34 (+1.22%)</span>
@ -306,16 +317,24 @@ function ChartPanel({ compact = false }: { compact?: boolean }) {
function SignalPanel({ hero = false }: { hero?: boolean }) {
return (
<Panel title="Signals & Alerts" meta="All / Signals / System" className={hero ? "mock-signals is-hero" : "mock-signals"}>
<Panel
title="Signals & Alerts"
meta="All / Signals / System"
className={hero ? "mock-signals is-hero" : "mock-signals"}
>
<div className="mock-signal-list">
{signals.map(([time, title, symbol, value, tag]) => (
<article className="mock-signal-item" key={`${time}-${title}`}>
<time>{time}</time>
<div>
<strong>{title}</strong>
<span>{symbol} / {value}</span>
<span>
{symbol} / {value}
</span>
</div>
<span className={`mock-pill ${tag === "Bearish" ? "is-bearish" : tag === "News" ? "is-news" : "is-bullish"}`}>
<span
className={`mock-pill ${tag === "Bearish" ? "is-bearish" : tag === "News" ? "is-news" : "is-bullish"}`}
>
{tag}
</span>
</article>
@ -332,7 +351,9 @@ function FeedHealth() {
{feedHealth.map(([feed, status, lag, rate]) => (
<div className="mock-table-row" key={feed}>
<span>{feed}</span>
<span className={`mock-pill ${status === "Degraded" ? "is-warning" : "is-bullish"}`}>{status}</span>
<span className={`mock-pill ${status === "Degraded" ? "is-warning" : "is-bullish"}`}>
{status}
</span>
<span>{lag}</span>
<span>{rate}/s</span>
</div>
@ -350,7 +371,9 @@ function DarkFlow() {
<div className="mock-table-row" key={`${time}-${side}-${size}`}>
<span>{time}</span>
<strong>{symbol}</strong>
<span className={`mock-pill ${side === "Sell" ? "is-bearish" : "is-bullish"}`}>{side}</span>
<span className={`mock-pill ${side === "Sell" ? "is-bearish" : "is-bullish"}`}>
{side}
</span>
<span>{size}</span>
<span>{notional}</span>
<span>{type}</span>
@ -402,7 +425,11 @@ function EventContext() {
function ReplayRail({ compact = false }: { compact?: boolean }) {
return (
<Panel title="Replay" meta="May 16, 2024" className={compact ? "mock-replay is-compact" : "mock-replay"}>
<Panel
title="Replay"
meta="May 16, 2024"
className={compact ? "mock-replay is-compact" : "mock-replay"}
>
<div className="mock-replay-controls">
<button type="button">Prev</button>
<button type="button">Pause</button>
@ -430,8 +457,9 @@ function SymbolBrief() {
<span className="mock-move is-up">+1.22%</span>
</div>
<p>
Dark sweep pressure aligns with short-window momentum and a fresh news catalyst. Context confidence is high, but
the largest block remains off-exchange and should be checked against next print behavior.
Dark sweep pressure aligns with short-window momentum and a fresh news catalyst. Context
confidence is high, but the largest block remains off-exchange and should be checked against
next print behavior.
</p>
<div className="mock-brief-tags">
<span className="mock-pill is-bullish">Bullish</span>
@ -444,7 +472,12 @@ function SymbolBrief() {
function Sparkline({ direction }: { direction: string }) {
return (
<svg className="mock-sparkline" viewBox="0 0 96 28" role="img" aria-label={`${direction} sparkline`}>
<svg
className="mock-sparkline"
viewBox="0 0 96 28"
role="img"
aria-label={`${direction} sparkline`}
>
<polyline
fill="none"
points={

View file

@ -161,7 +161,10 @@ input {
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.76rem;
transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
transition:
border-color 0.15s ease,
background-color 0.15s ease,
color 0.15s ease;
}
.terminal-nav-link:hover {
@ -800,8 +803,7 @@ h3 {
border: 1px solid var(--border-strong);
border-radius: 8px;
background:
linear-gradient(135deg, oklch(0.78 0.12 74 / 0.7), oklch(0.28 0.035 250)),
var(--accent-soft);
linear-gradient(135deg, oklch(0.78 0.12 74 / 0.7), oklch(0.28 0.035 250)), var(--accent-soft);
}
.command-deck-kicker,
@ -1608,19 +1610,31 @@ h3 {
.data-table-row-classified {
background:
linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.012 + var(--classifier-intensity, 0) * 0.06)), transparent 62%),
linear-gradient(
90deg,
rgba(
var(--classifier-rgb, 192, 200, 210),
calc(0.012 + var(--classifier-intensity, 0) * 0.06)
),
transparent 62%
),
oklch(0.98 0.008 250 / 0.008);
}
.data-table-row-classified:hover,
.data-table-row-classified:focus-visible {
background:
linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.02 + var(--classifier-intensity, 0) * 0.1)), transparent 68%),
linear-gradient(
90deg,
rgba(var(--classifier-rgb, 192, 200, 210), calc(0.02 + var(--classifier-intensity, 0) * 0.1)),
transparent 68%
),
oklch(0.78 0.12 74 / 0.035);
}
.data-table-row-classified.is-classified {
box-shadow: inset 0 0 0 1px rgba(var(--classifier-rgb), calc(0.16 + var(--classifier-intensity) * 0.12));
box-shadow: inset 0 0 0 1px
rgba(var(--classifier-rgb), calc(0.16 + var(--classifier-intensity) * 0.12));
}
.data-table-row-warn,
@ -1641,32 +1655,56 @@ h3 {
.data-table-options .data-table-head,
.data-table-options .data-table-row {
grid-template-columns: minmax(72px, 0.8fr) minmax(50px, 0.55fr) minmax(64px, 0.7fr) minmax(58px, 0.6fr) minmax(34px, 0.35fr) minmax(62px, 0.65fr) minmax(104px, 1fr) minmax(54px, 0.55fr) minmax(66px, 0.7fr) minmax(48px, 0.5fr) minmax(42px, 0.45fr) minmax(92px, 0.9fr);
grid-template-columns: minmax(72px, 0.8fr) minmax(50px, 0.55fr) minmax(64px, 0.7fr) minmax(
58px,
0.6fr
) minmax(34px, 0.35fr) minmax(62px, 0.65fr) minmax(104px, 1fr) minmax(54px, 0.55fr) minmax(
66px,
0.7fr
) minmax(48px, 0.5fr) minmax(42px, 0.45fr) minmax(92px, 0.9fr);
}
.data-table-equities .data-table-head,
.data-table-equities .data-table-row {
grid-template-columns: minmax(76px, 0.9fr) minmax(70px, 0.8fr) minmax(76px, 0.8fr) minmax(70px, 0.75fr) minmax(80px, 0.8fr) minmax(76px, 0.75fr);
grid-template-columns: minmax(76px, 0.9fr) minmax(70px, 0.8fr) minmax(76px, 0.8fr) minmax(
70px,
0.75fr
) minmax(80px, 0.8fr) minmax(76px, 0.75fr);
}
.data-table-flow .data-table-head,
.data-table-flow .data-table-row {
grid-template-columns: minmax(148px, 1.1fr) minmax(180px, 1.4fr) minmax(62px, 0.45fr) minmax(70px, 0.5fr) minmax(88px, 0.7fr) minmax(74px, 0.55fr) minmax(132px, 1fr) minmax(110px, 0.8fr) minmax(210px, 1.6fr);
grid-template-columns: minmax(148px, 1.1fr) minmax(180px, 1.4fr) minmax(62px, 0.45fr) minmax(
70px,
0.5fr
) minmax(88px, 0.7fr) minmax(74px, 0.55fr) minmax(132px, 1fr) minmax(110px, 0.8fr) minmax(
210px,
1.6fr
);
}
.data-table-alerts .data-table-head,
.data-table-alerts .data-table-row {
grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.4fr) minmax(52px, 0.45fr) minmax(58px, 0.45fr) minmax(52px, 0.4fr) minmax(66px, 0.55fr) minmax(260px, 2fr);
grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.4fr) minmax(52px, 0.45fr) minmax(
58px,
0.45fr
) minmax(52px, 0.4fr) minmax(66px, 0.55fr) minmax(260px, 2fr);
}
.data-table-classifier .data-table-head,
.data-table-classifier .data-table-row {
grid-template-columns: minmax(76px, 0.75fr) minmax(180px, 1.45fr) minmax(70px, 0.6fr) minmax(74px, 0.65fr) minmax(300px, 2.2fr);
grid-template-columns: minmax(76px, 0.75fr) minmax(180px, 1.45fr) minmax(70px, 0.6fr) minmax(
74px,
0.65fr
) minmax(300px, 2.2fr);
}
.data-table-dark .data-table-head,
.data-table-dark .data-table-row {
grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.35fr) minmax(76px, 0.65fr) minmax(74px, 0.65fr) minmax(74px, 0.65fr) minmax(260px, 2fr);
grid-template-columns: minmax(76px, 0.75fr) minmax(170px, 1.35fr) minmax(76px, 0.65fr) minmax(
74px,
0.65fr
) minmax(74px, 0.65fr) minmax(260px, 2fr);
}
.data-table-cell {
@ -1698,7 +1736,13 @@ h3 {
.options-table-head,
.options-table-row {
display: grid;
grid-template-columns: minmax(72px, 0.8fr) minmax(50px, 0.55fr) minmax(64px, 0.7fr) minmax(58px, 0.6fr) minmax(34px, 0.35fr) minmax(62px, 0.65fr) minmax(104px, 1fr) minmax(54px, 0.55fr) minmax(66px, 0.7fr) minmax(48px, 0.5fr) minmax(42px, 0.45fr) minmax(92px, 0.9fr);
grid-template-columns: minmax(72px, 0.8fr) minmax(50px, 0.55fr) minmax(64px, 0.7fr) minmax(
58px,
0.6fr
) minmax(34px, 0.35fr) minmax(62px, 0.65fr) minmax(104px, 1fr) minmax(54px, 0.55fr) minmax(
66px,
0.7fr
) minmax(48px, 0.5fr) minmax(42px, 0.45fr) minmax(92px, 0.9fr);
align-items: center;
column-gap: 8px;
}
@ -1729,7 +1773,14 @@ h3 {
border: 0;
border-bottom: 1px solid oklch(0.72 0.012 250 / 0.08);
background:
linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.012 + var(--classifier-intensity, 0) * 0.06)), transparent 62%),
linear-gradient(
90deg,
rgba(
var(--classifier-rgb, 192, 200, 210),
calc(0.012 + var(--classifier-intensity, 0) * 0.06)
),
transparent 62%
),
oklch(0.98 0.008 250 / 0.012);
color: inherit;
font: inherit;
@ -1740,13 +1791,18 @@ h3 {
.options-table-row:focus-visible {
outline: none;
background:
linear-gradient(90deg, rgba(var(--classifier-rgb, 192, 200, 210), calc(0.02 + var(--classifier-intensity, 0) * 0.1)), transparent 68%),
linear-gradient(
90deg,
rgba(var(--classifier-rgb, 192, 200, 210), calc(0.02 + var(--classifier-intensity, 0) * 0.1)),
transparent 68%
),
oklch(0.78 0.12 74 / 0.03);
}
.options-table-row.is-classified {
cursor: pointer;
box-shadow: inset 0 0 0 1px rgba(var(--classifier-rgb), calc(0.16 + var(--classifier-intensity) * 0.12));
box-shadow: inset 0 0 0 1px
rgba(var(--classifier-rgb), calc(0.16 + var(--classifier-intensity) * 0.12));
}
.options-table-row > span {
@ -1761,17 +1817,39 @@ h3 {
font-variant-numeric: tabular-nums;
}
.classifier-green { --classifier-rgb: 37, 193, 122; }
.classifier-red { --classifier-rgb: 255, 107, 95; }
.classifier-amber { --classifier-rgb: 245, 166, 35; }
.classifier-copper { --classifier-rgb: 198, 122, 75; }
.classifier-blue { --classifier-rgb: 77, 163, 255; }
.classifier-teal { --classifier-rgb: 64, 210, 190; }
.classifier-yellowgreen { --classifier-rgb: 174, 210, 78; }
.classifier-violet { --classifier-rgb: 170, 130, 255; }
.classifier-cyan { --classifier-rgb: 94, 214, 255; }
.classifier-magenta { --classifier-rgb: 255, 92, 205; }
.classifier-neutral { --classifier-rgb: 192, 200, 210; }
.classifier-green {
--classifier-rgb: 37, 193, 122;
}
.classifier-red {
--classifier-rgb: 255, 107, 95;
}
.classifier-amber {
--classifier-rgb: 245, 166, 35;
}
.classifier-copper {
--classifier-rgb: 198, 122, 75;
}
.classifier-blue {
--classifier-rgb: 77, 163, 255;
}
.classifier-teal {
--classifier-rgb: 64, 210, 190;
}
.classifier-yellowgreen {
--classifier-rgb: 174, 210, 78;
}
.classifier-violet {
--classifier-rgb: 170, 130, 255;
}
.classifier-cyan {
--classifier-rgb: 94, 214, 255;
}
.classifier-magenta {
--classifier-rgb: 255, 92, 205;
}
.classifier-neutral {
--classifier-rgb: 192, 200, 210;
}
.contract,
.drawer-row-title {
@ -1921,7 +1999,9 @@ h3 {
opacity: 0;
pointer-events: none;
transform: translateY(8px);
transition: opacity 0.15s ease, transform 0.15s ease;
transition:
opacity 0.15s ease,
transform 0.15s ease;
z-index: 5;
}
@ -2047,7 +2127,10 @@ h3 {
color: var(--text-dim);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
z-index: 45;
transition: border-color 0.16s ease, background-color 0.16s ease, color 0.16s ease;
transition:
border-color 0.16s ease,
background-color 0.16s ease,
color 0.16s ease;
}
.synthetic-control-gear:hover,
@ -2213,7 +2296,9 @@ h3 {
background: oklch(0.18 0.012 250 / 0.6);
color: var(--text);
text-align: left;
transition: border-color 150ms ease, background 150ms ease;
transition:
border-color 150ms ease,
background 150ms ease;
}
.news-row:hover {
@ -2520,7 +2605,11 @@ h3 {
@media (max-width: 720px) {
.terminal-shell {
background-size: 24px 24px, 24px 24px, 100% 100%, auto;
background-size:
24px 24px,
24px 24px,
100% 100%,
auto;
}
.terminal-nav-drawer {
@ -2877,9 +2966,7 @@ h3 {
width: 34px;
height: 34px;
border-radius: 9px;
background:
linear-gradient(135deg, oklch(0.68 0.14 246), oklch(0.68 0.12 164)),
var(--blue-soft);
background: linear-gradient(135deg, oklch(0.68 0.14 246), oklch(0.68 0.12 164)), var(--blue-soft);
box-shadow: inset 0 0 0 1px oklch(0.94 0.02 240 / 0.24);
}

View file

@ -4,7 +4,8 @@ const redirect = mock((path: string) => {
throw new Error(`NEXT_REDIRECT:${path}`);
});
mock.module("next/navigation", () => ({ redirect }));
mock.module("next/navigation", () => ({ default: { redirect }, redirect }));
mock.module("next/navigation.js", () => ({ default: { redirect }, redirect }));
describe("legacy page redirects", () => {
beforeEach(() => {

View file

@ -1,6 +1,36 @@
import { describe, expect, it } from "bun:test";
import { describe, expect, it, mock } from "bun:test";
import { getSubscriptionKey as getLiveSubscriptionKey } from "@islandflow/types";
import {
const redirect = mock((path: string) => {
throw new Error(`NEXT_REDIRECT:${path}`);
});
const nextNavigationMock = {
default: {
redirect,
usePathname: () => "/options"
},
redirect,
usePathname: () => "/options"
};
const nextNavigationResolved = import.meta.resolve("next/navigation");
const nextNavigationJsResolved = import.meta.resolve("next/navigation.js");
mock.module("next/navigation", () => ({
...nextNavigationMock
}));
mock.module("next/navigation.js", () => ({
...nextNavigationMock
}));
mock.module(nextNavigationResolved, () => ({
...nextNavigationMock
}));
mock.module(nextNavigationJsResolved, () => ({
...nextNavigationMock
}));
const {
NAV_ITEMS,
appendHistoryTail,
buildAlertContextPath,
@ -49,7 +79,7 @@ import {
resolveAlertFlowPacket,
statusLabel,
toggleFilterValue
} from "./terminal";
} = await import("./terminal");
const makeItem = (traceId: string, seq: number, ts: number) => ({
trace_id: traceId,
@ -281,12 +311,16 @@ describe("live manifest", () => {
});
it("includes news subscriptions on home and /news", () => {
expect(getLiveManifest("/", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toContain(
"news"
);
expect(getLiveManifest("/news", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toEqual([
"news"
]);
expect(
getLiveManifest("/", "SPY", 60000, buildDefaultFlowFilters()).map(
(subscription) => subscription.channel
)
).toContain("news");
expect(
getLiveManifest("/news", "SPY", 60000, buildDefaultFlowFilters()).map(
(subscription) => subscription.channel
)
).toEqual(["news"]);
});
it("scopes /charts subscriptions to chart channels only", () => {
@ -490,12 +524,36 @@ describe("route feature map", () => {
describe("fixed tape virtualization config", () => {
it("uses expected fixed row heights and overscan by table", () => {
expect(getTapeVirtualConfig("options")).toEqual({ rowHeight: 36, overscan: 44, debugLabel: "options" });
expect(getTapeVirtualConfig("equities")).toEqual({ rowHeight: 36, overscan: 36, debugLabel: "equities" });
expect(getTapeVirtualConfig("flow")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "flow" });
expect(getTapeVirtualConfig("alerts")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "alerts" });
expect(getTapeVirtualConfig("classifier")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "classifier" });
expect(getTapeVirtualConfig("dark")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "dark" });
expect(getTapeVirtualConfig("options")).toEqual({
rowHeight: 36,
overscan: 44,
debugLabel: "options"
});
expect(getTapeVirtualConfig("equities")).toEqual({
rowHeight: 36,
overscan: 36,
debugLabel: "equities"
});
expect(getTapeVirtualConfig("flow")).toEqual({
rowHeight: 44,
overscan: 24,
debugLabel: "flow"
});
expect(getTapeVirtualConfig("alerts")).toEqual({
rowHeight: 44,
overscan: 24,
debugLabel: "alerts"
});
expect(getTapeVirtualConfig("classifier")).toEqual({
rowHeight: 44,
overscan: 24,
debugLabel: "classifier"
});
expect(getTapeVirtualConfig("dark")).toEqual({
rowHeight: 44,
overscan: 24,
debugLabel: "dark"
});
});
});
@ -682,7 +740,11 @@ describe("live tape history helpers", () => {
});
it("promotes hot-window overflow into the history tail", () => {
const currentHot = [makeItem("hot-3", 3, 300), makeItem("hot-2", 2, 200), makeItem("hot-1", 1, 100)];
const currentHot = [
makeItem("hot-3", 3, 300),
makeItem("hot-2", 2, 200),
makeItem("hot-1", 1, 100)
];
const incoming = [makeItem("hot-4", 4, 400)];
const { kept, evicted } = mergeNewestWithOverflow(incoming, currentHot, 3);
@ -697,7 +759,11 @@ describe("live tape history helpers", () => {
let history: Array<ReturnType<typeof makeItem>> = [];
for (let seq = 1; seq <= 5; seq += 1) {
const { kept, evicted } = mergeNewestWithOverflow([makeItem(`row-${seq}`, seq, seq * 100)], hot, 2);
const { kept, evicted } = mergeNewestWithOverflow(
[makeItem(`row-${seq}`, seq, seq * 100)],
hot,
2
);
hot = kept;
history = appendHistoryTail(history, evicted, hot, 5000);
}
@ -732,13 +798,24 @@ describe("live tape history helpers", () => {
});
it("dedupes the seam between promoted overflow and fetched history", () => {
const currentHot = [makeItem("hot-3", 3, 300), makeItem("hot-2", 2, 200), makeItem("hot-1", 1, 100)];
const currentHot = [
makeItem("hot-3", 3, 300),
makeItem("hot-2", 2, 200),
makeItem("hot-1", 1, 100)
];
const { kept, evicted } = mergeNewestWithOverflow([makeItem("hot-4", 4, 400)], currentHot, 3);
const promoted = appendHistoryTail([], evicted, kept, 5000);
const merged = appendHistoryTail(promoted, [makeItem("hot-1", 1, 100), makeItem("older", 0, 50)], kept, 5000);
const merged = appendHistoryTail(
promoted,
[makeItem("hot-1", 1, 100), makeItem("older", 0, 50)],
kept,
5000
);
expect(merged.map((item) => item.trace_id)).toEqual(["hot-1", "older"]);
expect(new Set([...kept, ...merged].map((item) => item.trace_id)).size).toBe(kept.length + merged.length);
expect(new Set([...kept, ...merged].map((item) => item.trace_id)).size).toBe(
kept.length + merged.length
);
});
it("trims the history tail to the soft cap", () => {
@ -791,10 +868,9 @@ describe("live tape history helpers", () => {
makeItem("hist-2", 2, 200)
];
expect(mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)).toEqual([
"hist-3",
"hist-2"
]);
expect(
mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)
).toEqual(["hist-3", "hist-2"]);
});
it("appends truly older lazy-loaded rows to the held history tail", () => {
@ -807,12 +883,9 @@ describe("live tape history helpers", () => {
makeItem("older-0", 0, 50)
];
expect(mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)).toEqual([
"hist-3",
"hist-2",
"older-1",
"older-0"
]);
expect(
mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)
).toEqual(["hist-3", "hist-2", "older-1", "older-0"]);
});
it("resyncs buffered live history by replacing the held segment after resume", () => {
@ -825,7 +898,12 @@ describe("live tape history helpers", () => {
const resynced = appendHistoryTail([], [makeItem("overflow-newer", 6, 600), ...held], [], 0);
expect(held.map((item) => item.trace_id)).toEqual(["hist-3", "hist-2", "older-1"]);
expect(resynced.map((item) => item.trace_id)).toEqual(["overflow-newer", "hist-3", "hist-2", "older-1"]);
expect(resynced.map((item) => item.trace_id)).toEqual([
"overflow-newer",
"hist-3",
"hist-2",
"older-1"
]);
});
});
@ -905,9 +983,21 @@ describe("classifier row decoration helpers", () => {
it("selects primary hits by confidence, source timestamp, then seq", () => {
const hit = selectPrimaryClassifierHit([
{ ...makeAlert({ classifier_id: "old", confidence: 0.9, source_ts: 1_000, seq: 1 }), direction: "bullish", explanations: [] },
{ ...makeAlert({ classifier_id: "new", confidence: 0.9, source_ts: 2_000, seq: 1 }), direction: "bullish", explanations: [] },
{ ...makeAlert({ classifier_id: "low", confidence: 0.5, source_ts: 3_000, seq: 9 }), direction: "bullish", explanations: [] }
{
...makeAlert({ classifier_id: "old", confidence: 0.9, source_ts: 1_000, seq: 1 }),
direction: "bullish",
explanations: []
},
{
...makeAlert({ classifier_id: "new", confidence: 0.9, source_ts: 2_000, seq: 1 }),
direction: "bullish",
explanations: []
},
{
...makeAlert({ classifier_id: "low", confidence: 0.5, source_ts: 3_000, seq: 9 }),
direction: "bullish",
explanations: []
}
]);
expect(hit?.classifier_id).toBe("new");
@ -980,9 +1070,9 @@ describe("signals helpers", () => {
)
).toBe("bearish");
expect(deriveAlertDirection(makeAlert({ hits: [{ direction: "weird", confidence: 0.4 }] }))).toBe(
"neutral"
);
expect(
deriveAlertDirection(makeAlert({ hits: [{ direction: "weird", confidence: 0.4 }] }))
).toBe("neutral");
expect(deriveAlertDirection(makeAlert({ hits: [] }))).toBe("neutral");
});

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"incremental": true,
"noEmit": true,
"allowJs": true,
@ -24,8 +20,5 @@
".next/types/**/*.ts",
".next-dev/types/**/*.ts"
],
"exclude": [
"node_modules",
"scripts"
]
"exclude": ["node_modules", "scripts"]
}

93
biome.json Normal file
View file

@ -0,0 +1,93 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": [
"*.json",
"*.ts",
".forgejo/workflows/*.yml",
"apps/**",
"deployment/docker/workspace-root/package.json",
"packages/**",
"scripts/**",
"services/**",
"!**/node_modules",
"!**/.next",
"!**/dist",
"!**/out",
"!**/coverage",
"!apps/web/tsconfig.tsbuildinfo"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useAriaPropsSupportedByRole": "off",
"useFocusableInteractive": "off",
"useSemanticElements": "off"
},
"complexity": {
"noImportantStyles": "off",
"noUselessContinue": "off",
"noUselessSwitchCase": "off",
"noUselessUndefinedInitialization": "off",
"useOptionalChain": "off"
},
"correctness": {
"useExhaustiveDependencies": "off",
"noUnusedFunctionParameters": "off",
"noUnusedImports": "off",
"noUnusedVariables": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noControlCharactersInRegex": "off",
"noExplicitAny": "off",
"noAssignInExpressions": "off",
"noShorthandPropertyOverrides": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"style": {
"noDescendingSpecificity": "off",
"noNonNullAssertion": "off",
"useExponentiationOperator": "off",
"useImportType": "off",
"useTemplate": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "none"
}
},
"json": {
"parser": {
"allowComments": true,
"allowTrailingCommas": true
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

View file

@ -8,6 +8,7 @@
"@pierre/diffs": "^1.2.2",
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@types/bun": "^1.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3",
@ -172,11 +173,30 @@
},
},
"overrides": {
"@electron/node-gyp": "^10.2.0-electron.2",
"postcss": "^8.5.15",
"tar": "^7.5.15",
"tmp": "^0.2.5",
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
"@clickhouse/client": ["@clickhouse/client@0.2.10", "", { "dependencies": { "@clickhouse/client-common": "0.2.10" } }, "sha512-ZwBgzjEAFN/ogS0ym5KHVbR7Hx/oYCX01qGp2baEyfN2HM73kf/7Vp3GvMHWRy+zUXISONEtFv7UTViOXnmFrg=="],
"@clickhouse/client-common": ["@clickhouse/client-common@0.2.10", "", {}, "sha512-BvTY0IXS96y9RUeNCpKL4HUzHmY80L0lDcGN0lmUD6zjOqYMn78+xyHYJ/AIAX7JQsc+/KwFt2soZutQTKxoGQ=="],
@ -213,7 +233,7 @@
"@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
"@electron/node-gyp": ["@electron/node-gyp@10.2.0-electron.2", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OhO6fwqpetMO1vWI3+J8mb3a4s4A405tgKoUCJsgd4nyQDdFh0VvZm+gj/Cc70iRLQoIYUfSaAgYSVwmLsQHig=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],

View file

@ -8,6 +8,7 @@
"@pierre/diffs": "^1.2.2",
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@types/bun": "^1.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3",
@ -172,11 +173,30 @@
},
},
"overrides": {
"@electron/node-gyp": "^10.2.0-electron.2",
"postcss": "^8.5.15",
"tar": "^7.5.15",
"tmp": "^0.2.5",
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
"@clickhouse/client": ["@clickhouse/client@0.2.10", "", { "dependencies": { "@clickhouse/client-common": "0.2.10" } }, "sha512-ZwBgzjEAFN/ogS0ym5KHVbR7Hx/oYCX01qGp2baEyfN2HM73kf/7Vp3GvMHWRy+zUXISONEtFv7UTViOXnmFrg=="],
"@clickhouse/client-common": ["@clickhouse/client-common@0.2.10", "", {}, "sha512-BvTY0IXS96y9RUeNCpKL4HUzHmY80L0lDcGN0lmUD6zjOqYMn78+xyHYJ/AIAX7JQsc+/KwFt2soZutQTKxoGQ=="],
@ -213,7 +233,7 @@
"@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
"@electron/node-gyp": ["@electron/node-gyp@10.2.0-electron.2", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OhO6fwqpetMO1vWI3+J8mb3a4s4A405tgKoUCJsgd4nyQDdFh0VvZm+gj/Cc70iRLQoIYUfSaAgYSVwmLsQHig=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],

View file

@ -15,6 +15,10 @@
"dev:desktop:remote": "bun run scripts/dev-desktop.ts --remote",
"dev:web": "bun --cwd=apps/web run dev",
"dev:services": "bun run scripts/dev-services.ts",
"fmt": "biome format --write .",
"fmt:check": "biome format .",
"lint": "biome lint .",
"check": "biome check .",
"package:desktop": "bun --cwd=apps/desktop run package",
"make:desktop": "bun --cwd=apps/desktop run make",
"deploy": "bun run scripts/deploy.ts",
@ -26,6 +30,7 @@
"check:docker-workspace": "bun run scripts/check-docker-workspace.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@types/bun": "^1.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3",
@ -34,7 +39,8 @@
"overrides": {
"postcss": "^8.5.15",
"tar": "^7.5.15",
"tmp": "^0.2.5"
"tmp": "^0.2.5",
"@electron/node-gyp": "^10.2.0-electron.2"
},
"dependencies": {
"@pierre/diffs": "^1.2.2"

View file

@ -8,6 +8,6 @@
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"noEmit": true,
},
"noEmit": true
}
}

View file

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CI Dependency Resolution Fix</title>
<style>
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
margin: 0;
padding: 24px;
line-height: 1.45;
color: #111;
background: #fff;
}
h1,
h2,
h3 {
margin-top: 1.2em;
}
section {
margin-bottom: 1.4em;
}
.stack {
background: #f6f8fa;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
overflow: auto;
}
code,
pre {
font-family: ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
a {
color: #0b57d0;
}
</style>
</head>
<body>
<h1>CI Dependency Resolution Fix</h1>
<section>
<h2>Summary</h2>
<p>
I fixed the failing Forgejo CI install by removing the GitHub git-commit dependency on
<code>@electron/node-gyp</code> from lock resolution and forcing it through the npm package
<code>@electron/node-gyp@^10.2.0-electron.2</code> via repository overrides.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>
Added an override in <a href="/Users/kell/dev/islandflow/package.json:35">package.json</a>:
<code>@electron/node-gyp: ^10.2.0-electron.2</code>.
</li>
<li>
Synchronized lock files so both roots resolve from the npm package instead of the
short GitHub tarball ref:
<a href="/Users/kell/dev/islandflow/bun.lock:172">bun.lock</a> and
<a href="/Users/kell/dev/islandflow/deployment/docker/workspace-root/bun.lock:172">deployment/docker/workspace-root/bun.lock</a>.
</li>
<li>
Updated mirrored workspace package manifest:
<a href="/Users/kell/dev/islandflow/deployment/docker/workspace-root/package.json:35">deployment/docker/workspace-root/package.json</a>.
</li>
<li>
Re-synced docker workspace files using <code>bun run sync:docker-workspace</code>.
</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
CI was failing in dependency install with this error:
</p>
<pre class="stack">error: failed to download @electron/node-gyp@github:electron/node-gyp#06b29aa ... 404 Not Found</pre>
<p>
In this environment, that endpoint is interpreted by the Forgejo git proxy and the
short SHA is resolved against an unavailable internal mirror path. For a CI runner, this is
a fragile install path.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>
Using an override keeps all transitive graph consumers of <code>@electron/node-gyp</code>
on the same npm release and avoids GitHub tarball URL resolution entirely.
</li>
<li>
The lockfile entry moved from a git URL spec to
<code>@electron/node-gyp@10.2.0-electron.2</code> with a resolved tarball checksum entry,
which is stable in CI contexts.
</li>
<li>
The Docker workspace copy was updated to avoid drift between root and
deployment lock snapshots.
</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<pre class="stack"><code>diff --git a/package.json b/package.json
@@
"overrides": {
"postcss": "^8.5.15",
"tar": "^7.5.15",
- "tmp": "^0.2.5"
+ "tmp": "^0.2.5",
+ "@electron/node-gyp": "^10.2.0-electron.2"
},
@@
diff --git a/deployment/docker/workspace-root/package.json b/deployment/docker/workspace-root/package.json
@@
"overrides": {
"postcss": "^8.5.15",
"tar": "^7.5.15",
- "tmp": "^0.2.5"
+ "tmp": "^0.2.5",
+ "@electron/node-gyp": "^10.2.0-electron.2"
},
@@
diff --git a/bun.lock b/bun.lock
@@
- "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
+ "@electron/node-gyp": ["@electron/node-gyp@10.2.0-electron.2", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OhO6fwqpetMO1vWI3+J8mb3a4s4A405tgKoUCJsgd4nyQDdFh0VvZm+gj/Cc70iRLQoIYUfSaAgYSVwmLsQHig=="],
@@
diff --git a/deployment/docker/workspace-root/bun.lock b/deployment/docker/workspace-root/bun.lock
@@
- "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
+ "@electron/node-gyp": ["@electron/node-gyp@10.2.0-electron.2", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OhO6fwqpetMO1vWI3+J8mb3a4s4A405tgKoUCJsgd4nyQDdFh0VvZm+gj/Cc70iRLQoIYUfSaAgYSVwmLsQHig=="],
</code></pre>
<p>
Note: For this repository-required documentation rule, lockfile snippets were summarized
directly because rendered <code>@pierre/diffs</code> output is very verbose with embedded
style payloads for each file block.
</p>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<ul>
<li>Forgejo CI installs should no longer fail on unresolved <code>@electron/node-gyp</code> GitHub commit tarball lookups.</li>
<li>Dependency install becomes deterministic using a versioned npm package artifact.</li>
<li>Docker workspace and root lockfiles remain in sync.</li>
</ul>
</section>
<section>
<h2>Validation</h2>
<ul>
<li><code>bun install</code> (lock refresh after override)</li>
<li><code>bun install --frozen-lockfile</code></li>
<li><code>bun run typecheck</code></li>
<li><code>bun run check:docker-workspace</code></li>
<li><code>bun test</code></li>
<li><code>bun --cwd=apps/web run build</code></li>
</ul>
<p>All checks completed successfully.</p>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>
The transitive package <code>@electron/rebuild</code> still references the same GitHub commit in its
dependency metadata, but override forces resolution to the npm package, which is now what the lock
consumes in this repo.
</li>
<li>
If another service writes lockfile with a different package-manager behavior, a re-sync is required.
We already captured this in the workflow by syncing the docker workspace copy.
</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Watch one CI run on Forgejo to confirm the endpoint that caused 404 is fully gone.</li>
<li>Consider a small dependency bump for <code>@electron/rebuild</code> if it later publishes a lockfile-safe package-only variant.</li>
<li>Pin lockfile sync as a required step in any scripted dependency maintenance path.</li>
</ul>
</section>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,10 @@
"dev:desktop:remote": "bun run scripts/dev-desktop.ts --remote",
"dev:web": "bun --cwd=apps/web run dev",
"dev:services": "bun run scripts/dev-services.ts",
"fmt": "biome format --write .",
"fmt:check": "biome format .",
"lint": "biome lint .",
"check": "biome check .",
"package:desktop": "bun --cwd=apps/desktop run package",
"make:desktop": "bun --cwd=apps/desktop run make",
"deploy": "bun run scripts/deploy.ts",
@ -26,6 +30,7 @@
"check:docker-workspace": "bun run scripts/check-docker-workspace.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@types/bun": "^1.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3",
@ -34,7 +39,8 @@
"overrides": {
"postcss": "^8.5.15",
"tar": "^7.5.15",
"tmp": "^0.2.5"
"tmp": "^0.2.5",
"@electron/node-gyp": "^10.2.0-electron.2"
},
"dependencies": {
"@pierre/diffs": "^1.2.2"

View file

@ -16,7 +16,11 @@ import {
nanos,
millis
} from "nats";
import { getKnownStreamDefinitions, getStreamDefinition, type StreamRetentionClass } from "./streams";
import {
getKnownStreamDefinitions,
getStreamDefinition,
type StreamRetentionClass
} from "./streams";
export type NatsConnectionOptions = {
servers: string | string[];
@ -251,7 +255,8 @@ const diffConfigFields = (
for (const field of fields) {
const currentValue = getFieldValue(current, field);
const desiredValue = getFieldValue(desired, field);
const matches = Array.isArray(currentValue) && Array.isArray(desiredValue)
const matches =
Array.isArray(currentValue) && Array.isArray(desiredValue)
? arraysEqual(currentValue, desiredValue)
: currentValue === desiredValue;
@ -391,7 +396,10 @@ const formatStructuredValue = (value: unknown): string => {
const formatStructuralMismatchMessage = (audit: StreamAuditReport): string => {
const details = audit.structuralMismatch
.map((delta) => `${delta.field} current=${formatStructuredValue(delta.current)} desired=${formatStructuredValue(delta.desired)}`)
.map(
(delta) =>
`${delta.field} current=${formatStructuredValue(delta.current)} desired=${formatStructuredValue(delta.desired)}`
)
.join("; ");
return `Refusing to reconcile stream ${audit.name}: structural mismatch (${details})`;
};
@ -447,12 +455,14 @@ const formatReportLine = (
case "retention_drift": {
const details = report.retentionDrift
.map((delta) => {
const desiredValue = delta.field === "max_age"
const desiredValue =
delta.field === "max_age"
? formatDurationMs(millis(Number(delta.desired)))
: delta.field === "max_bytes"
? formatBytes(Number(delta.desired))
: formatStructuredValue(delta.desired);
const currentValue = delta.field === "max_age"
const currentValue =
delta.field === "max_age"
? formatDurationMs(millis(Number(delta.current)))
: delta.field === "max_bytes"
? formatBytes(Number(delta.current))
@ -464,7 +474,10 @@ const formatReportLine = (
}
case "structural_mismatch": {
const details = report.structuralMismatch
.map((delta) => `${delta.field}:${formatStructuredValue(delta.current)}->${formatStructuredValue(delta.desired)}`)
.map(
(delta) =>
`${delta.field}:${formatStructuredValue(delta.current)}->${formatStructuredValue(delta.desired)}`
)
.join(" ");
return `${report.name} structural-mismatch ${details}`;
}

View file

@ -59,7 +59,9 @@ export const STREAM_CATALOG: readonly KnownStreamDefinition[] = [
{ name: STREAM_NEWS, subject: SUBJECT_NEWS, retentionClass: "derived" }
];
const STREAM_CATALOG_BY_NAME = new Map(STREAM_CATALOG.map((definition) => [definition.name, definition]));
const STREAM_CATALOG_BY_NAME = new Map(
STREAM_CATALOG.map((definition) => [definition.name, definition])
);
export const getKnownStreamDefinitions = (): readonly KnownStreamDefinition[] => {
return STREAM_CATALOG;

View file

@ -11,44 +11,31 @@ export const SYNTHETIC_CONTROL_GLOBAL_KEY = "global";
const codec = JSONCodec<SyntheticControlState>();
const decodeSyntheticControlEntry = (
entry: KvEntry | null | undefined
): SyntheticControlState => {
const decodeSyntheticControlEntry = (entry: KvEntry | null | undefined): SyntheticControlState => {
if (!entry || entry.operation !== "PUT") {
return DEFAULT_SYNTHETIC_CONTROL_STATE;
}
return SyntheticControlStateSchema.parse(entry.json());
};
export const openSyntheticControlKv = async (
js: JetStreamClient
): Promise<KV> => {
export const openSyntheticControlKv = async (js: JetStreamClient): Promise<KV> => {
return js.views.kv(SYNTHETIC_CONTROL_BUCKET, {
description: "Hosted synthetic market internal control state",
history: 8
});
};
export const readSyntheticControlState = async (
kv: KV
): Promise<SyntheticControlState> => {
return decodeSyntheticControlEntry(
await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY)
);
export const readSyntheticControlState = async (kv: KV): Promise<SyntheticControlState> => {
return decodeSyntheticControlEntry(await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY));
};
export const ensureSyntheticControlState = async (
kv: KV
): Promise<SyntheticControlState> => {
export const ensureSyntheticControlState = async (kv: KV): Promise<SyntheticControlState> => {
const current = await kv.get(SYNTHETIC_CONTROL_GLOBAL_KEY);
if (current && current.operation === "PUT") {
return SyntheticControlStateSchema.parse(current.json());
}
await kv.put(
SYNTHETIC_CONTROL_GLOBAL_KEY,
codec.encode(DEFAULT_SYNTHETIC_CONTROL_STATE)
);
await kv.put(SYNTHETIC_CONTROL_GLOBAL_KEY, codec.encode(DEFAULT_SYNTHETIC_CONTROL_STATE));
return DEFAULT_SYNTHETIC_CONTROL_STATE;
};
@ -57,10 +44,7 @@ export const writeSyntheticControlState = async (
control: Partial<SyntheticControlState>
): Promise<SyntheticControlState> => {
const normalized = normalizeSyntheticControlState(control);
await kv.put(
SYNTHETIC_CONTROL_GLOBAL_KEY,
codec.encode(normalized)
);
await kv.put(SYNTHETIC_CONTROL_GLOBAL_KEY, codec.encode(normalized));
return normalized;
};

View file

@ -43,10 +43,9 @@ const buildMockStreamManager = (configs: Record<string, StreamConfig | null>) =>
};
const buildAllKnownConfigs = (env: Record<string, string | undefined> = {}) => {
return Object.fromEntries(STREAMS.map((name) => [name, buildKnownStreamConfig(name, env)])) as Record<
string,
StreamConfig
>;
return Object.fromEntries(
STREAMS.map((name) => [name, buildKnownStreamConfig(name, env)])
) as Record<string, StreamConfig>;
};
describe("jetstream retention defaults", () => {
@ -194,7 +193,9 @@ describe("runReconcileStreamsCommand", () => {
});
expect(exitCode).toBe(1);
expect(outputs.some((line) => line.includes("OPTIONS_PRINTS") && line.includes("drift"))).toBe(true);
expect(outputs.some((line) => line.includes("OPTIONS_PRINTS") && line.includes("drift"))).toBe(
true
);
});
it("updates drift in --apply mode and reports actions", async () => {
@ -240,7 +241,11 @@ describe("runReconcileStreamsCommand", () => {
});
expect(exitCode).toBe(1);
expect(outputs.some((line) => line.includes("OPTIONS_PRINTS") && line.includes("structural-mismatch"))).toBe(true);
expect(
outputs.some(
(line) => line.includes("OPTIONS_PRINTS") && line.includes("structural-mismatch")
)
).toBe(true);
expect(errors.some((line) => line.includes("OPTIONS_PRINTS"))).toBe(true);
});
});

View file

@ -15,14 +15,10 @@ type AlpacaCredentialEnv = {
const normalize = (value: string | undefined): string => value?.trim() ?? "";
export const resolveAlpacaCredentials = (
env: AlpacaCredentialEnv
): AlpacaCredentials => {
export const resolveAlpacaCredentials = (env: AlpacaCredentialEnv): AlpacaCredentials => {
const legacyToken = normalize(env.ALPACA_API_KEY);
const explicitKeyId =
normalize(env.ALPACA_API_KEY_ID) || normalize(env.ALPACA_KEY_ID);
const secret =
normalize(env.ALPACA_API_SECRET_KEY) || normalize(env.ALPACA_SECRET_KEY);
const explicitKeyId = normalize(env.ALPACA_API_KEY_ID) || normalize(env.ALPACA_KEY_ID);
const secret = normalize(env.ALPACA_API_SECRET_KEY) || normalize(env.ALPACA_SECRET_KEY);
const keyId = explicitKeyId || legacyToken;
const usesLegacyBearer = !explicitKeyId && !secret && legacyToken.length > 0;
@ -42,9 +38,7 @@ export const hasAlpacaCredentials = (credentials: AlpacaCredentials): boolean =>
return credentials.keyId.length > 0 && credentials.secret.length > 0;
};
export const buildAlpacaAuthHeaders = (
credentials: AlpacaCredentials
): Record<string, string> => {
export const buildAlpacaAuthHeaders = (credentials: AlpacaCredentials): Record<string, string> => {
if (credentials.usesLegacyBearer) {
return {
Authorization: `Bearer ${credentials.legacyToken}`

View file

@ -99,7 +99,9 @@ const safeProfileScoreArray = (value: string): SmartMoneyProfileScore[] => {
return {
profile_id: String(record.profile_id ?? "") as SmartMoneyProfileScore["profile_id"],
probability: Number(record.probability ?? 0),
confidence_band: String(record.confidence_band ?? "low") as SmartMoneyProfileScore["confidence_band"],
confidence_band: String(
record.confidence_band ?? "low"
) as SmartMoneyProfileScore["confidence_band"],
direction: String(record.direction ?? "unknown") as SmartMoneyProfileScore["direction"],
reasons: Array.isArray(record.reasons) ? record.reasons.map((item) => String(item)) : []
};
@ -122,7 +124,9 @@ export const fromAlertRecord = (record: AlertRecord): AlertEvent => {
severity: record.severity,
hits: safeHitArray(record.hits_json),
evidence_refs: safeStringArray(record.evidence_refs_json),
...(record.primary_profile_id ? { primary_profile_id: record.primary_profile_id as AlertEvent["primary_profile_id"] } : {}),
...(record.primary_profile_id
? { primary_profile_id: record.primary_profile_id as AlertEvent["primary_profile_id"] }
: {}),
profile_scores: safeProfileScoreArray(record.profile_scores_json)
};
};

View file

@ -35,16 +35,8 @@ import {
OPTION_PRINTS_TABLE
} from "./option-prints";
import { normalizeOptionNBBO, optionNBBOTableDDL, OPTION_NBBO_TABLE } from "./option-nbbo";
import {
equityPrintsTableDDL,
EQUITY_PRINTS_TABLE,
normalizeEquityPrint
} from "./equity-prints";
import {
equityQuotesTableDDL,
EQUITY_QUOTES_TABLE,
normalizeEquityQuote
} from "./equity-quotes";
import { equityPrintsTableDDL, EQUITY_PRINTS_TABLE, normalizeEquityPrint } from "./equity-prints";
import { equityQuotesTableDDL, EQUITY_QUOTES_TABLE, normalizeEquityQuote } from "./equity-quotes";
import {
equityCandlesTableDDL,
EQUITY_CANDLES_TABLE,
@ -93,13 +85,7 @@ import {
toSmartMoneyEventRecord,
type SmartMoneyEventRecord
} from "./smart-money-events";
import {
NEWS_TABLE,
newsTableDDL,
fromNewsRecord,
toNewsRecord,
type NewsRecord
} from "./news";
import { NEWS_TABLE, newsTableDDL, fromNewsRecord, toNewsRecord, type NewsRecord } from "./news";
export type ClickHouseOptions = {
url: string;
@ -116,7 +102,11 @@ type ClickHouseQueryResult = {
export type ClickHouseClient = {
exec(params: { query: string }): Promise<void>;
insert(params: { table: string; values: unknown[]; format: ClickHouseQueryFormat }): Promise<void>;
insert(params: {
table: string;
values: unknown[];
format: ClickHouseQueryFormat;
}): Promise<void>;
query(params: { query: string; format: ClickHouseQueryFormat }): Promise<ClickHouseQueryResult>;
ping(): Promise<{ success: boolean; error?: Error }>;
close(): Promise<void>;
@ -140,7 +130,9 @@ const buildHeaders = (options: ClickHouseOptions, hasBody: boolean): Headers =>
}
if (options.username || options.password) {
const auth = Buffer.from(`${options.username ?? "default"}:${options.password ?? ""}`).toString("base64");
const auth = Buffer.from(`${options.username ?? "default"}:${options.password ?? ""}`).toString(
"base64"
);
headers.set("authorization", `Basic ${auth}`);
}
@ -217,7 +209,8 @@ export const createClickHouseClient = (options: ClickHouseOptions): ClickHouseCl
});
if (!response.ok) {
const message = (await response.text()).trim() || `${response.status} ${response.statusText}`;
const message =
(await response.text()).trim() || `${response.status} ${response.statusText}`;
return { success: false, error: new Error(message) };
}
@ -237,9 +230,7 @@ export const createClickHouseClient = (options: ClickHouseOptions): ClickHouseCl
};
};
export const ensureOptionPrintsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureOptionPrintsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: optionPrintsTableDDL()
});
@ -248,73 +239,55 @@ export const ensureOptionPrintsTable = async (
}
};
export const ensureOptionNBBOTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureOptionNBBOTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: optionNBBOTableDDL()
});
};
export const ensureEquityPrintsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureEquityPrintsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: equityPrintsTableDDL()
});
};
export const ensureEquityQuotesTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureEquityQuotesTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: equityQuotesTableDDL()
});
};
export const ensureEquityCandlesTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureEquityCandlesTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: equityCandlesTableDDL()
});
};
export const ensureEquityPrintJoinsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureEquityPrintJoinsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: equityPrintJoinsTableDDL()
});
};
export const ensureInferredDarkTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureInferredDarkTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: inferredDarkTableDDL()
});
};
export const ensureFlowPacketsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureFlowPacketsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: flowPacketsTableDDL()
});
};
export const ensureSmartMoneyEventsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureSmartMoneyEventsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: smartMoneyEventsTableDDL()
});
};
export const ensureClassifierHitsTable = async (
client: ClickHouseClient
): Promise<void> => {
export const ensureClassifierHitsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: classifierHitsTableDDL()
});
@ -464,7 +437,10 @@ export const insertAlert = async (client: ClickHouseClient, alert: AlertEvent):
});
};
export const insertNewsStory = async (client: ClickHouseClient, story: NewsStory): Promise<void> => {
export const insertNewsStory = async (
client: ClickHouseClient,
story: NewsStory
): Promise<void> => {
const record = toNewsRecord(story);
await client.insert({
table: NEWS_TABLE,
@ -617,17 +593,11 @@ export const enqueueClassifierHitInsert = (
writer.enqueue(CLASSIFIER_HITS_TABLE, toClassifierHitRecord(hit));
};
export const enqueueAlertInsert = (
writer: ClickHouseBatchWriter,
alert: AlertEvent
): void => {
export const enqueueAlertInsert = (writer: ClickHouseBatchWriter, alert: AlertEvent): void => {
writer.enqueue(ALERTS_TABLE, toAlertRecord(alert));
};
export const enqueueNewsStoryInsert = (
writer: ClickHouseBatchWriter,
story: NewsStory
): void => {
export const enqueueNewsStoryInsert = (writer: ClickHouseBatchWriter, story: NewsStory): void => {
writer.enqueue(NEWS_TABLE, toNewsRecord(story));
};
@ -973,9 +943,7 @@ const normalizeFlowPacketRow = (row: unknown): FlowPacketRecord | null => {
seq: coerceNumber(record.seq) as number,
trace_id: String(record.trace_id ?? ""),
id: String(record.id ?? ""),
members: Array.isArray(record.members)
? record.members.map((value) => String(value))
: [],
members: Array.isArray(record.members) ? record.members.map((value) => String(value)) : [],
features_json: String(record.features_json ?? "{}"),
join_quality_json: String(record.join_quality_json ?? "{}")
};
@ -1011,7 +979,9 @@ const normalizeSmartMoneyEventRow = (row: unknown): SmartMoneyEventRecord | null
seq: coerceNumber(record.seq) as number,
trace_id: String(record.trace_id ?? ""),
event_id: String(record.event_id ?? ""),
packet_ids: Array.isArray(record.packet_ids) ? record.packet_ids.map((value) => String(value)) : [],
packet_ids: Array.isArray(record.packet_ids)
? record.packet_ids.map((value) => String(value))
: [],
member_print_ids: Array.isArray(record.member_print_ids)
? record.member_print_ids.map((value) => String(value))
: [],
@ -1390,8 +1360,12 @@ export const fetchAlertContextByTraceId = async (
const packetIds = new Set(flowPackets.flatMap((packet) => [packet.id, packet.trace_id]));
const printIds = new Set(optionPrints.map((print) => print.trace_id));
const missingRefs = refs.filter((ref) => {
const packetResolved = flowPacketCandidatesFromRef(ref).some((candidate) => packetIds.has(candidate));
const printResolved = optionPrintCandidatesFromRef(ref).some((candidate) => printIds.has(candidate));
const packetResolved = flowPacketCandidatesFromRef(ref).some((candidate) =>
packetIds.has(candidate)
);
const printResolved = optionPrintCandidatesFromRef(ref).some((candidate) =>
printIds.has(candidate)
);
return !packetResolved && !printResolved;
});

View file

@ -130,7 +130,10 @@ describe("alerts storage helpers", () => {
});
it("returns an empty context when the alert is missing", async () => {
const bundle = await fetchAlertContextByTraceId(makeClient(() => []), "alert:missing");
const bundle = await fetchAlertContextByTraceId(
makeClient(() => []),
"alert:missing"
);
expect(bundle).toEqual({
alert: null,

View file

@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test";
import { createClickHouseClient, fetchFlowPacketById, fetchFlowPacketsBefore } from "../src/clickhouse";
import {
createClickHouseClient,
fetchFlowPacketById,
fetchFlowPacketsBefore
} from "../src/clickhouse";
import {
flowPacketsTableDDL,
FLOW_PACKETS_TABLE,

View file

@ -1,16 +1,7 @@
import { describe, expect, it } from "bun:test";
import type { ClickHouseClient } from "../src/clickhouse";
import {
NEWS_TABLE,
fromNewsRecord,
newsTableDDL,
toNewsRecord
} from "../src/news";
import {
fetchNewsAfter,
fetchNewsBefore,
fetchRecentNews
} from "../src/clickhouse";
import { NEWS_TABLE, fromNewsRecord, newsTableDDL, toNewsRecord } from "../src/news";
import { fetchNewsAfter, fetchNewsBefore, fetchRecentNews } from "../src/clickhouse";
const makeClient = (resolver: (query: string) => unknown[]): ClickHouseClient =>
({

View file

@ -5,7 +5,11 @@ import {
fetchOptionPrintsByTraceIds,
fetchRecentOptionPrints
} from "../src/clickhouse";
import { normalizeOptionPrint, optionPrintsTableDDL, OPTION_PRINTS_TABLE } from "../src/option-prints";
import {
normalizeOptionPrint,
optionPrintsTableDDL,
OPTION_PRINTS_TABLE
} from "../src/option-prints";
const basePrint = {
source_ts: 100,

View file

@ -18,37 +18,103 @@ export const OptionPrintSchema = EventMetaSchema.merge(
size: z.number().int().positive(),
exchange: z.string().min(1),
conditions: z.array(z.string().min(1)).optional(),
underlying_id: z.preprocess((value) => (value === null ? undefined : value), z.string().min(1).optional()),
option_type: z.preprocess((value) => (value === null ? undefined : value), OptionTypeSchema.optional()),
notional: z.preprocess((value) => (value === null ? undefined : value), z.number().nonnegative().optional()),
nbbo_side: z.preprocess((value) => (value === null ? undefined : value), OptionNbboSideSchema.optional()),
execution_nbbo_bid: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_nbbo_ask: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_nbbo_mid: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_nbbo_spread: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_nbbo_bid_size: z.preprocess((value) => (value === null ? undefined : value), z.number().int().nonnegative().optional()),
execution_nbbo_ask_size: z.preprocess((value) => (value === null ? undefined : value), z.number().int().nonnegative().optional()),
execution_nbbo_ts: z.preprocess((value) => (value === null ? undefined : value), z.number().int().nonnegative().optional()),
execution_nbbo_age_ms: z.preprocess((value) => (value === null ? undefined : value), z.number().nonnegative().optional()),
execution_nbbo_side: z.preprocess((value) => (value === null ? undefined : value), OptionNbboSideSchema.optional()),
execution_underlying_spot: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_underlying_bid: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_underlying_ask: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_underlying_mid: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_underlying_spread: z.preprocess((value) => (value === null ? undefined : value), z.number().optional()),
execution_underlying_ts: z.preprocess((value) => (value === null ? undefined : value), z.number().int().nonnegative().optional()),
execution_underlying_age_ms: z.preprocess((value) => (value === null ? undefined : value), z.number().nonnegative().optional()),
underlying_id: z.preprocess(
(value) => (value === null ? undefined : value),
z.string().min(1).optional()
),
option_type: z.preprocess(
(value) => (value === null ? undefined : value),
OptionTypeSchema.optional()
),
notional: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().nonnegative().optional()
),
nbbo_side: z.preprocess(
(value) => (value === null ? undefined : value),
OptionNbboSideSchema.optional()
),
execution_nbbo_bid: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_nbbo_ask: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_nbbo_mid: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_nbbo_spread: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_nbbo_bid_size: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().int().nonnegative().optional()
),
execution_nbbo_ask_size: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().int().nonnegative().optional()
),
execution_nbbo_ts: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().int().nonnegative().optional()
),
execution_nbbo_age_ms: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().nonnegative().optional()
),
execution_nbbo_side: z.preprocess(
(value) => (value === null ? undefined : value),
OptionNbboSideSchema.optional()
),
execution_underlying_spot: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_underlying_bid: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_underlying_ask: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_underlying_mid: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_underlying_spread: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().optional()
),
execution_underlying_ts: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().int().nonnegative().optional()
),
execution_underlying_age_ms: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().nonnegative().optional()
),
execution_underlying_source: z.preprocess(
(value) => (value === null ? undefined : value),
z.literal("equity_quote_mid").optional()
),
execution_iv: z.preprocess((value) => (value === null ? undefined : value), z.number().nonnegative().optional()),
execution_iv: z.preprocess(
(value) => (value === null ? undefined : value),
z.number().nonnegative().optional()
),
execution_iv_source: z.preprocess(
(value) => (value === null ? undefined : value),
z.enum(["provider", "synthetic_pressure_model"]).optional()
),
is_etf: z.preprocess((value) => (value === null ? undefined : value), z.boolean().optional()),
signal_pass: z.preprocess((value) => (value === null ? undefined : value), z.boolean().optional()),
signal_pass: z.preprocess(
(value) => (value === null ? undefined : value),
z.boolean().optional()
),
signal_reasons: z.array(z.string().min(1)).optional(),
signal_profile: z.preprocess(
(value) => (value === null ? undefined : value),
@ -146,7 +212,13 @@ export const SmartMoneyProfileIdSchema = z.enum([
export type SmartMoneyProfileId = z.infer<typeof SmartMoneyProfileIdSchema>;
export const SmartMoneyDirectionSchema = z.enum(["bullish", "bearish", "neutral", "mixed", "unknown"]);
export const SmartMoneyDirectionSchema = z.enum([
"bullish",
"bearish",
"neutral",
"mixed",
"unknown"
]);
export type SmartMoneyDirection = z.infer<typeof SmartMoneyDirectionSchema>;

View file

@ -13,10 +13,7 @@ import {
OptionPrintSchema,
SmartMoneyEventSchema
} from "./events";
import {
OptionFlowFiltersSchema,
optionFlowFilterKey
} from "./options-flow";
import { OptionFlowFiltersSchema, optionFlowFilterKey } from "./options-flow";
export const CursorSchema = z.object({
ts: z.number().int().nonnegative(),
@ -94,7 +91,15 @@ export const LiveSubscriptionSchema = z.discriminatedUnion("channel", [
snapshot_limit: z.number().int().positive().optional()
}),
z.object({
channel: z.enum(["nbbo", "equity-quotes", "equity-joins", "classifier-hits", "alerts", "inferred-dark", "news"]),
channel: z.enum([
"nbbo",
"equity-quotes",
"equity-joins",
"classifier-hits",
"alerts",
"inferred-dark",
"news"
]),
snapshot_limit: z.number().int().positive().optional()
}),
z.object({

View file

@ -212,7 +212,8 @@ export const deriveOptionPrintMetadata = (
const parsed = parseOptionContractId(print.option_contract_id);
const underlying = parsed?.root?.toUpperCase();
const optionType = parsed?.right === "C" ? "call" : parsed?.right === "P" ? "put" : undefined;
const notional = Number.isFinite(print.price) && Number.isFinite(print.size)
const notional =
Number.isFinite(print.price) && Number.isFinite(print.size)
? Number((print.price * print.size * 100).toFixed(2))
: undefined;
@ -243,7 +244,14 @@ const balancedThresholds = (config: OptionsSignalConfig): OptionsSignalConfig =>
export const evaluateOptionSignal = (
print: Pick<
OptionPrint,
"size" | "conditions" | "signal_profile" | "underlying_id" | "option_type" | "notional" | "nbbo_side" | "is_etf"
| "size"
| "conditions"
| "signal_profile"
| "underlying_id"
| "option_type"
| "notional"
| "nbbo_side"
| "is_etf"
>,
baseConfig: OptionsSignalConfig
): OptionSignalDecision => {
@ -260,7 +268,8 @@ export const evaluateOptionSignal = (
const reasons: string[] = [];
const notional = print.notional ?? 0;
const side = print.nbbo_side ?? "MISSING";
const isSweepOrIso = hasCondition(print.conditions, "SWEEP") || hasCondition(print.conditions, "ISO");
const isSweepOrIso =
hasCondition(print.conditions, "SWEEP") || hasCondition(print.conditions, "ISO");
if (notional < config.minNotional) {
return {
@ -413,8 +422,14 @@ export const matchesFlowPacketFilters = (
}
const features = packet.features ?? {};
const totalNotional = typeof features.total_notional === "number" ? features.total_notional : Number(features.total_notional ?? 0);
if (typeof filters.minNotional === "number" && (!Number.isFinite(totalNotional) || totalNotional < filters.minNotional)) {
const totalNotional =
typeof features.total_notional === "number"
? features.total_notional
: Number(features.total_notional ?? 0);
if (
typeof filters.minNotional === "number" &&
(!Number.isFinite(totalNotional) || totalNotional < filters.minNotional)
) {
return false;
}
@ -433,10 +448,7 @@ export const matchesFlowPacketFilters = (
: typeof features.structure_rights === "string"
? features.structure_rights.toLowerCase()
: null;
if (
!optionType ||
!filters.optionTypes.some((selected) => optionType.includes(selected))
) {
if (!optionType || !filters.optionTypes.some((selected) => optionType.includes(selected))) {
return false;
}
}

View file

@ -501,7 +501,7 @@ export const SP500_SYMBOLS = [
"YUM",
"ZBRA",
"ZBH",
"ZTS",
"ZTS"
] as const;
export type Sp500Symbol = typeof SP500_SYMBOLS[number];
export type Sp500Symbol = (typeof SP500_SYMBOLS)[number];

View file

@ -26,10 +26,7 @@ const SMART_MONEY_PROFILE_IDS = [
"arbitrage",
"hedge_reactive"
] as const satisfies readonly SmartMoneyProfileId[];
const SYNTHETIC_SCENARIO_FAMILY_IDS = [
...SMART_MONEY_PROFILE_IDS,
"neutral_noise"
] as const;
const SYNTHETIC_SCENARIO_FAMILY_IDS = [...SMART_MONEY_PROFILE_IDS, "neutral_noise"] as const;
const REGIME_IDS = [
"trend_up",
"trend_down",
@ -54,18 +51,14 @@ export const SyntheticCoverageWindowMinutesSchema = z.union([
z.literal(20),
z.literal(30)
]);
export type SyntheticCoverageWindowMinutes = z.infer<
typeof SyntheticCoverageWindowMinutesSchema
>;
export type SyntheticCoverageWindowMinutes = z.infer<typeof SyntheticCoverageWindowMinutesSchema>;
export const SyntheticProfileWeightValueSchema = z.union([
z.literal(0.6),
z.literal(1.0),
z.literal(1.6)
]);
export type SyntheticProfileWeightValue = z.infer<
typeof SyntheticProfileWeightValueSchema
>;
export type SyntheticProfileWeightValue = z.infer<typeof SyntheticProfileWeightValueSchema>;
export const SyntheticProfileWeightMapSchema = z
.object({
@ -77,9 +70,7 @@ export const SyntheticProfileWeightMapSchema = z
hedge_reactive: SyntheticProfileWeightValueSchema
})
.strict();
export type SyntheticProfileWeightMap = z.infer<
typeof SyntheticProfileWeightMapSchema
>;
export type SyntheticProfileWeightMap = z.infer<typeof SyntheticProfileWeightMapSchema>;
export const SyntheticControlStateSchema = z
.object({
@ -94,23 +85,14 @@ export const SyntheticControlStateSchema = z
.strict();
export type SyntheticControlState = z.infer<typeof SyntheticControlStateSchema>;
export const SyntheticSessionPhaseSchema = z.enum([
"open",
"midday",
"power_hour",
"after_event"
]);
export const SyntheticSessionPhaseSchema = z.enum(["open", "midday", "power_hour", "after_event"]);
export type SyntheticSessionPhase = z.infer<typeof SyntheticSessionPhaseSchema>;
export const SyntheticRegimeSchema = z.enum(REGIME_IDS);
export type SyntheticRegime = z.infer<typeof SyntheticRegimeSchema>;
export const SyntheticScenarioFamilyIdSchema = z.enum(
SYNTHETIC_SCENARIO_FAMILY_IDS
);
export type SyntheticScenarioFamilyId = z.infer<
typeof SyntheticScenarioFamilyIdSchema
>;
export const SyntheticScenarioFamilyIdSchema = z.enum(SYNTHETIC_SCENARIO_FAMILY_IDS);
export type SyntheticScenarioFamilyId = z.infer<typeof SyntheticScenarioFamilyIdSchema>;
export const SyntheticCoverageConfigSchema = z
.object({
@ -118,9 +100,7 @@ export const SyntheticCoverageConfigSchema = z
coverage_window_minutes: SyntheticCoverageWindowMinutesSchema
})
.strict();
export type SyntheticCoverageConfig = z.infer<
typeof SyntheticCoverageConfigSchema
>;
export type SyntheticCoverageConfig = z.infer<typeof SyntheticCoverageConfigSchema>;
export const SyntheticDerivedStatusSchema = z
.object({
@ -131,9 +111,7 @@ export const SyntheticDerivedStatusSchema = z
coverage_window_minutes: SyntheticCoverageWindowMinutesSchema
})
.strict();
export type SyntheticDerivedStatus = z.infer<
typeof SyntheticDerivedStatusSchema
>;
export type SyntheticDerivedStatus = z.infer<typeof SyntheticDerivedStatusSchema>;
export type SyntheticSessionState = {
session_phase: SyntheticSessionPhase;
@ -160,10 +138,7 @@ export type SyntheticUnderlyingState = {
offExchangeBias: number;
};
export type SyntheticScenarioWeightMap = Record<
SyntheticScenarioFamilyId,
number
>;
export type SyntheticScenarioWeightMap = Record<SyntheticScenarioFamilyId, number>;
export type SyntheticCoverageState = {
profile_hit_counts: Record<SmartMoneyProfileId, number>;
@ -195,10 +170,7 @@ export const DEFAULT_SYNTHETIC_CONTROL_STATE: SyntheticControlState = {
updated_by: "system"
};
const PRESET_REGIME_BIAS: Record<
SyntheticControlPresetId,
Record<SyntheticRegime, number>
> = {
const PRESET_REGIME_BIAS: Record<SyntheticControlPresetId, Record<SyntheticRegime, number>> = {
balanced_demo: {
trend_up: 1.0,
trend_down: 0.95,
@ -257,10 +229,7 @@ const PRESET_ACTIVITY_BIAS: Record<
quiet_range: { focusCount: 2, eventCount: 1, amplitude: 0.72 }
};
const REGIME_PROFILE_BIAS: Record<
SyntheticRegime,
SyntheticScenarioWeightMap
> = {
const REGIME_PROFILE_BIAS: Record<SyntheticRegime, SyntheticScenarioWeightMap> = {
trend_up: {
institutional_directional: 1.35,
retail_whale: 1.05,
@ -411,16 +380,12 @@ const mixSeed = (...parts: number[]): number => {
return seed >>> 0;
};
const pick = <T,>(items: readonly T[], seed: number): T => {
const pick = <T>(items: readonly T[], seed: number): T => {
const index = Math.abs(seed) % items.length;
return items[index]!;
};
const pickManyUnique = <T,>(
items: readonly T[],
count: number,
seed: number
): T[] => {
const pickManyUnique = <T>(items: readonly T[], count: number, seed: number): T[] => {
const pool = [...items];
const output: T[] = [];
let cursor = seed;
@ -432,10 +397,7 @@ const pickManyUnique = <T,>(
return output;
};
const weightedPick = <T extends string>(
weights: Record<T, number>,
seed: number
): T => {
const weightedPick = <T extends string>(weights: Record<T, number>, seed: number): T => {
const entries = Object.entries(weights) as Array<[T, number]>;
const total = entries.reduce((sum, [, weight]) => sum + Math.max(0.0001, weight), 0);
let target = positiveNoise(seed) * total;
@ -461,10 +423,7 @@ export const hashSyntheticSymbol = (value: string): number => {
return hash;
};
export const buildEmptySyntheticProfileHitCounts = (): Record<
SmartMoneyProfileId,
number
> => ({
export const buildEmptySyntheticProfileHitCounts = (): Record<SmartMoneyProfileId, number> => ({
institutional_directional: 0,
retail_whale: 0,
event_driven: 0,
@ -487,10 +446,7 @@ export const normalizeSyntheticControlState = (
return SyntheticControlStateSchema.parse(merged);
};
const resolvePhaseBias = (
phase: SyntheticSessionPhase,
regime: SyntheticRegime
): number => {
const resolvePhaseBias = (phase: SyntheticSessionPhase, regime: SyntheticRegime): number => {
if (phase === "open") {
return regime === "event_ramp" ? 1.08 : 1.02;
}
@ -566,10 +522,7 @@ export const getSyntheticSessionState = (
mixSeed(activitySeed, 211)
);
const focus_symbols: string[] = pickManyUnique(
[
...event_symbols,
...SYNTHETIC_SYMBOLS.filter((symbol) => !event_symbols.includes(symbol))
],
[...event_symbols, ...SYNTHETIC_SYMBOLS.filter((symbol) => !event_symbols.includes(symbol))],
focusCount,
mixSeed(activitySeed, 389)
);
@ -579,11 +532,7 @@ export const getSyntheticSessionState = (
session_phase: phase,
regime,
volatility_level: roundTo(
clamp(
stateBase.volatility * amplitude + signedNoise(activitySeed + 3) * 0.08,
0.18,
1.2
)
clamp(stateBase.volatility * amplitude + signedNoise(activitySeed + 3) * 0.08, 0.18, 1.2)
),
liquidity_level: roundTo(
clamp(
@ -656,9 +605,7 @@ export const getSyntheticUnderlyingState = (
? -meanRevertWave * (12 + session.liquidity_level * 10)
: meanRevertWave * 6;
const gammaChop =
session.regime === "dealer_gamma"
? Math.sin((minuteOfSession + (hash % 11)) / 2.8) * 16
: 0;
session.regime === "dealer_gamma" ? Math.sin((minuteOfSession + (hash % 11)) / 2.8) * 16 : 0;
const noiseBps =
signedNoise(mixSeed(hash, session.seed_bucket, control.shared_seed)) *
(6 + session.volatility_level * 18);
@ -731,10 +678,7 @@ export const getSyntheticScenarioWeights = (
};
for (const profileId of SMART_MONEY_PROFILE_IDS) {
weights[profileId] = roundTo(
weights[profileId] * normalized.profile_weights[profileId],
4
);
weights[profileId] = roundTo(weights[profileId] * normalized.profile_weights[profileId], 4);
}
if (isFocus) {
@ -745,10 +689,7 @@ export const getSyntheticScenarioWeights = (
}
if (isEvent) {
weights.event_driven = roundTo(weights.event_driven * 1.36, 4);
weights.institutional_directional = roundTo(
weights.institutional_directional * 1.04,
4
);
weights.institutional_directional = roundTo(weights.institutional_directional * 1.04, 4);
weights.neutral_noise = roundTo(weights.neutral_noise * 0.8, 4);
}
if (isPower) {
@ -765,10 +706,7 @@ export const getSyntheticScenarioWeights = (
export const getSyntheticCoverageBoost = (
profileId: SmartMoneyProfileId,
coverageState: SyntheticCoverageState,
control: Pick<
SyntheticControlState,
"coverage_assist" | "coverage_window_minutes"
>
control: Pick<SyntheticControlState, "coverage_assist" | "coverage_window_minutes">
): number => {
if (!control.coverage_assist) {
return 1;

View file

@ -31,9 +31,7 @@ describe("live protocol types", () => {
underlying_ids: ["NVDA", "AAPL"],
option_contract_id: "AAPL-2025-01-17-200-C"
})
).toBe(
'options|{"view":"signal"}|underlyings:AAPL,NVDA|contract:AAPL-2025-01-17-200-C'
);
).toBe('options|{"view":"signal"}|underlyings:AAPL,NVDA|contract:AAPL-2025-01-17-200-C');
expect(getSubscriptionKey({ channel: "equities", underlying_ids: ["NVDA", "AAPL"] })).toBe(
"equities|underlyings:AAPL,NVDA"
);

View file

@ -68,7 +68,9 @@ const listWorkspacePaths = async (workspacePatterns: string[]): Promise<string[]
const paths = new Set<string>();
for (const pattern of workspacePatterns) {
const globPattern = pattern.endsWith("/") ? `${pattern}package.json` : `${pattern}/package.json`;
const globPattern = pattern.endsWith("/")
? `${pattern}package.json`
: `${pattern}/package.json`;
const glob = new Bun.Glob(globPattern);
for await (const match of glob.scan({ cwd: repoRoot })) {
const normalized = match.replaceAll("\\", "/");
@ -124,8 +126,14 @@ const formatDependencyDiff = (
const check = async (): Promise<number> => {
const issues: string[] = [];
const [rootPackage, deploymentPackage, rootTsconfig, deploymentTsconfig, rootLock, deploymentLock] =
await Promise.all([
const [
rootPackage,
deploymentPackage,
rootTsconfig,
deploymentTsconfig,
rootLock,
deploymentLock
] = await Promise.all([
parseObjectLiteral<RootPackageManifest>(rootPackagePath),
parseObjectLiteral(deploymentPackagePath),
parseObjectLiteral(rootTsconfigPath),
@ -172,7 +180,9 @@ const check = async (): Promise<number> => {
"peerDependencies"
];
for (const section of sections) {
const expectedMap = normalizedDependencyMap(workspacePackage[section] as DependencyMap | undefined);
const expectedMap = normalizedDependencyMap(
workspacePackage[section] as DependencyMap | undefined
);
const actualMap = normalizedDependencyMap(
deploymentWorkspace[section] as DependencyMap | undefined
);
@ -212,7 +222,9 @@ const check = async (): Promise<number> => {
"peerDependencies"
];
for (const section of sections) {
const expectedMap = normalizedDependencyMap(rootWorkspace[section] as DependencyMap | undefined);
const expectedMap = normalizedDependencyMap(
rootWorkspace[section] as DependencyMap | undefined
);
const actualMap = normalizedDependencyMap(
deploymentWorkspace[section] as DependencyMap | undefined
);

View file

@ -7,7 +7,10 @@ type RouteCheck = {
const routeChecks: RouteCheck[] = [
{ path: "/prints/options?view=signal&limit=1", expectJson: true },
{ path: "/history/options?view=signal&before_ts=4102444800000&before_seq=999999999&limit=1", expectJson: true },
{
path: "/history/options?view=signal&before_ts=4102444800000&before_seq=999999999&limit=1",
expectJson: true
},
{ path: "/replay/options?view=signal&after_ts=0&after_seq=0&limit=1", expectJson: true },
{ path: "/nbbo/options?limit=1", expectJson: true },
{ path: "/ws/live", expectJson: true }
@ -31,7 +34,9 @@ const assertPublicApiRoute = async ({ path, expectJson }: RouteCheck): Promise<v
if (expectJson && !isJsonResponse(response)) {
const sample = responseText.replace(/\s+/g, " ").slice(0, 120);
throw new Error(`${url.pathname} returned non-JSON content (${response.headers.get("content-type") ?? "none"}): ${sample}`);
throw new Error(
`${url.pathname} returned non-JSON content (${response.headers.get("content-type") ?? "none"}): ${sample}`
);
}
};

View file

@ -30,21 +30,12 @@ const SSH_KEY =
process.env.DEPLOY_SSH_KEY_PATH?.trim() ||
path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519");
const DEPLOY_FORCE_SSH = process.env.DEPLOY_FORCE_SSH?.trim() === "1";
const SSH_OPTIONS = [
"-i",
SSH_KEY,
"-o",
"IdentitiesOnly=yes",
"-o",
"BatchMode=yes"
];
const SSH_OPTIONS = ["-i", SSH_KEY, "-o", "IdentitiesOnly=yes", "-o", "BatchMode=yes"];
const ALLOWED_REMOTE_UNTRACKED = new Set([
"deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz"
]);
const PUBLIC_APP_URL =
process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
const PUBLIC_API_HEALTH_URL =
process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
const PUBLIC_APP_URL = process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
const PUBLIC_API_HEALTH_URL = process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
const DEPLOY_GIT_REMOTE_OVERRIDE = process.env.DEPLOY_GIT_REMOTE?.trim() || null;
const DEPLOY_NATIVE_EDGE_READY = process.env.DEPLOY_NATIVE_EDGE_READY?.trim() === "1";
const NATIVE_SYSTEMCTL_PREFIX =
@ -171,11 +162,7 @@ function formatCommand(command: string, args: string[]): string {
.join(" ");
}
function runChecked(
command: string,
args: string[],
options: SpawnSyncOptions = {}
): void {
function runChecked(command: string, args: string[], options: SpawnSyncOptions = {}): void {
console.log(`$ ${formatCommand(command, args)}`);
const result = spawnSync(command, args, {
cwd: repoRoot,
@ -188,11 +175,7 @@ function runChecked(
}
}
function captureChecked(
command: string,
args: string[],
options: SpawnSyncOptions = {}
): string {
function captureChecked(command: string, args: string[], options: SpawnSyncOptions = {}): string {
const result = spawnSync(command, args, {
cwd: repoRoot,
encoding: "utf8",
@ -225,11 +208,7 @@ function tryCapture(
return result.stdout ?? "";
}
function runRemoteScript(
title: string,
script: string,
args: string[] = []
): void {
function runRemoteScript(title: string, script: string, args: string[] = []): void {
section(title);
if (isLocalServerExecution) {
@ -360,7 +339,9 @@ function assertSshKeyExists(): void {
if (!existsSync(SSH_KEY)) {
console.error(`Missing SSH key: ${SSH_KEY}`);
console.error("Set DEPLOY_SSH_KEY_PATH or run from the live server checkout without DEPLOY_FORCE_SSH.");
console.error(
"Set DEPLOY_SSH_KEY_PATH or run from the live server checkout without DEPLOY_FORCE_SSH."
);
process.exit(1);
}
}
@ -399,10 +380,12 @@ function localGitRemotes(): string[] {
}
function localHasRemote(name: string): boolean {
return spawnSync("git", ["remote", "get-url", name], {
return (
spawnSync("git", ["remote", "get-url", name], {
cwd: repoRoot,
stdio: "ignore"
}).status === 0;
}).status === 0
);
}
function resolveDeployRemote(mode: DeployMode, branch: string | null): string {
@ -444,12 +427,8 @@ function resolveDeployRemote(mode: DeployMode, branch: string | null): string {
return selected;
}
console.error(
`Unable to resolve a deploy git remote. Checked candidates: ${deduped.join(", ")}`
);
console.error(
"Set DEPLOY_GIT_REMOTE to a valid remote name or configure branch.<name>.remote."
);
console.error(`Unable to resolve a deploy git remote. Checked candidates: ${deduped.join(", ")}`);
console.error("Set DEPLOY_GIT_REMOTE to a valid remote name or configure branch.<name>.remote.");
process.exit(1);
}
@ -748,7 +727,9 @@ fi
return;
}
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
const units = nativeUnitsForScope(scope)
.map((value) => shellEscape(value))
.join(" ");
runRemoteScript(
"Remote Runtime Precheck",
`#!/usr/bin/env bash
@ -819,9 +800,7 @@ function remoteDockerRollout(
upArgs.push("--force-recreate");
}
const buildServices = dockerBuildServicesForScope(scope);
const buildCommand = noBuild
? null
: `docker compose build ${buildServices.join(" ")}`;
const buildCommand = noBuild ? null : `docker compose build ${buildServices.join(" ")}`;
const upCommand = `docker compose ${[...upArgs, ...rolloutServices].join(" ")}`;
runRemoteScript(
@ -844,7 +823,9 @@ function remoteNativeRollout(
scope: DeployScope,
noBuild: boolean
): void {
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
const units = nativeUnitsForScope(scope)
.map((value) => shellEscape(value))
.join(" ");
const buildSteps: string[] = [];
if (!noBuild) {
@ -854,7 +835,11 @@ function remoteNativeRollout(
}
}
buildSteps.push(`${NATIVE_SYSTEMCTL_PREFIX} restart ${nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ")}`);
buildSteps.push(
`${NATIVE_SYSTEMCTL_PREFIX} restart ${nativeUnitsForScope(scope)
.map((value) => shellEscape(value))
.join(" ")}`
);
runRemoteScript(
"Remote Rollout",
@ -899,9 +884,7 @@ function remoteDockerVerification(scope: DeployScope, fast: boolean): void {
const psServices = dockerServicesForScope(scope);
const logServices = dockerLogServicesForScope(scope);
const psCommand =
psServices.length > 0
? `docker compose ps ${psServices.join(" ")}`
: "docker compose ps";
psServices.length > 0 ? `docker compose ps ${psServices.join(" ")}` : "docker compose ps";
const logCommand = fast
? `echo '[deploy] Fast mode: skipping docker compose logs tail for quicker feedback.'`
: `docker compose logs --tail=100 ${logServices.join(" ")}`;
@ -933,7 +916,9 @@ ${checks.join("\n")}
}
function remoteNativeVerification(scope: DeployScope, fast: boolean): void {
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
const units = nativeUnitsForScope(scope)
.map((value) => shellEscape(value))
.join(" ");
const checks: string[] = [];
if (scope === "full" || scope === "api" || scope === "services" || scope === "workers") {
@ -941,11 +926,11 @@ function remoteNativeVerification(scope: DeployScope, fast: boolean): void {
}
if (scopeIncludesApi(scope)) {
checks.push('curl -fksS http://127.0.0.1:4000/health');
checks.push("curl -fksS http://127.0.0.1:4000/health");
}
if (scopeIncludesWeb(scope)) {
checks.push('curl -I -fksS http://127.0.0.1:3000/');
checks.push("curl -I -fksS http://127.0.0.1:3000/");
}
runRemoteScript(
@ -962,7 +947,7 @@ fi
declare -a units=(${units})
for unit in "\${units[@]}"; do
${NATIVE_SYSTEMCTL_PREFIX} is-active --quiet "$unit"
${fast ? "echo \"[deploy] Fast mode: skipping unit status and recent journal dump for $unit.\"": `${NATIVE_SYSTEMCTL_PREFIX} status --no-pager "$unit" || true\n journalctl -u "$unit" -n 50 --no-pager || true`}
${fast ? 'echo "[deploy] Fast mode: skipping unit status and recent journal dump for $unit."' : `${NATIVE_SYSTEMCTL_PREFIX} status --no-pager "$unit" || true\n journalctl -u "$unit" -n 50 --no-pager || true`}
done
${checks.join("\n")}
`
@ -1074,9 +1059,7 @@ function main(): void {
timedPhase(timings, "remote verification", () =>
remoteVerification(options.runtime, scope, options.fast)
);
timedPhase(timings, "public verification", () =>
publicVerification(scope, options.fast)
);
timedPhase(timings, "public verification", () => publicVerification(scope, options.fast));
printTimingSummary(timings);
}

View file

@ -6,7 +6,7 @@ const outputFile = path.join(docsDir, "index.html");
const dateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeStyle: "short"
});
function escapeHtml(value) {
@ -71,7 +71,7 @@ async function collectDocsFiles(rootDir, currentDir = rootDir, acc = []) {
relativePath,
category: relativePath.includes("/") ? relativePath.split("/")[0] : "root",
sizeBytes: stats.size,
modifiedAt: stats.mtime,
modifiedAt: stats.mtime
});
}
}

View file

@ -4,11 +4,7 @@ import path from "node:path";
const repoRoot = path.resolve(import.meta.dir, "..");
const deploymentRoot = path.join(repoRoot, "deployment/docker/workspace-root");
const filesToSync = [
"package.json",
"bun.lock",
"tsconfig.base.json"
] as const;
const filesToSync = ["package.json", "bun.lock", "tsconfig.base.json"] as const;
for (const fileName of filesToSync) {
const source = path.join(repoRoot, fileName);
@ -16,4 +12,3 @@ for (const fileName of filesToSync) {
await copyFile(source, destination);
console.log(`synced ${fileName}`);
}

View file

@ -33,15 +33,30 @@ if (tsconfigs.length === 0) {
}
let failed = false;
const bunExecutable = process.execPath;
for (const tsconfig of tsconfigs) {
const label = relative(process.cwd(), tsconfig);
console.log(`\nTypechecking ${label}`);
const result = Bun.spawnSync(["bunx", "tsc", "-p", tsconfig, "--noEmit", "--incremental", "false", "--pretty", "false"], {
const result = Bun.spawnSync(
[
bunExecutable,
"x",
"tsc",
"-p",
tsconfig,
"--noEmit",
"--incremental",
"false",
"--pretty",
"false"
],
{
stdout: "inherit",
stderr: "inherit"
});
}
);
if (result.exitCode !== 0) {
failed = true;

View file

@ -465,8 +465,7 @@ const parseCandleParams = (
const endTs = params.end_ts ?? Date.now();
const limit = params.limit ?? env.REST_DEFAULT_LIMIT;
const startTs =
params.start_ts ?? Math.max(0, Math.floor(endTs - params.interval_ms * limit));
const startTs = params.start_ts ?? Math.max(0, Math.floor(endTs - params.interval_ms * limit));
const rangeStart = Math.min(startTs, endTs);
const rangeEnd = Math.max(startTs, endTs);
@ -482,7 +481,13 @@ const parseCandleParams = (
const parseCandleReplayParams = (
url: URL
): { underlyingId: string; intervalMs: number; afterTs: number; afterSeq: number; limit: number } => {
): {
underlyingId: string;
intervalMs: number;
afterTs: number;
afterSeq: number;
limit: number;
} => {
const params = candleReplaySchema.parse({
underlying_id: url.searchParams.get("underlying_id") ?? undefined,
interval_ms: url.searchParams.get("interval_ms") ?? undefined,
@ -601,7 +606,10 @@ const matchesScopedOptionSubscription = (
print: { underlying_id?: string; option_contract_id: string },
subscription: Extract<LiveSubscription, { channel: "options" }>
): boolean => {
if (subscription.option_contract_id && subscription.option_contract_id !== print.option_contract_id) {
if (
subscription.option_contract_id &&
subscription.option_contract_id !== print.option_contract_id
) {
return false;
}
if (subscription.underlying_ids?.length) {
@ -693,8 +701,7 @@ const run = async () => {
env.OPTIONS_INGEST_ADAPTER,
env.EQUITIES_INGEST_ADAPTER
);
const syntheticBackendDisabledReason =
getSyntheticBackendDisabledReason(syntheticBackendMode);
const syntheticBackendDisabledReason = getSyntheticBackendDisabledReason(syntheticBackendMode);
const syntheticControlKv = await openSyntheticControlKv(js);
let syntheticControl = await ensureSyntheticControlState(syntheticControlKv);
const syntheticProfileHits = createRollingSyntheticProfileHits();
@ -899,11 +906,7 @@ const run = async () => {
}
}
const subscribeWithReset = async <T>(
subject: string,
stream: string,
durableName: string
) => {
const subscribeWithReset = async <T>(subject: string, stream: string, durableName: string) => {
const opts = buildDurableConsumer(durableName);
applyDeliverPolicy(opts, env.API_DELIVER_POLICY);
try {
@ -924,7 +927,8 @@ const run = async () => {
try {
await jsm.consumers.delete(stream, durableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: durableName,
@ -1023,8 +1027,12 @@ const run = async () => {
}
const matchingSubscriptions =
subscription.channel === "options" || subscription.channel === "flow" || subscription.channel === "equities"
? [...subscriptionDefinitions.entries()].filter(([, candidate]) => candidate.channel === subscription.channel)
subscription.channel === "options" ||
subscription.channel === "flow" ||
subscription.channel === "equities"
? [...subscriptionDefinitions.entries()].filter(
([, candidate]) => candidate.channel === subscription.channel
)
: [[getSubscriptionKey(subscription), subscription] as const];
if (matchingSubscriptions.length === 0) {
@ -1032,8 +1040,12 @@ const run = async () => {
}
const optionItem = ingestChannel === "options" ? (item as OptionPrint) : null;
const equityItem = ingestChannel === "equities" ? (item as Parameters<typeof matchesScopedEquitySubscription>[0]) : null;
const flowItem = ingestChannel === "flow" ? (item as Parameters<typeof matchesFlowPacketFilters>[0]) : null;
const equityItem =
ingestChannel === "equities"
? (item as Parameters<typeof matchesScopedEquitySubscription>[0])
: null;
const flowItem =
ingestChannel === "flow" ? (item as Parameters<typeof matchesFlowPacketFilters>[0]) : null;
let matchedSubscriptions = 0;
for (const [key, candidate] of matchingSubscriptions) {
@ -1315,9 +1327,7 @@ const run = async () => {
},
control: syntheticBackendMode === "synthetic" ? syntheticControl : null,
derived,
...(syntheticBackendDisabledReason
? { disabled_reason: syntheticBackendDisabledReason }
: {})
...(syntheticBackendDisabledReason ? { disabled_reason: syntheticBackendDisabledReason } : {})
};
};
@ -1385,11 +1395,7 @@ const run = async () => {
syntheticControl = await writeSyntheticControlState(syntheticControlKv, payload);
return jsonResponse({
control: syntheticControl,
derived: buildSyntheticDerivedStatus(
Date.now(),
syntheticControl,
syntheticProfileHits
)
derived: buildSyntheticDerivedStatus(Date.now(), syntheticControl, syntheticProfileHits)
});
} catch (error) {
return jsonResponse(
@ -1436,7 +1442,13 @@ const run = async () => {
if (req.method === "GET" && url.pathname === "/prints/equities/range") {
try {
const { underlyingId, startTs, endTs, limit } = parseEquityPrintRangeParams(url);
const data = await fetchEquityPrintsRange(clickhouse, underlyingId, startTs, endTs, limit);
const data = await fetchEquityPrintsRange(
clickhouse,
underlyingId,
startTs,
endTs,
limit
);
return jsonResponse({ data });
} catch (error) {
return jsonResponse(
@ -1566,7 +1578,9 @@ const run = async () => {
source,
storageFilters
);
return jsonResponse(buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq })));
return jsonResponse(
buildHistoryResponse(data, (item) => ({ ts: item.ts, seq: item.seq }))
);
} catch (error) {
return jsonResponse(
{
@ -1986,7 +2000,9 @@ const run = async () => {
const payload =
typeof message === "string"
? message
: new TextDecoder().decode(message instanceof Uint8Array ? message : new Uint8Array(message));
: new TextDecoder().decode(
message instanceof Uint8Array ? message : new Uint8Array(message)
);
const parsed = LiveClientMessageSchema.parse(JSON.parse(payload));
if (parsed.op === "ping") {
sendLiveMessage(socket, {

View file

@ -165,11 +165,21 @@ const parseGenericLimitFallback = (env: NodeJS.ProcessEnv, fallback: number): nu
return Math.max(MIN_GENERIC_LIMIT, Math.min(MAX_GENERIC_LIMIT, Math.floor(parsed)));
};
export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env): GenericLiveLimits => {
export const resolveGenericLiveLimits = (
env: NodeJS.ProcessEnv = process.env
): GenericLiveLimits => {
const liveLimitDefault = parseGenericLimitFallback(env, DEFAULT_GENERIC_LIMIT);
return {
options: parseGenericLimit(env, "options", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.options),
nbbo: parseGenericLimit(env, "nbbo", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.nbbo),
options: parseGenericLimit(
env,
"options",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.options
),
nbbo: parseGenericLimit(
env,
"nbbo",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.nbbo
),
equities: parseGenericLimit(
env,
"equities",
@ -185,7 +195,11 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
"equity-joins",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["equity-joins"]
),
flow: parseGenericLimit(env, "flow", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.flow),
flow: parseGenericLimit(
env,
"flow",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.flow
),
"smart-money": parseGenericLimit(
env,
"smart-money",
@ -196,13 +210,21 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
"classifier-hits",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["classifier-hits"]
),
alerts: parseGenericLimit(env, "alerts", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.alerts),
alerts: parseGenericLimit(
env,
"alerts",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.alerts
),
"inferred-dark": parseGenericLimit(
env,
"inferred-dark",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["inferred-dark"]
),
news: parseGenericLimit(env, "news", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.news)
news: parseGenericLimit(
env,
"news",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.news
)
};
};
@ -227,12 +249,18 @@ const extractFreshnessTs = (channel: LiveGenericChannel, item: any): number | nu
export const resolveLiveStateConfig = (env: NodeJS.ProcessEnv = process.env): LiveStateConfig => ({
limits: resolveGenericLiveLimits(env),
scopedCacheMaxKeys: parsePositiveInt(env.LIVE_SCOPED_CACHE_MAX_KEYS, DEFAULT_SCOPED_CACHE_MAX_KEYS),
scopedCacheMaxKeys: parsePositiveInt(
env.LIVE_SCOPED_CACHE_MAX_KEYS,
DEFAULT_SCOPED_CACHE_MAX_KEYS
),
redisFlushIntervalMs: parsePositiveInt(
env.LIVE_REDIS_FLUSH_INTERVAL_MS,
DEFAULT_REDIS_FLUSH_INTERVAL_MS
),
redisFlushMaxItems: parsePositiveInt(env.LIVE_REDIS_FLUSH_MAX_ITEMS, DEFAULT_REDIS_FLUSH_MAX_ITEMS)
redisFlushMaxItems: parsePositiveInt(
env.LIVE_REDIS_FLUSH_MAX_ITEMS,
DEFAULT_REDIS_FLUSH_MAX_ITEMS
)
});
const parsePositiveInt = (value: string | undefined, fallback: number): number => {
const parsed = Number(value);
@ -242,10 +270,7 @@ const parsePositiveInt = (value: string | undefined, fallback: number): number =
return Math.max(1, Math.floor(parsed));
};
type RedisLike = Pick<
RedisClientType,
"isOpen" | "lRange" | "lPush" | "lTrim" | "hGet" | "hSet"
>;
type RedisLike = Pick<RedisClientType, "isOpen" | "lRange" | "lPush" | "lTrim" | "hGet" | "hSet">;
const parseCursor = (value: string | null): Cursor | null => {
if (!value) {
@ -259,7 +284,9 @@ const parseCursor = (value: string | null): Cursor | null => {
}
};
const getGenericConfig = (limits: GenericLiveLimits): {
const getGenericConfig = (
limits: GenericLiveLimits
): {
[K in LiveGenericChannel]: GenericFeedConfig;
} => ({
options: {
@ -365,7 +392,7 @@ const parseJsonList = <T>(payloads: string[], parse: (value: unknown) => T): T[]
return items;
};
const compareCursors = (a: Cursor, b: Cursor): number => (b.ts - a.ts) || (b.seq - a.seq);
const compareCursors = (a: Cursor, b: Cursor): number => b.ts - a.ts || b.seq - a.seq;
const sortGenericItems = <T>(items: T[], cursorOf: (item: T) => Cursor): T[] =>
[...items].sort((a, b) => compareCursors(cursorOf(a), cursorOf(b)));
@ -480,7 +507,10 @@ const matchesScopedOptionSnapshot = (
return false;
}
if (subscription.option_contract_id && item.option_contract_id !== subscription.option_contract_id) {
if (
subscription.option_contract_id &&
item.option_contract_id !== subscription.option_contract_id
) {
return false;
}
@ -529,11 +559,8 @@ const candleCursorField = (underlyingId: string, intervalMs: number): string =>
const overlayRedisKey = (underlyingId: string): string => `live:equity-overlay:${underlyingId}`;
const overlayCursorField = (underlyingId: string): string => `equities:${underlyingId}`;
const dropMatchingCursor = <T>(
items: T[],
target: Cursor,
cursorOf: (item: T) => Cursor
): T[] => items.filter((item) => compareCursors(cursorOf(item), target) !== 0);
const dropMatchingCursor = <T>(items: T[], target: Cursor, cursorOf: (item: T) => Cursor): T[] =>
items.filter((item) => compareCursors(cursorOf(item), target) !== 0);
const insertNewestFirst = <T>(
items: T[],
@ -676,7 +703,13 @@ export class LiveStateManager {
this.pendingRedisWrites.clear();
for (const write of writes) {
await this.persistList(write.listKey, write.cursorField, write.items, write.limit, write.cursor);
await this.persistList(
write.listKey,
write.cursorField,
write.items,
write.limit,
write.cursor
);
this.stats.redisFlushCount += 1;
this.stats.redisFlushItems += write.items.length;
metrics.count("api.live.redis_flush_count", 1);
@ -726,7 +759,12 @@ export class LiveStateManager {
}
}
private updateFreshnessMetric(listKey: string, channel: LiveChannel, item: unknown, now = Date.now()): void {
private updateFreshnessMetric(
listKey: string,
channel: LiveChannel,
item: unknown,
now = Date.now()
): void {
const ts =
channel === "equity-candles" || channel === "equity-overlay"
? typeof (item as { ts?: unknown })?.ts === "number"
@ -784,12 +822,22 @@ export class LiveStateManager {
config.cursorField,
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, config.cursorField))
);
await this.persistList(config.redisKey, config.cursorField, cached, config.limit, this.genericCursors.get(config.cursorField) ?? null);
await this.persistList(
config.redisKey,
config.cursorField,
cached,
config.limit,
this.genericCursors.get(config.cursorField) ?? null
);
return;
}
}
const fresh = normalizeGenericItems(channel, await config.fetchRecent(this.clickhouse, config.limit), config);
const fresh = normalizeGenericItems(
channel,
await config.fetchRecent(this.clickhouse, config.limit),
config
);
this.stats.genericHydrateFromClickHouse += 1;
this.stats.cacheDepthByKey.set(config.redisKey, fresh.length);
this.genericItems.set(channel, fresh);
@ -806,7 +854,8 @@ export class LiveStateManager {
case "options": {
const config = this.generic.options;
const limit = snapshotLimitFor(subscription, config.limit);
const scoped = Boolean(subscription.underlying_ids?.length) || Boolean(subscription.option_contract_id);
const scoped =
Boolean(subscription.underlying_ids?.length) || Boolean(subscription.option_contract_id);
if (subscription.filters?.view === "raw" || scoped) {
const cached = (this.genericItems.get("options") ?? [])
.filter((entry) => matchesScopedOptionSnapshot(entry, subscription))
@ -815,8 +864,16 @@ export class LiveStateManager {
if (cached.length < limit) {
this.stats.scopedClickHouseSnapshots += 1;
const storageFilters = buildOptionSnapshotFilters(subscription);
const backfill = await fetchRecentOptionPrints(this.clickhouse, limit, undefined, storageFilters);
items = mergeSnapshotBackfill(cached, backfill, limit, (entry) => ({ ts: entry.ts, seq: entry.seq }));
const backfill = await fetchRecentOptionPrints(
this.clickhouse,
limit,
undefined,
storageFilters
);
items = mergeSnapshotBackfill(cached, backfill, limit, (entry) => ({
ts: entry.ts,
seq: entry.seq
}));
}
return {
subscription,
@ -942,7 +999,11 @@ export class LiveStateManager {
this.candleItems.set(key, nextState.items);
this.candleCursors.set(cursorField, cursor);
this.touchAccess(this.candleAccess, key);
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
this.evictScopedCachesIfNeeded(
this.candleItems as Map<string, unknown[]>,
this.candleCursors,
this.candleAccess
);
if (nextState.outOfOrder) {
this.stats.outOfOrderEvents += 1;
metrics.count("api.live.out_of_order_events", 1);
@ -968,7 +1029,11 @@ export class LiveStateManager {
this.overlayItems.set(key, nextState.items);
this.overlayCursors.set(cursorField, cursor);
this.touchAccess(this.overlayAccess, key);
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
this.evictScopedCachesIfNeeded(
this.overlayItems as Map<string, unknown[]>,
this.overlayCursors,
this.overlayAccess
);
if (nextState.outOfOrder) {
this.stats.outOfOrderEvents += 1;
metrics.count("api.live.out_of_order_events", 1);
@ -991,10 +1056,19 @@ export class LiveStateManager {
const nextState =
channel === "nbbo"
? {
items: normalizeGenericItems(channel, [parsed, ...(this.genericItems.get(channel) ?? [])], config),
items: normalizeGenericItems(
channel,
[parsed, ...(this.genericItems.get(channel) ?? [])],
config
),
outOfOrder: false
}
: insertNewestFirst(this.genericItems.get(channel) ?? [], parsed, config.cursor, config.limit);
: insertNewestFirst(
this.genericItems.get(channel) ?? [],
parsed,
config.cursor,
config.limit
);
if (nextState.outOfOrder) {
this.stats.outOfOrderEvents += 1;
@ -1007,7 +1081,13 @@ export class LiveStateManager {
if (nextState.items.length > 0) {
this.updateFreshnessMetric(config.redisKey, channel, nextState.items[0]);
}
this.queueRedisWrite(config.redisKey, config.cursorField, nextState.items, config.limit, cursor);
this.queueRedisWrite(
config.redisKey,
config.cursorField,
nextState.items,
config.limit,
cursor
);
return cursor;
}
}
@ -1022,18 +1102,34 @@ export class LiveStateManager {
if (cached.length > 0) {
this.candleItems.set(key, cached);
this.touchAccess(this.candleAccess, key);
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
this.evictScopedCachesIfNeeded(
this.candleItems as Map<string, unknown[]>,
this.candleCursors,
this.candleAccess
);
this.stats.cacheDepthByKey.set(key, cached.length);
this.updateFreshnessMetric(key, "equity-candles", cached[0]);
this.candleCursors.set(cursorField, parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField)));
this.candleCursors.set(
cursorField,
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField))
);
return;
}
}
const fresh = await fetchRecentEquityCandles(this.clickhouse, underlyingId, intervalMs, CHART_LIMITS.candles);
const fresh = await fetchRecentEquityCandles(
this.clickhouse,
underlyingId,
intervalMs,
CHART_LIMITS.candles
);
this.candleItems.set(key, fresh);
this.touchAccess(this.candleAccess, key);
this.evictScopedCachesIfNeeded(this.candleItems as Map<string, unknown[]>, this.candleCursors, this.candleAccess);
this.evictScopedCachesIfNeeded(
this.candleItems as Map<string, unknown[]>,
this.candleCursors,
this.candleAccess
);
this.stats.cacheDepthByKey.set(key, fresh.length);
if (fresh.length > 0) {
this.updateFreshnessMetric(key, "equity-candles", fresh[0]);
@ -1052,10 +1148,17 @@ export class LiveStateManager {
if (cached.length > 0) {
this.overlayItems.set(key, cached);
this.touchAccess(this.overlayAccess, key);
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
this.evictScopedCachesIfNeeded(
this.overlayItems as Map<string, unknown[]>,
this.overlayCursors,
this.overlayAccess
);
this.stats.cacheDepthByKey.set(key, cached.length);
this.updateFreshnessMetric(key, "equity-overlay", cached[0]);
this.overlayCursors.set(cursorField, parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField)));
this.overlayCursors.set(
cursorField,
parseCursor(await this.redis.hGet(CURSOR_HASH_KEY, cursorField))
);
return;
}
}
@ -1065,7 +1168,11 @@ export class LiveStateManager {
);
this.overlayItems.set(key, fresh);
this.touchAccess(this.overlayAccess, key);
this.evictScopedCachesIfNeeded(this.overlayItems as Map<string, unknown[]>, this.overlayCursors, this.overlayAccess);
this.evictScopedCachesIfNeeded(
this.overlayItems as Map<string, unknown[]>,
this.overlayCursors,
this.overlayAccess
);
this.stats.cacheDepthByKey.set(key, fresh.length);
if (fresh.length > 0) {
this.updateFreshnessMetric(key, "equity-overlay", fresh[0]);

View file

@ -83,11 +83,7 @@ export const buildSyntheticDerivedStatus = (
session_phase: session.session_phase,
regime: session.regime,
focus_symbols: session.focus_symbols,
profile_hit_counts: getSyntheticProfileHitCounts(
state,
now,
control.coverage_window_minutes
),
profile_hit_counts: getSyntheticProfileHitCounts(state, now, control.coverage_window_minutes),
coverage_window_minutes: control.coverage_window_minutes
});
};

View file

@ -3,7 +3,9 @@ import { isAlertContextPath, parseAlertContextTraceIdPath } from "../src/alert-c
describe("alert context route helpers", () => {
it("extracts a valid alert trace id from the context endpoint path", () => {
expect(parseAlertContextTraceIdPath("/flow/alerts/alert%3Actx%2Fone/context")).toBe("alert:ctx/one");
expect(parseAlertContextTraceIdPath("/flow/alerts/alert%3Actx%2Fone/context")).toBe(
"alert:ctx/one"
);
});
it("returns null for unrelated alert paths", () => {

View file

@ -9,9 +9,7 @@ import {
shouldFanoutLiveEvent
} from "../src/live";
const makeClickHouse = (
queryResolver?: (query: string) => unknown[]
): ClickHouseClient =>
const makeClickHouse = (queryResolver?: (query: string) => unknown[]): ClickHouseClient =>
({
exec: async () => {},
insert: async () => {},
@ -149,10 +147,7 @@ describe("LiveStateManager", () => {
it("trims generic windows to configured per-channel limits", async () => {
const redis = makeRedis();
const now = Date.now();
const manager = new LiveStateManager(
makeClickHouse(),
redis as never,
{
const manager = new LiveStateManager(makeClickHouse(), redis as never, {
options: 10000,
nbbo: 10000,
equities: 10000,
@ -163,8 +158,7 @@ describe("LiveStateManager", () => {
"classifier-hits": 10000,
alerts: 10000,
"inferred-dark": 10000
}
);
});
await manager.ingest("flow", {
source_ts: now,
@ -503,18 +497,15 @@ describe("LiveStateManager", () => {
manager.getSnapshot({ channel: "flow" })
]);
expect((optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
"opt-fresh",
"opt-stale"
]);
expect((nbboSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
"nbbo-fresh",
"nbbo-stale"
]);
expect((equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
"eq-fresh",
"eq-stale"
]);
expect(
(optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
).toEqual(["opt-fresh", "opt-stale"]);
expect(
(nbboSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
).toEqual(["nbbo-fresh", "nbbo-stale"]);
expect(
(equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
).toEqual(["eq-fresh", "eq-stale"]);
expect((flowSnapshot.items as Array<{ id: string }>).map((item) => item.id)).toEqual([
"flow-fresh",
"flow-stale"
@ -699,10 +690,9 @@ describe("LiveStateManager", () => {
option_contract_id: "AAPL-2025-01-17-200-C"
});
expect((snapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id).slice(0, 2)).toEqual([
"opt-hot",
"opt-backfill"
]);
expect(
(snapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id).slice(0, 2)
).toEqual(["opt-hot", "opt-backfill"]);
});
it("seeds scoped equity snapshots from clickhouse rows older than 24h", async () => {
@ -806,12 +796,12 @@ describe("LiveStateManager", () => {
manager.getSnapshot({ channel: "flow" })
]);
expect((optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
"opt-retained"
]);
expect((equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)).toEqual([
"eq-retained"
]);
expect(
(optionsSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
).toEqual(["opt-retained"]);
expect(
(equitiesSnapshot.items as Array<{ trace_id: string }>).map((item) => item.trace_id)
).toEqual(["eq-retained"]);
expect((flowSnapshot.items as Array<{ id: string }>).map((item) => item.id)).toEqual([
"flow-retained"
]);
@ -1047,7 +1037,10 @@ describe("LiveStateManager", () => {
});
it("tracks generic cache and scoped clickhouse snapshot sources separately", async () => {
const manager = new LiveStateManager(makeClickHouse(() => []), null);
const manager = new LiveStateManager(
makeClickHouse(() => []),
null
);
const now = Date.now();
await manager.ingest("options", {
@ -1075,7 +1068,10 @@ describe("LiveStateManager", () => {
});
it("keeps backend channel health healthy when a scoped query is quiet", async () => {
const manager = new LiveStateManager(makeClickHouse(() => []), null);
const manager = new LiveStateManager(
makeClickHouse(() => []),
null
);
const now = Date.now();
await manager.ingest("options", {
@ -1098,7 +1094,9 @@ describe("LiveStateManager", () => {
expect(quietSnapshot.items).toEqual([]);
expect(manager.getHotChannelHealth().options.healthy).toBe(true);
expect(manager.getStatsSnapshot().freshnessAgeMsByKey[HOT_LIVE_REDIS_KEYS.options]).toBeLessThanOrEqual(50);
expect(
manager.getStatsSnapshot().freshnessAgeMsByKey[HOT_LIVE_REDIS_KEYS.options]
).toBeLessThanOrEqual(50);
});
it("exposes freshness helper for feed status", () => {

View file

@ -33,9 +33,7 @@ const envSchema = z.object({
CANDLE_INTERVALS_MS: z.string().default("60000,300000"),
CANDLE_MAX_LATE_MS: z.coerce.number().int().nonnegative().default(0),
CANDLE_CACHE_LIMIT: z.coerce.number().int().nonnegative().default(2000),
CANDLE_DELIVER_POLICY: z
.enum(["new", "all", "last", "last_per_subject"])
.default("new"),
CANDLE_DELIVER_POLICY: z.enum(["new", "all", "last", "last_per_subject"]).default("new"),
CANDLE_CONSUMER_RESET: z
.preprocess((value) => {
if (typeof value === "string") {
@ -290,7 +288,10 @@ const run = async () => {
} else {
try {
const info = await jsm.consumers.info(STREAM_EQUITY_PRINTS, durableName);
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.CANDLE_DELIVER_POLICY) {
if (
info?.config?.deliver_policy &&
info.config.deliver_policy !== env.CANDLE_DELIVER_POLICY
) {
logger.warn("resetting consumer due to deliver policy change", {
durable: durableName,
current: info.config.deliver_policy,
@ -301,7 +302,10 @@ const run = async () => {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("not found")) {
logger.warn("failed to inspect jetstream consumer", { durable: durableName, error: message });
logger.warn("failed to inspect jetstream consumer", {
durable: durableName,
error: message
});
}
}
}
@ -327,7 +331,8 @@ const run = async () => {
try {
await jsm.consumers.delete(STREAM_EQUITY_PRINTS, durableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: durableName,

View file

@ -14,4 +14,3 @@ export const scoreAlert = (
const severity = score >= 80 ? "high" : score >= 45 ? "medium" : "low";
return { score, severity };
};

View file

@ -573,10 +573,7 @@ const buildVerticalSpreadHit = (
};
};
const buildLadderHit = (
packet: FlowPacket,
config: ClassifierConfig
): ClassifierHit | null => {
const buildLadderHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierHit | null => {
const structureType = getStringFeature(packet, "structure_type");
if (structureType !== "ladder") {
return null;
@ -648,7 +645,8 @@ const buildRollHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierH
}
const activity = getLargeActivity(packet, config);
const qualifies = activity.totalPremium >= config.spikeMinPremium || activity.totalSize >= config.spikeMinSize;
const qualifies =
activity.totalPremium >= config.spikeMinPremium || activity.totalSize >= config.spikeMinSize;
if (!qualifies) {
return null;
}
@ -708,7 +706,9 @@ const buildRollHit = (packet: FlowPacket, config: ClassifierConfig): ClassifierH
const expiryNote = hasExpiryPair
? `Expiries: ${fromExpiry} -> ${toExpiry}${
expiryDaysDelta !== null && expiryDaysDelta !== 0 ? ` (${Math.round(expiryDaysDelta)}d)` : ""
expiryDaysDelta !== null && expiryDaysDelta !== 0
? ` (${Math.round(expiryDaysDelta)}d)`
: ""
}.`
: "Expiry pairing unavailable.";
const strikeNote = hasStrikePair
@ -850,7 +850,8 @@ export const evaluateClassifiers = (
const packetKind = getStringFeature(packet, "packet_kind");
const structureOnly = packetKind === "structure";
const contractId = typeof packet.features.option_contract_id === "string"
const contractId =
typeof packet.features.option_contract_id === "string"
? packet.features.option_contract_id
: "";
const contract = structureOnly ? null : parseContractId(contractId);

View file

@ -15,10 +15,7 @@ const roundTo = (value: number, digits = 4): number => {
return Number(value.toFixed(digits));
};
export const classifyQuotePlacement = (
price: number,
join: EquityQuoteJoin
): QuotePlacement => {
export const classifyQuotePlacement = (price: number, join: EquityQuoteJoin): QuotePlacement => {
if (!Number.isFinite(price)) {
return "MISSING";
}

View file

@ -46,7 +46,7 @@ import {
enqueueEquityPrintJoinInsert,
enqueueFlowPacketInsert,
enqueueInferredDarkInsert,
enqueueSmartMoneyEventInsert,
enqueueSmartMoneyEventInsert
} from "@islandflow/storage";
import {
AlertEventSchema,
@ -324,7 +324,9 @@ const buildPacketId = (cluster: ClusterState): string => {
const isExpectedShutdownNatsError = (error: unknown): boolean => {
const code = getErrorCode(error);
return runtimeState.shuttingDown && (code === "CONNECTION_DRAINING" || code === "CONNECTION_CLOSED");
return (
runtimeState.shuttingDown && (code === "CONNECTION_DRAINING" || code === "CONNECTION_CLOSED")
);
};
const createPlacementCounts = (): NbboPlacementCounts => ({
@ -337,7 +339,14 @@ const createPlacementCounts = (): NbboPlacementCounts => ({
stale: 0
});
const SPECIAL_PRINT_CONDITIONS = new Set(["AUCTION", "CROSS", "OPENING", "CLOSING", "COMPLEX", "SPREAD"]);
const SPECIAL_PRINT_CONDITIONS = new Set([
"AUCTION",
"CROSS",
"OPENING",
"CLOSING",
"COMPLEX",
"SPREAD"
]);
const SYNTHETIC_EVENT_CONDITION_RE = /^EVENT_(\d+)D$/i;
const normalizeConditions = (conditions: readonly string[] | undefined): string[] =>
@ -460,11 +469,7 @@ const storeRecentRootLeg = (leg: LegEvidence, anchorTs: number): void => {
recentLegsByRoot.set(key, next);
};
const collectActiveLegs = (
key: string,
anchorTs: number,
excludeId: string
): LegEvidence[] => {
const collectActiveLegs = (key: string, anchorTs: number, excludeId: string): LegEvidence[] => {
const legs: LegEvidence[] = [];
for (const [contractId, cluster] of clusters) {
if (contractId === excludeId) {
@ -485,11 +490,7 @@ const collectActiveLegs = (
return legs;
};
const collectActiveRootLegs = (
key: string,
anchorTs: number,
excludeId: string
): LegEvidence[] => {
const collectActiveRootLegs = (key: string, anchorTs: number, excludeId: string): LegEvidence[] => {
const legs: LegEvidence[] = [];
for (const [contractId, cluster] of clusters) {
if (contractId === excludeId) {
@ -601,12 +602,19 @@ const applyDeliverPolicy = (
const buildCluster = (print: OptionPrint): ClusterState => {
const placements = createPlacementCounts();
const normalizedConditions = normalizeConditions(print.conditions);
const executionIv = typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv) ? print.execution_iv : null;
const executionIv =
typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)
? print.execution_iv
: null;
const executionUnderlyingMid =
typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)
typeof print.execution_underlying_mid === "number" &&
Number.isFinite(print.execution_underlying_mid)
? print.execution_underlying_mid
: null;
recordPlacement(placements, classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts)));
recordPlacement(
placements,
classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts))
);
return {
contractId: print.option_contract_id,
underlyingId: print.underlying_id ?? null,
@ -661,11 +669,18 @@ const updateCluster = (cluster: ClusterState, print: OptionPrint): ClusterState
if (typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)) {
cluster.lastExecutionIv = print.execution_iv;
cluster.minExecutionIv =
cluster.minExecutionIv === null ? print.execution_iv : Math.min(cluster.minExecutionIv, print.execution_iv);
cluster.minExecutionIv === null
? print.execution_iv
: Math.min(cluster.minExecutionIv, print.execution_iv);
cluster.maxExecutionIv =
cluster.maxExecutionIv === null ? print.execution_iv : Math.max(cluster.maxExecutionIv, print.execution_iv);
cluster.maxExecutionIv === null
? print.execution_iv
: Math.max(cluster.maxExecutionIv, print.execution_iv);
}
if (typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)) {
if (
typeof print.execution_underlying_mid === "number" &&
Number.isFinite(print.execution_underlying_mid)
) {
if (cluster.firstUnderlyingMid === null) {
cluster.firstUnderlyingMid = print.execution_underlying_mid;
}
@ -686,11 +701,7 @@ type NbboJoin = {
const updateNbboCache = (nbbo: OptionNBBO): void => {
const existing = nbboCache.get(nbbo.option_contract_id);
if (
!existing ||
nbbo.ts > existing.ts ||
(nbbo.ts === existing.ts && nbbo.seq >= existing.seq)
) {
if (!existing || nbbo.ts > existing.ts || (nbbo.ts === existing.ts && nbbo.seq >= existing.seq)) {
nbboCache.set(nbbo.option_contract_id, nbbo);
nbboCacheTouchedAt.set(nbbo.option_contract_id, Date.now());
}
@ -907,14 +918,18 @@ const flushCluster = async (
features.special_print_count = cluster.specialPrintCount;
}
if (cluster.minExecutionIv !== null && cluster.maxExecutionIv !== null) {
features.execution_iv_shock = roundTo(Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv));
features.execution_iv_shock = roundTo(
Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv)
);
}
if (
cluster.firstUnderlyingMid !== null &&
cluster.lastUnderlyingMid !== null &&
cluster.firstUnderlyingMid > 0
) {
const moveBps = ((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) * 10_000;
const moveBps =
((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) *
10_000;
features.underlying_move_bps = roundTo(moveBps);
}
const syntheticEventOffsetDays = parseSyntheticEventOffsetDays(cluster.conditions);
@ -1004,7 +1019,13 @@ const flushCluster = async (
const rollLegs = [currentLeg, ...rootCandidates];
const rollSummary = summarizeStructure(rollLegs);
if (rollSummary?.type === "roll") {
await emitStructurePacketIfNeeded(js, batchWriter, rollLegs, rollSummary, currentLeg.contractId);
await emitStructurePacketIfNeeded(
js,
batchWriter,
rollLegs,
rollSummary,
currentLeg.contractId
);
}
storeRecentLeg(currentLeg, anchorTs);
@ -1072,13 +1093,21 @@ const emitClassifiers = async (
const underlyingId =
typeof packet.features.underlying_id === "string"
? packet.features.underlying_id
: parseContractId(typeof packet.features.option_contract_id === "string" ? packet.features.option_contract_id : "")?.root;
: parseContractId(
typeof packet.features.option_contract_id === "string"
? packet.features.option_contract_id
: ""
)?.root;
const referenceTs =
typeof packet.features.end_ts === "number" && Number.isFinite(packet.features.end_ts)
? packet.features.end_ts
: packet.source_ts;
const eventCalendarMatch = underlyingId ? eventCalendarProvider.findNextEvent(underlyingId, referenceTs) : null;
smartMoneyEvent = SmartMoneyEventSchema.parse(buildSmartMoneyEventFromPacket(packet, { eventCalendarMatch }));
const eventCalendarMatch = underlyingId
? eventCalendarProvider.findNextEvent(underlyingId, referenceTs)
: null;
smartMoneyEvent = SmartMoneyEventSchema.parse(
buildSmartMoneyEventFromPacket(packet, { eventCalendarMatch })
);
enqueueSmartMoneyEventInsert(batchWriter, smartMoneyEvent);
await publishJson(js, SUBJECT_SMART_MONEY_EVENTS, smartMoneyEvent);
emitCounters.smartMoneyEvents += 1;
@ -1282,20 +1311,29 @@ const run = async () => {
if (env.SMART_MONEY_EVENT_CALENDAR_PATH) {
try {
eventCalendarProvider = await loadEventCalendarProviderFromFile(env.SMART_MONEY_EVENT_CALENDAR_PATH);
logger.info("smart money event calendar loaded", { path: env.SMART_MONEY_EVENT_CALENDAR_PATH });
eventCalendarProvider = await loadEventCalendarProviderFromFile(
env.SMART_MONEY_EVENT_CALENDAR_PATH
);
logger.info("smart money event calendar loaded", {
path: env.SMART_MONEY_EVENT_CALENDAR_PATH
});
} catch (error) {
eventCalendarProvider = createEmptyEventCalendarProvider();
logger.warn("smart money event calendar unavailable; scoring will use neutral event features", {
logger.warn(
"smart money event calendar unavailable; scoring will use neutral event features",
{
path: env.SMART_MONEY_EVENT_CALENDAR_PATH,
error: error instanceof Error ? error.message : String(error)
});
}
);
}
}
const redis = createRedisClient(env.REDIS_URL);
redis.on("error", (error) => {
logger.warn("redis client error", { error: error instanceof Error ? error.message : String(error) });
logger.warn("redis client error", {
error: error instanceof Error ? error.message : String(error)
});
});
await retry("redis connect", 120, 500, async () => {
@ -1379,7 +1417,10 @@ const run = async () => {
} else {
try {
const info = await jsm.consumers.info(STREAM_OPTION_SIGNAL_PRINTS, durableName);
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
if (
info?.config?.deliver_policy &&
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
) {
logger.warn("resetting consumer due to deliver policy change", {
durable: durableName,
current: info.config.deliver_policy,
@ -1390,7 +1431,10 @@ const run = async () => {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("not found")) {
logger.warn("failed to inspect jetstream consumer", { durable: durableName, error: message });
logger.warn("failed to inspect jetstream consumer", {
durable: durableName,
error: message
});
}
}
}
@ -1402,13 +1446,19 @@ const run = async () => {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("not found")) {
logger.warn("failed to reset jetstream consumer", { durable: nbboDurableName, error: message });
logger.warn("failed to reset jetstream consumer", {
durable: nbboDurableName,
error: message
});
}
}
} else {
try {
const info = await jsm.consumers.info(STREAM_OPTION_NBBO, nbboDurableName);
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
if (
info?.config?.deliver_policy &&
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
) {
logger.warn("resetting consumer due to deliver policy change", {
durable: nbboDurableName,
current: info.config.deliver_policy,
@ -1419,7 +1469,10 @@ const run = async () => {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("not found")) {
logger.warn("failed to inspect jetstream consumer", { durable: nbboDurableName, error: message });
logger.warn("failed to inspect jetstream consumer", {
durable: nbboDurableName,
error: message
});
}
}
}
@ -1440,7 +1493,10 @@ const run = async () => {
} else {
try {
const info = await jsm.consumers.info(STREAM_EQUITY_PRINTS, equityPrintDurableName);
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
if (
info?.config?.deliver_policy &&
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
) {
logger.warn("resetting consumer due to deliver policy change", {
durable: equityPrintDurableName,
current: info.config.deliver_policy,
@ -1475,7 +1531,10 @@ const run = async () => {
} else {
try {
const info = await jsm.consumers.info(STREAM_EQUITY_QUOTES, equityQuoteDurableName);
if (info?.config?.deliver_policy && info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY) {
if (
info?.config?.deliver_policy &&
info.config.deliver_policy !== env.COMPUTE_DELIVER_POLICY
) {
logger.warn("resetting consumer due to deliver policy change", {
durable: equityQuoteDurableName,
current: info.config.deliver_policy,
@ -1515,7 +1574,8 @@ const run = async () => {
try {
await jsm.consumers.delete(STREAM_OPTION_SIGNAL_PRINTS, durableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: durableName,
@ -1551,7 +1611,8 @@ const run = async () => {
try {
await jsm.consumers.delete(STREAM_OPTION_NBBO, nbboDurableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: nbboDurableName,
@ -1582,12 +1643,16 @@ const run = async () => {
throw error;
}
logger.warn("resetting jetstream consumer", { durable: equityPrintDurableName, error: message });
logger.warn("resetting jetstream consumer", {
durable: equityPrintDurableName,
error: message
});
try {
await jsm.consumers.delete(STREAM_EQUITY_PRINTS, equityPrintDurableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: equityPrintDurableName,
@ -1626,7 +1691,8 @@ const run = async () => {
try {
await jsm.consumers.delete(STREAM_EQUITY_QUOTES, equityQuoteDurableName);
} catch (deleteError) {
const deleteMessage = deleteError instanceof Error ? deleteError.message : String(deleteError);
const deleteMessage =
deleteError instanceof Error ? deleteError.message : String(deleteError);
if (!deleteMessage.includes("not found")) {
logger.warn("failed to delete jetstream consumer", {
durable: equityQuoteDurableName,

View file

@ -78,7 +78,9 @@ const getDteDays = (packet: FlowPacket): number | null => {
const inferDirection = (packet: FlowPacket): SmartMoneyDirection => {
const structureRights = stringFeature(packet, "structure_rights");
const optionType = stringFeature(packet, "option_type") || parseContractId(stringFeature(packet, "option_contract_id"))?.right;
const optionType =
stringFeature(packet, "option_type") ||
parseContractId(stringFeature(packet, "option_contract_id"))?.right;
const buy = numberFeature(packet, "nbbo_aggressive_buy_ratio");
const sell = numberFeature(packet, "nbbo_aggressive_sell_ratio");
const sellDominant = sell >= buy + 0.12;
@ -102,16 +104,26 @@ export type SmartMoneyParentEventOptions = {
eventCalendarMatch?: EventCalendarMatch | null;
};
const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions = {}): SmartMoneyFeatures => {
const buildFeatures = (
packet: FlowPacket,
options: SmartMoneyParentEventOptions = {}
): SmartMoneyFeatures => {
const contractId = stringFeature(packet, "option_contract_id");
const contract = parseContractId(contractId);
const underlyingMid = numberFeature(packet, "underlying_mid");
const quoteAge = numberFeature(packet, "nbbo_age_ms") || numberFeature(packet, "underlying_quote_age_ms");
const printCount = Math.max(0, Math.round(numberFeature(packet, "count") || packet.members.length));
const quoteAge =
numberFeature(packet, "nbbo_age_ms") || numberFeature(packet, "underlying_quote_age_ms");
const printCount = Math.max(
0,
Math.round(numberFeature(packet, "count") || packet.members.length)
);
const staleCount = numberFeature(packet, "nbbo_stale_count");
const missingCount = numberFeature(packet, "nbbo_missing_count");
const structureLegs = Math.max(0, Math.round(numberFeature(packet, "structure_legs")));
const strikeCount = Math.max(1, Math.round(numberFeature(packet, "structure_strikes") || (contract ? 1 : 0)));
const strikeCount = Math.max(
1,
Math.round(numberFeature(packet, "structure_strikes") || (contract ? 1 : 0))
);
const specialCount = numberFeature(packet, "special_print_count");
const calendarEventTs = options.eventCalendarMatch?.event_ts ?? null;
const eventTs = calendarEventTs ?? numberFeature(packet, "corporate_event_ts");
@ -119,7 +131,9 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
const expiryTs = contract ? Date.parse(`${contract.expiry}T00:00:00Z`) : Number.NaN;
const atmProximity =
contract && underlyingMid > 0 ? Math.abs(contract.strike - underlyingMid) / underlyingMid : null;
contract && underlyingMid > 0
? Math.abs(contract.strike - underlyingMid) / underlyingMid
: null;
return {
contract_count: Math.max(1, structureLegs || 1),
@ -143,14 +157,18 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
nbbo_stale_ratio: printCount > 0 ? clamp((staleCount + missingCount) / printCount) : 0,
quote_age_ms: quoteAge > 0 ? quoteAge : null,
venue_count: Math.max(1, Math.round(numberFeature(packet, "venue_count") || 1)),
inter_fill_ms_mean: printCount > 1 ? numberFeature(packet, "window_ms") / Math.max(1, printCount - 1) : null,
inter_fill_ms_mean:
printCount > 1 ? numberFeature(packet, "window_ms") / Math.max(1, printCount - 1) : null,
strike_count: strikeCount,
strike_concentration: strikeCount > 0 ? clamp(1 / strikeCount) : 0,
...(stringFeature(packet, "structure_type") ? { structure_type: stringFeature(packet, "structure_type") } : {}),
...(stringFeature(packet, "structure_type")
? { structure_type: stringFeature(packet, "structure_type") }
: {}),
structure_legs: structureLegs,
same_size_leg_symmetry: clamp(numberFeature(packet, "same_size_leg_symmetry")),
net_directional_bias: clamp(
numberFeature(packet, "nbbo_aggressive_buy_ratio") - numberFeature(packet, "nbbo_aggressive_sell_ratio"),
numberFeature(packet, "nbbo_aggressive_buy_ratio") -
numberFeature(packet, "nbbo_aggressive_sell_ratio"),
-1,
1
),
@ -159,7 +177,10 @@ const buildFeatures = (packet: FlowPacket, options: SmartMoneyParentEventOptions
underlying_move_bps: numberFeature(packet, "underlying_move_bps") || null,
days_to_event: eventTs > 0 ? (eventTs - referenceTs) / MS_PER_DAY : null,
expiry_after_event: eventTs > 0 && Number.isFinite(expiryTs) ? expiryTs >= eventTs : null,
pre_event_concentration: eventTs > 0 && eventTs >= referenceTs ? clamp(1 - (eventTs - referenceTs) / (21 * MS_PER_DAY)) : null,
pre_event_concentration:
eventTs > 0 && eventTs >= referenceTs
? clamp(1 - (eventTs - referenceTs) / (21 * MS_PER_DAY))
: null,
special_print_ratio: printCount > 0 ? clamp(specialCount / printCount) : 0
};
};
@ -170,7 +191,10 @@ const detectSuppression = (packet: FlowPacket, features: SmartMoneyFeatures): st
.split(",")
.map((item) => item.trim().toUpperCase())
.filter(Boolean);
if (conditions.some((condition) => SPECIAL_CONDITIONS.has(condition)) || features.special_print_ratio >= 0.34) {
if (
conditions.some((condition) => SPECIAL_CONDITIONS.has(condition)) ||
features.special_print_ratio >= 0.34
) {
reasons.push("special_print_or_complex_context");
}
if (features.nbbo_coverage_ratio < 0.35 || features.nbbo_stale_ratio >= 0.5) {
@ -198,7 +222,10 @@ const evaluateProfiles = (
const burstFactor = clamp(features.print_count / 8);
const quality = clamp(features.nbbo_coverage_ratio - features.nbbo_stale_ratio);
const shortDatedOtm =
dte <= 7 && features.atm_proximity !== null && features.atm_proximity >= 0.05 && features.option_type === "C";
dte <= 7 &&
features.atm_proximity !== null &&
features.atm_proximity >= 0.05 &&
features.option_type === "C";
const nearAtm = features.atm_proximity !== null && features.atm_proximity <= 0.015;
const preEvent =
features.days_to_event !== null &&
@ -211,7 +238,11 @@ const evaluateProfiles = (
"institutional_directional",
suppressed.length > 0 || shortDatedOtm
? 0.18
: 0.2 + premiumFactor * 0.25 + burstFactor * 0.18 + quality * 0.16 + (buy >= 0.58 || sell >= 0.58 ? 0.12 : 0),
: 0.2 +
premiumFactor * 0.25 +
burstFactor * 0.18 +
quality * 0.16 +
(buy >= 0.58 || sell >= 0.58 ? 0.12 : 0),
direction,
[
"large_parent_event",
@ -232,13 +263,19 @@ const evaluateProfiles = (
),
score(
"event_driven",
0.12 + (preEvent ? 0.32 : 0) + premiumFactor * 0.14 + clamp(features.spread_widening ?? 0, 0, 0.16),
0.12 +
(preEvent ? 0.32 : 0) +
premiumFactor * 0.14 +
clamp(features.spread_widening ?? 0, 0, 0.16),
direction === "unknown" ? "neutral" : direction,
["event_calendar_alignment", "expiry_after_event", "pre_event_concentration"]
),
score(
"vol_seller",
0.12 + (sell >= 0.58 ? 0.24 : 0) + (structure === "straddle" || structure === "strangle" ? 0.2 : 0) + premiumFactor * 0.14,
0.12 +
(sell >= 0.58 ? 0.24 : 0) +
(structure === "straddle" || structure === "strangle" ? 0.2 : 0) +
premiumFactor * 0.14,
"neutral",
["sell_side_premium", "short_vol_structure_evidence"]
),
@ -273,9 +310,14 @@ export const buildSmartMoneyEventFromPacket = (
const suppressed = detectSuppression(packet, features);
const profileScores = evaluateProfiles(packet, features, suppressed);
const primary = profileScores[0] ?? null;
const abstained = !primary || primary.probability < 0.42 || suppressed.includes("stale_or_missing_quote_context");
const underlying = stringFeature(packet, "underlying_id") || parseContractId(features.option_contract_id ?? "")?.root || "UNKNOWN";
const eventKind = features.structure_legs >= 2 || stringFeature(packet, "packet_kind") === "structure"
const abstained =
!primary || primary.probability < 0.42 || suppressed.includes("stale_or_missing_quote_context");
const underlying =
stringFeature(packet, "underlying_id") ||
parseContractId(features.option_contract_id ?? "")?.root ||
"UNKNOWN";
const eventKind =
features.structure_legs >= 2 || stringFeature(packet, "packet_kind") === "structure"
? "multi_leg_event"
: "single_leg_event";
@ -292,8 +334,8 @@ export const buildSmartMoneyEventFromPacket = (
event_window_ms: features.window_ms,
features,
profile_scores: profileScores,
primary_profile_id: abstained ? null : primary?.profile_id ?? null,
primary_direction: abstained ? "unknown" : primary?.direction ?? "unknown",
primary_profile_id: abstained ? null : (primary?.profile_id ?? null),
primary_direction: abstained ? "unknown" : (primary?.direction ?? "unknown"),
abstained,
suppressed_reasons: suppressed
});
@ -308,7 +350,9 @@ const LEGACY_PROFILE_MAP: Record<SmartMoneyProfileId, string> = {
hedge_reactive: "smart_money_hedge_reactive"
};
export const deriveClassifierHitsFromSmartMoneyEvent = (event: SmartMoneyEvent): ClassifierHit[] => {
export const deriveClassifierHitsFromSmartMoneyEvent = (
event: SmartMoneyEvent
): ClassifierHit[] => {
if (event.abstained || !event.primary_profile_id) {
return [];
}

View file

@ -24,9 +24,7 @@ type RollingWindowEntry = {
};
const toNumbers = (values: string[]): number[] => {
return values
.map((value) => Number(value))
.filter((value) => Number.isFinite(value));
return values.map((value) => Number(value)).filter((value) => Number.isFinite(value));
};
export const computeStats = (values: number[]): { mean: number; stddev: number; count: number } => {

View file

@ -1,4 +1,9 @@
import type { FlowPacket, SmartMoneyDirection, SmartMoneyEvent, SmartMoneyProfileId } from "@islandflow/types";
import type {
FlowPacket,
SmartMoneyDirection,
SmartMoneyEvent,
SmartMoneyProfileId
} from "@islandflow/types";
import { buildSmartMoneyEventFromPacket, type SmartMoneyParentEventOptions } from "./parent-events";
export type SmartMoneyLabel = {
@ -115,8 +120,12 @@ export const compareSmartMoneyReplayOutputs = (
liveEvents: SmartMoneyEvent[],
batchEvents: SmartMoneyEvent[]
): ReplayConsistencyReport => {
const liveById = new Map(liveEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)]));
const batchById = new Map(batchEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)]));
const liveById = new Map(
liveEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)])
);
const batchById = new Map(
batchEvents.map((event) => [event.event_id, smartMoneyEventSignature(event)])
);
const ids = [...new Set([...liveById.keys(), ...batchById.keys()])].sort();
const mismatches: ReplayConsistencyMismatch[] = [];
@ -153,7 +162,9 @@ export const evaluateSmartMoneyEvents = (
const labelsById = new Map(labels.map((label) => [label.event_id, label]));
const labeledEvents = events
.map((event) => ({ event, label: labelsById.get(event.event_id) }))
.filter((entry): entry is { event: SmartMoneyEvent; label: SmartMoneyLabel } => Boolean(entry.label));
.filter((entry): entry is { event: SmartMoneyEvent; label: SmartMoneyLabel } =>
Boolean(entry.label)
);
const emitted = events.filter((event) => !event.abstained && event.primary_profile_id);
const profilePrecision: SmartMoneyEvaluationReport["profile_precision"] = {};
@ -163,7 +174,8 @@ export const evaluateSmartMoneyEvents = (
const predicted = labeledEvents.filter((entry) => entry.event.primary_profile_id === profile);
const actual = labeledEvents.filter((entry) => entry.label.profile_id === profile);
const truePositive = predicted.filter((entry) => entry.label.profile_id === profile).length;
profilePrecision[profile] = predicted.length > 0 ? round(truePositive / predicted.length) : null;
profilePrecision[profile] =
predicted.length > 0 ? round(truePositive / predicted.length) : null;
profileRecall[profile] = actual.length > 0 ? round(truePositive / actual.length) : null;
}
@ -175,7 +187,10 @@ export const evaluateSmartMoneyEvents = (
labeled_count: labeledEvents.length,
emitted_count: emitted.length,
abstained_count: events.filter((event) => event.abstained).length,
abstention_rate: events.length > 0 ? round(events.filter((event) => event.abstained).length / events.length) : 0,
abstention_rate:
events.length > 0
? round(events.filter((event) => event.abstained).length / events.length)
: 0,
profile_precision: profilePrecision,
profile_recall: profileRecall,
calibration,
@ -195,7 +210,9 @@ const buildCalibration = (
}));
for (const { event, label } of entries) {
const probability = event.profile_scores.find((entry) => entry.profile_id === event.primary_profile_id)?.probability ?? 0;
const probability =
event.profile_scores.find((entry) => entry.profile_id === event.primary_profile_id)
?.probability ?? 0;
const index = Math.min(bucketCount - 1, Math.floor(probability * bucketCount));
buckets[index].probabilities.push(probability);
if (!event.abstained && event.primary_profile_id === label.profile_id) {
@ -209,9 +226,13 @@ const buildCalibration = (
count: bucket.probabilities.length,
average_probability:
bucket.probabilities.length > 0
? round(bucket.probabilities.reduce((sum, value) => sum + value, 0) / bucket.probabilities.length)
? round(
bucket.probabilities.reduce((sum, value) => sum + value, 0) /
bucket.probabilities.length
)
: 0,
accuracy: bucket.probabilities.length > 0 ? round(bucket.correct / bucket.probabilities.length) : null
accuracy:
bucket.probabilities.length > 0 ? round(bucket.correct / bucket.probabilities.length) : null
}));
};
@ -223,7 +244,10 @@ const buildEconomicSanity = (
sign: directionalSign(event.primary_direction),
realized: label.realized_return_bps
}))
.filter((entry): entry is { sign: number; realized: number } => entry.sign !== 0 && Number.isFinite(entry.realized));
.filter(
(entry): entry is { sign: number; realized: number } =>
entry.sign !== 0 && Number.isFinite(entry.realized)
);
if (directional.length === 0) {
return {
@ -236,7 +260,12 @@ const buildEconomicSanity = (
const signedReturns = directional.map((entry) => entry.sign * entry.realized);
return {
directional_count: directional.length,
direction_hit_rate: round(signedReturns.filter((value) => value > 0).length / directional.length),
average_signed_return_bps: round(signedReturns.reduce((sum, value) => sum + value, 0) / signedReturns.length, 2)
direction_hit_rate: round(
signedReturns.filter((value) => value > 0).length / directional.length
),
average_signed_return_bps: round(
signedReturns.reduce((sum, value) => sum + value, 0) / signedReturns.length,
2
)
};
};

View file

@ -134,7 +134,9 @@ const dayDiff = (from: string | null, to: string | null): number | null => {
};
const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
const sizes = legs.map((leg) => leg.totalSize).filter((value) => Number.isFinite(value) && value > 0);
const sizes = legs
.map((leg) => leg.totalSize)
.filter((value) => Number.isFinite(value) && value > 0);
if (sizes.length < 2) {
return 0;
}
@ -146,7 +148,10 @@ const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
return min / max;
};
export const shouldEmitStructurePacket = (legs: LegEvidence[], currentLegContractId: string): boolean => {
export const shouldEmitStructurePacket = (
legs: LegEvidence[],
currentLegContractId: string
): boolean => {
if (legs.length < 2) {
return false;
}
@ -226,7 +231,8 @@ export const planStructurePacket = (
const totalSize = legs.reduce((sum, leg) => sum + leg.totalSize, 0);
const count = legs.reduce((sum, leg) => sum + leg.members.length, 0);
const placements = mergePlacements(legs);
const placementTotal = placements.aa + placements.a + placements.b + placements.bb + placements.mid;
const placementTotal =
placements.aa + placements.a + placements.b + placements.bb + placements.mid;
const aggressiveTotal = placements.aa + placements.a + placements.b + placements.bb;
const aggressiveBuy = placements.aa + placements.a;
const aggressiveSell = placements.bb + placements.b;
@ -235,7 +241,10 @@ export const planStructurePacket = (
const nbboAggressiveSellRatio = aggressiveTotal > 0 ? aggressiveSell / aggressiveTotal : 0;
const nbboAggressiveRatio = placementTotal > 0 ? aggressiveTotal / placementTotal : 0;
const source_ts = legs.reduce((min, leg) => Math.min(min, leg.source_ts), Number.POSITIVE_INFINITY);
const source_ts = legs.reduce(
(min, leg) => Math.min(min, leg.source_ts),
Number.POSITIVE_INFINITY
);
const ingest_ts = legs.reduce((max, leg) => Math.max(max, leg.ingest_ts), 0);
const seq = legs.reduce((max, leg) => Math.max(max, leg.seq), 0);

View file

@ -47,7 +47,10 @@ export const summarizeStructure = (legs: ContractLeg[]): StructureSummary | null
legs: legs.length,
strikes: strikes.length,
strikeSpan,
rights: rights.size === 2 ? "C/P" : Array.from(rights)[0] ?? "",
contractIds: legs.map((leg) => leg.contractId).slice().sort()
rights: rights.size === 2 ? "C/P" : (Array.from(rights)[0] ?? ""),
contractIds: legs
.map((leg) => leg.contractId)
.slice()
.sort()
};
};

View file

@ -293,4 +293,3 @@ describe("compute classifiers", () => {
expect(hit!.explanations[0]).toMatch(/Consistent with/i);
});
});

View file

@ -17,7 +17,8 @@ export const TEST_CLASSIFIER_CONFIG: ClassifierConfig = {
zeroDteMinSize: 400
};
export const buildFlowPacket = (opts: {
export const buildFlowPacket = (
opts: {
id?: string;
source_ts?: number;
ingest_ts?: number;
@ -26,7 +27,8 @@ export const buildFlowPacket = (opts: {
members?: string[];
features?: FlowPacket["features"];
join_quality?: FlowPacket["join_quality"];
} = {}): FlowPacket => {
} = {}
): FlowPacket => {
const id = opts.id ?? "flowpacket:test";
const source_ts = opts.source_ts ?? Date.parse("2025-01-01T14:30:00Z");
const ingest_ts = opts.ingest_ts ?? source_ts;
@ -66,4 +68,3 @@ export const buildFlowPacket = (opts: {
export const getHit = (hits: ClassifierHit[], id: string): ClassifierHit | null => {
return hits.find((hit) => hit.classifier_id === id) ?? null;
};

View file

@ -18,7 +18,9 @@ const placements = (overrides?: Partial<LegEvidence["placements"]>): LegEvidence
...overrides
});
const leg = (input: Partial<LegEvidence> & Pick<LegEvidence, "contractId" | "right" | "strike">): LegEvidence => {
const leg = (
input: Partial<LegEvidence> & Pick<LegEvidence, "contractId" | "right" | "strike">
): LegEvidence => {
return {
contractId: input.contractId,
root: "SPY",

View file

@ -85,10 +85,14 @@ const decodePayload = (data: WebSocket.RawData): unknown => {
}
if (ArrayBuffer.isView(data)) {
return JSON.parse(new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength))) as unknown;
return JSON.parse(
new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength))
) as unknown;
}
return JSON.parse(new TextDecoder().decode(new Uint8Array(data as unknown as ArrayBuffer))) as unknown;
return JSON.parse(
new TextDecoder().decode(new Uint8Array(data as unknown as ArrayBuffer))
) as unknown;
};
const extractExchangeMeta = (payload: unknown): AlpacaExchangeMetaEntry[] => {
@ -103,8 +107,18 @@ const extractExchangeMeta = (payload: unknown): AlpacaExchangeMetaEntry[] => {
continue;
}
const candidate = entry as Record<string, unknown>;
const code = typeof candidate.code === "string" ? candidate.code : typeof candidate.exchange === "string" ? candidate.exchange : null;
const name = typeof candidate.name === "string" ? candidate.name : typeof candidate.description === "string" ? candidate.description : null;
const code =
typeof candidate.code === "string"
? candidate.code
: typeof candidate.exchange === "string"
? candidate.exchange
: null;
const name =
typeof candidate.name === "string"
? candidate.name
: typeof candidate.description === "string"
? candidate.description
: null;
if (!code || !name) {
continue;
}
@ -128,9 +142,19 @@ const buildExchangeNameMap = (entries: AlpacaExchangeMetaEntry[]): Map<string, s
return map;
};
const OFF_EXCHANGE_HINTS = ["FINRA", "TRF", "ADF", "OTC", "Trade Reporting Facility", "Alternative Display Facility"];
const OFF_EXCHANGE_HINTS = [
"FINRA",
"TRF",
"ADF",
"OTC",
"Trade Reporting Facility",
"Alternative Display Facility"
];
export const inferOffExchangeFlag = (exchangeCode: string | undefined, exchangeNameMap: Map<string, string>): boolean => {
export const inferOffExchangeFlag = (
exchangeCode: string | undefined,
exchangeNameMap: Map<string, string>
): boolean => {
if (!exchangeCode) {
return false;
}
@ -151,7 +175,9 @@ const buildWsUrl = (wsBaseUrl: string, feed: AlpacaEquitiesFeed): string => {
return `${parsed.origin}/v2/${feed}`;
};
const fetchExchangeMeta = async (config: AlpacaEquitiesAdapterConfig): Promise<Map<string, string>> => {
const fetchExchangeMeta = async (
config: AlpacaEquitiesAdapterConfig
): Promise<Map<string, string>> => {
const url = new URL("/v2/stocks/meta/exchanges", config.restUrl);
try {
@ -243,7 +269,10 @@ export const createAlpacaEquitiesAdapter = (
continue;
}
const message = entry as (AlpacaTradeMessage | AlpacaQuoteMessage | { T?: string; msg?: string });
const message = entry as
| AlpacaTradeMessage
| AlpacaQuoteMessage
| { T?: string; msg?: string };
const type = message.T;
if (type === "success") {

View file

@ -89,11 +89,7 @@ const priceForPlacement = (
return formatPrice(Math.max(0.01, price));
};
const buildQuoteContext = (
symbol: string,
now: number,
control: SyntheticControlState
) => {
const buildQuoteContext = (symbol: string, now: number, control: SyntheticControlState) => {
const session = getSyntheticSessionState(now, control);
const state = getSyntheticUnderlyingState(symbol, now, control, session);
return {
@ -184,7 +180,9 @@ export const createSyntheticEquitiesAdapter = (
session.regime === "retail_chase";
if (allowDark) {
const darkSymbol = focusSymbols[seq % focusSymbols.length] ?? SYNTHETIC_SYMBOLS[symbolCursor % SYNTHETIC_SYMBOLS.length]!;
const darkSymbol =
focusSymbols[seq % focusSymbols.length] ??
SYNTHETIC_SYMBOLS[symbolCursor % SYNTHETIC_SYMBOLS.length]!;
const darkQuote = buildQuoteContext(darkSymbol, now, control);
const darkPlacement = pickDarkPlacement(
darkQuote.state.driftBps,
@ -203,13 +201,7 @@ export const createSyntheticEquitiesAdapter = (
if (handlers.onQuote) {
quoteSeq += 1;
void handlers.onQuote(
buildSyntheticQuote(
quoteSeq,
now - 2,
darkSymbol,
darkQuote.bid,
darkQuote.ask
)
buildSyntheticQuote(quoteSeq, now - 2, darkSymbol, darkQuote.bid, darkQuote.ask)
);
}
@ -236,11 +228,7 @@ export const createSyntheticEquitiesAdapter = (
const eventTs = now + i * 4;
const quote = buildQuoteContext(symbol, eventTs, control);
const clustered = focusSet.has(symbol);
const placement = pickPrimaryPlacement(
quote.state.driftBps,
session.regime,
seq + i
);
const placement = pickPrimaryPlacement(quote.state.driftBps, session.regime, seq + i);
const exchange = EXCHANGES[(seq + symbol.charCodeAt(0) + i) % EXCHANGES.length]!;
const baseSize =
throughput.litSizeBase +
@ -255,13 +243,7 @@ export const createSyntheticEquitiesAdapter = (
if (handlers.onQuote) {
quoteSeq += 1;
void handlers.onQuote(
buildSyntheticQuote(
quoteSeq,
eventTs - 2,
symbol,
quote.bid,
quote.ask
)
buildSyntheticQuote(quoteSeq, eventTs - 2, symbol, quote.bid, quote.ask)
);
}

View file

@ -240,10 +240,7 @@ const run = async () => {
await ensureEquityQuotesTable(clickhouse);
});
const adapter = selectAdapter(
env.EQUITIES_INGEST_ADAPTER,
() => syntheticControl
);
const adapter = selectAdapter(env.EQUITIES_INGEST_ADAPTER, () => syntheticControl);
logger.info("ingest adapter selected", { adapter: adapter.name });
const allowPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);
const allowQuotePublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);

View file

@ -126,9 +126,13 @@ const decodePayload = (data: WebSocket.RawData): unknown => {
return JSON.parse(new TextDecoder().decode(new Uint8Array(data))) as unknown;
}
if (ArrayBuffer.isView(data)) {
return JSON.parse(new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength))) as unknown;
return JSON.parse(
new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength))
) as unknown;
}
return JSON.parse(new TextDecoder().decode(new Uint8Array(data as unknown as ArrayBuffer))) as unknown;
return JSON.parse(
new TextDecoder().decode(new Uint8Array(data as unknown as ArrayBuffer))
) as unknown;
};
const run = async () => {

View file

@ -152,10 +152,7 @@ const normalizeUnderlyings = (value: string[]): string[] => {
return result;
};
const fetchJson = async <T>(
url: URL,
config: AlpacaOptionsAdapterConfig
): Promise<T> => {
const fetchJson = async <T>(url: URL, config: AlpacaOptionsAdapterConfig): Promise<T> => {
const response = await fetch(url.toString(), {
headers: buildAlpacaAuthHeaders(config.credentials)
});
@ -235,10 +232,7 @@ const fetchOptionSnapshots = async (
return contracts;
};
const selectExpiries = (
contracts: OptionContract[],
maxDteDays: number
): ExpiryInfo[] => {
const selectExpiries = (contracts: OptionContract[], maxDteDays: number): ExpiryInfo[] => {
const today = new Date();
const expiryMap = new Map<string, ExpiryInfo>();
@ -332,7 +326,9 @@ const selectContractsForUnderlying = (
const minStrike = price * (1 - config.moneynessPct);
const maxStrike = price * (1 + config.moneynessPct);
const strikePairs = Array.from(strikeMap.entries())
.filter(([strike, pair]) => pair.call && pair.put && strike >= minStrike && strike <= maxStrike)
.filter(
([strike, pair]) => pair.call && pair.put && strike >= minStrike && strike <= maxStrike
)
.map(([strike, pair]) => ({
strike,
call: pair.call as string,
@ -540,7 +536,10 @@ export const createAlpacaOptionsAdapter = (
continue;
}
const message = entry as AlpacaTradeMessage | AlpacaQuoteMessage | { T?: string; msg?: string };
const message = entry as
| AlpacaTradeMessage
| AlpacaQuoteMessage
| { T?: string; msg?: string };
const type = message.T;
if (type === "t") {

View file

@ -235,8 +235,7 @@ export const createDatabentoOptionsAdapter = (
return;
}
const scaledPrice =
config.priceScale === 1 ? price : price / config.priceScale;
const scaledPrice = config.priceScale === 1 ? price : price / config.priceScale;
const conditions = Array.isArray(payload.conditions)
? payload.conditions.map((entry) => String(entry))

View file

@ -59,9 +59,7 @@ const readLines = async (
}
};
export const createIbkrOptionsAdapter = (
config: IbkrOptionsAdapterConfig
): OptionIngestAdapter => {
export const createIbkrOptionsAdapter = (config: IbkrOptionsAdapterConfig): OptionIngestAdapter => {
return {
name: "ibkr",
start: (handlers: OptionIngestHandlers) => {

View file

@ -715,10 +715,7 @@ const SYNTHETIC_PROFILES: Record<SyntheticMarketMode, SyntheticOptionsProfile> =
...scenario,
countRange: [scenario.countRange[0], scenario.countRange[1]],
sizeRange: [scenario.sizeRange[0], scenario.sizeRange[1]],
targetNotionalRange: [
scenario.targetNotionalRange[0],
scenario.targetNotionalRange[1]
]
targetNotionalRange: [scenario.targetNotionalRange[0], scenario.targetNotionalRange[1]]
})),
pricePlacements: PLACEMENTS
},
@ -743,10 +740,7 @@ const SYNTHETIC_PROFILES: Record<SyntheticMarketMode, SyntheticOptionsProfile> =
scenarios: SCENARIO_LIBRARY.map((scenario) => ({
...scenario,
countRange: [scenario.countRange[0] + 2, scenario.countRange[1] + 4],
sizeRange: [
Math.round(scenario.sizeRange[0] * 1.8),
Math.round(scenario.sizeRange[1] * 2.1)
],
sizeRange: [Math.round(scenario.sizeRange[0] * 1.8), Math.round(scenario.sizeRange[1] * 2.1)],
targetNotionalRange: [
Math.round(scenario.targetNotionalRange[0] * 1.7),
Math.round(scenario.targetNotionalRange[1] * 2.0)
@ -768,7 +762,7 @@ const SMART_MONEY_TEMPLATE_SCENARIOS: Record<
hedge_reactive: "reactive_put_wall"
};
const pick = <T,>(items: readonly T[], seed: number): T => {
const pick = <T>(items: readonly T[], seed: number): T => {
return items[Math.abs(seed) % items.length]!;
};
@ -850,9 +844,7 @@ export const updateSyntheticIvForTest = (
const sizeImpact = Math.log10(Math.max(10, input.size)) * 0.012;
const notionalImpact = Math.log10(Math.max(1_000, input.notional)) * 0.01;
pressure +=
input.placement === "AA"
? sizeImpact + notionalImpact
: (sizeImpact + notionalImpact) * 0.65;
input.placement === "AA" ? sizeImpact + notionalImpact : (sizeImpact + notionalImpact) * 0.65;
} else if (input.placement === "MID") {
pressure += 0.001;
} else {
@ -879,8 +871,7 @@ const estimateSyntheticOptionMid = (input: {
: Math.max(0, input.strike - input.underlying);
const timeYears = Math.max(1, input.dteDays + 1) / 365;
const baselineIv = initializeSyntheticIv(input.dteDays, input.moneyness);
const modeBoost =
input.mode === "firehose" ? 1.18 : input.mode === "active" ? 1.08 : 0.96;
const modeBoost = input.mode === "firehose" ? 1.18 : input.mode === "active" ? 1.08 : 0.96;
const distance = Math.abs(input.moneyness - 1);
const extrinsic =
input.underlying *
@ -939,12 +930,7 @@ const chooseScenario = (
): Scenario => {
const session = getSyntheticSessionState(now, control);
const focusSymbol = session.focus_symbols[0] ?? SYNTHETIC_SYMBOLS[0]!;
const familyWeights = getSyntheticScenarioWeights(
focusSymbol,
now,
control,
session
);
const familyWeights = getSyntheticScenarioWeights(focusSymbol, now, control, session);
const coverageCounts = getCoverageCounts(coverageState, now, control);
const weightedScenarios = profile.scenarios.map((scenario, index) => {
const familyWeight = familyWeights[scenario.label];
@ -964,7 +950,10 @@ const chooseScenario = (
: 1;
return {
...scenario,
weight: Math.max(1, Math.round(scenario.weight * familyWeight * coverageBoost * quietBias * 100))
weight: Math.max(
1,
Math.round(scenario.weight * familyWeight * coverageBoost * quietBias * 100)
)
};
});
return pickWeighted(weightedScenarios, now + control.shared_seed * 31);
@ -977,7 +966,8 @@ const pickScenarioSymbol = (
): string => {
const session = getSyntheticSessionState(now, control);
const symbolPool =
scenario.preferredSymbols?.length && (scenario.label === "event_driven" || Math.abs(now) % 4 === 0)
scenario.preferredSymbols?.length &&
(scenario.label === "event_driven" || Math.abs(now) % 4 === 0)
? [...scenario.preferredSymbols]
: session.focus_symbols.length > 0
? [...session.focus_symbols, ...SYNTHETIC_SYMBOLS]
@ -1033,7 +1023,8 @@ const buildDynamicFlowFeatures = (
0,
0.26
),
underlying_move_bps: Math.round(
underlying_move_bps:
Math.round(
(Number(scenario.flowFeatures.underlying_move_bps ?? underlying.driftBps) +
underlying.shockBps * 0.35) *
100
@ -1059,18 +1050,14 @@ const buildBurst = (
coverageState: CoverageWindowState,
scenarioOverride?: Scenario
): Burst => {
const scenario =
scenarioOverride ?? chooseScenario(profile, now, control, coverageState);
const scenario = scenarioOverride ?? chooseScenario(profile, now, control, coverageState);
const symbol = pickScenarioSymbol(scenario, now, control);
const symbolHash = hashSyntheticSymbol(symbol);
const seed = symbolHash + burstIndex * 7;
const session = getSyntheticSessionState(now, control);
const underlyingState = getSyntheticUnderlyingState(symbol, now, control, session);
const baseUnderlying = underlyingState.mid;
const expiryOffset = pick(
scenario.expiryOffsets ?? EXPIRY_OFFSETS,
symbolHash + burstIndex
);
const expiryOffset = pick(scenario.expiryOffsets ?? EXPIRY_OFFSETS, symbolHash + burstIndex);
const strikeStep = baseUnderlying >= 200 ? 10 : baseUnderlying >= 100 ? 5 : 2.5;
const right =
scenario.right === "either"
@ -1099,8 +1086,7 @@ const buildBurst = (
const priceStep =
scenario.priceTrend === "up" ? 0.01 : scenario.priceTrend === "down" ? -0.01 : 0;
const flowFeatures = buildDynamicFlowFeatures(scenario, symbol, now, control);
const legTemplates =
scenario.legs?.length
const legTemplates = scenario.legs?.length
? scenario.legs
: [
{
@ -1127,8 +1113,7 @@ const buildBurst = (
const strike = Math.max(
1,
templateStrike ??
Math.round(baseUnderlying / strikeStep) * strikeStep +
strikeOffset * strikeStep
Math.round(baseUnderlying / strikeStep) * strikeStep + strikeOffset * strikeStep
);
const legSize = Math.max(1, Math.round(baseSize * (template.sizeMultiplier ?? 1)));
const legMoneyness = strike / baseUnderlying;
@ -1141,13 +1126,13 @@ const buildBurst = (
mode
});
const targetMid =
targetNotionalPerLeg /
Math.max(1, legSize * cycles * OPTION_CONTRACT_MULTIPLIER);
targetNotionalPerLeg / Math.max(1, legSize * cycles * OPTION_CONTRACT_MULTIPLIER);
const cappedTheoreticalMid = Math.min(
theoreticalMid,
Math.max(0.35, targetMid * (scenario.label === "institutional_directional" ? 2.2 : 2.6))
);
const blendedMid = cappedTheoreticalMid * 0.45 + targetMid * 0.55 * (template.priceMultiplier ?? 1);
const blendedMid =
cappedTheoreticalMid * 0.45 + targetMid * 0.55 * (template.priceMultiplier ?? 1);
return {
contractId: `${symbol}-${expiry}-${formatStrike(strike)}-${template.right}`,
right: template.right,
@ -1184,8 +1169,7 @@ const buildBurst = (
scenario.missingQuoteProbability ??
clampValue((1 - session.quote_cleanliness) * 0.16, 0, 0.18),
staleQuoteProbability:
scenario.staleQuoteProbability ??
clampValue((1 - session.quote_cleanliness) * 0.3, 0, 0.42)
scenario.staleQuoteProbability ?? clampValue((1 - session.quote_cleanliness) * 0.3, 0, 0.42)
};
};
@ -1202,7 +1186,9 @@ export const listSyntheticSmartMoneyScenariosForTest = (): SyntheticSmartMoneySc
hiddenLabel:
id === "neutral_noise"
? "single_print_mid"
: SMART_MONEY_TEMPLATE_SCENARIOS[id as Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise">]
: SMART_MONEY_TEMPLATE_SCENARIOS[
id as Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise">
]
}));
export const buildSyntheticSmartMoneyBurstForTest = (
@ -1233,16 +1219,16 @@ export const buildSyntheticSmartMoneyBurstForTest = (
updated_by: "system"
} satisfies SyntheticControlState;
const mode: SyntheticMarketMode =
scenarioId === "retail_whale" || scenarioId === "neutral_noise"
? "realistic"
: "active";
scenarioId === "retail_whale" || scenarioId === "neutral_noise" ? "realistic" : "active";
const profile = SYNTHETIC_PROFILES[mode];
const coverageState = createCoverageWindowState();
const scenario =
scenarioId === "neutral_noise"
? profile.scenarios.find((candidate) => candidate.id === "single_print_mid")!
: profile.scenarios.find(
(candidate) => candidate.id === SMART_MONEY_TEMPLATE_SCENARIOS[
(candidate) =>
candidate.id ===
SMART_MONEY_TEMPLATE_SCENARIOS[
scenarioId as Exclude<(typeof SMART_MONEY_SCENARIO_IDS)[number], "neutral_noise">
]
)!;
@ -1255,13 +1241,10 @@ export const buildSyntheticFlowPacketForTest = (
): { packet: FlowPacket; hiddenLabel: string } => {
const burst = buildSyntheticSmartMoneyBurstForTest(scenarioId, now);
const primaryLeg = burst.legs[0]!;
const corporateEventOffset = Number(
burst.flowFeatures.corporate_event_ts_offset_days ?? 0
);
const corporateEventOffset = Number(burst.flowFeatures.corporate_event_ts_offset_days ?? 0);
const totalSize = burst.legs.reduce((sum, leg) => sum + leg.baseSize * burst.cycles, 0);
const totalPremium = burst.legs.reduce(
(sum, leg) =>
sum + leg.basePrice * leg.baseSize * burst.cycles * OPTION_CONTRACT_MULTIPLIER,
(sum, leg) => sum + leg.basePrice * leg.baseSize * burst.cycles * OPTION_CONTRACT_MULTIPLIER,
0
);
const flowFeatures: FlowPacket["features"] = {
@ -1272,15 +1255,10 @@ export const buildSyntheticFlowPacketForTest = (
window_ms: Math.max(0, (burst.printCount - 1) * 45),
total_size: totalSize,
total_premium: Number(totalPremium.toFixed(2)),
total_notional: Number(
(burst.underlying * totalSize * OPTION_CONTRACT_MULTIPLIER).toFixed(2)
),
total_notional: Number((burst.underlying * totalSize * OPTION_CONTRACT_MULTIPLIER).toFixed(2)),
first_price: primaryLeg.basePrice,
last_price: Number(
(
primaryLeg.basePrice *
(1 + burst.priceStep * Math.max(0, burst.cycles - 1))
).toFixed(2)
(primaryLeg.basePrice * (1 + burst.priceStep * Math.max(0, burst.cycles - 1))).toFixed(2)
),
nbbo_missing_count: 0,
nbbo_stale_count: 0,
@ -1300,10 +1278,7 @@ export const buildSyntheticFlowPacketForTest = (
Number(flowFeatures.total_premium ?? totalPremium),
72_000
);
flowFeatures.execution_iv_shock = Math.max(
Number(flowFeatures.execution_iv_shock ?? 0),
0.22
);
flowFeatures.execution_iv_shock = Math.max(Number(flowFeatures.execution_iv_shock ?? 0), 0.22);
}
if (scenarioId === "event_driven") {
flowFeatures.count = 2;
@ -1411,14 +1386,7 @@ export const buildSyntheticBurstForTest = (
return cached[burstIndex - 1]!;
}
for (let index = cached.length + 1; index <= burstIndex; index += 1) {
const current = buildBurst(
index,
now + index * 1_000,
mode,
profile,
control,
coverageState
);
const current = buildBurst(index, now + index * 1_000, mode, profile, control, coverageState);
recordCoverageHit(coverageState, current.label, now + index * 1_000);
cached.push(current);
}
@ -1466,14 +1434,7 @@ export const createSyntheticOptionsAdapter = (
};
if (!currentBurst || remainingRuns <= 0) {
burstIndex += 1;
currentBurst = buildBurst(
burstIndex,
now,
config.mode,
profile,
control,
coverageState
);
currentBurst = buildBurst(burstIndex, now, config.mode, profile, control, coverageState);
recordCoverageHit(coverageState, currentBurst.label, now);
remainingRuns = pickInt(
profile.burstRunRange[0],
@ -1565,8 +1526,7 @@ export const createSyntheticOptionsAdapter = (
const quoteSeed = Math.abs(burst.seed + i * 17) % 1000;
const missingQuote = quoteSeed / 1000 < burst.missingQuoteProbability;
const staleQuote =
!missingQuote &&
((quoteSeed + 233) % 1000) / 1000 < burst.staleQuoteProbability;
!missingQuote && ((quoteSeed + 233) % 1000) / 1000 < burst.staleQuoteProbability;
if (handlers.onNBBO && !missingQuote) {
nbboSeq += 1;

View file

@ -48,7 +48,11 @@ export const selectAtOrBefore = <T extends { ts: number; seq: number }>(
if (item.ts > ts) {
continue;
}
if (!selected || item.ts > selected.ts || (item.ts === selected.ts && item.seq >= selected.seq)) {
if (
!selected ||
item.ts > selected.ts ||
(item.ts === selected.ts && item.seq >= selected.seq)
) {
selected = item;
}
}

View file

@ -43,7 +43,12 @@ import { createDatabentoOptionsAdapter } from "./adapters/databento";
import { createIbkrOptionsAdapter } from "./adapters/ibkr";
import { createSyntheticOptionsAdapter } from "./adapters/synthetic";
import type { OptionIngestAdapter, StopHandler } from "./adapters/types";
import { enrichOptionPrint, rememberContext, selectAtOrBefore, type ContextHistory } from "./enrichment";
import {
enrichOptionPrint,
rememberContext,
selectAtOrBefore,
type ContextHistory
} from "./enrichment";
import { z } from "zod";
const service = "ingest-options";
@ -87,7 +92,10 @@ const envSchema = z.object({
IBKR_EXPIRY: z.string().min(1).default("20250117"),
IBKR_STRIKE: z.coerce.number().positive().default(450),
IBKR_RIGHT: z
.preprocess((value) => (typeof value === "string" ? value.toUpperCase() : value), z.enum(["C", "P"]))
.preprocess(
(value) => (typeof value === "string" ? value.toUpperCase() : value),
z.enum(["C", "P"])
)
.default("C"),
IBKR_EXCHANGE: z.string().min(1).default("SMART"),
IBKR_CURRENCY: z.string().min(1).default("USD"),
@ -395,10 +403,7 @@ const run = async () => {
await ensureOptionNBBOTable(clickhouse);
});
const adapter = selectAdapter(
env.OPTIONS_INGEST_ADAPTER,
() => syntheticControl
);
const adapter = selectAdapter(env.OPTIONS_INGEST_ADAPTER, () => syntheticControl);
logger.info("ingest adapter selected", { adapter: adapter.name });
const allowPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);
const allowNbboPublish = buildThrottle(env.TESTING_MODE, env.TESTING_THROTTLE_MS);
@ -421,7 +426,10 @@ const run = async () => {
rawPrint.ts
);
const equityQuote = parsedMetadata.underlying_id
? selectAtOrBefore(equityQuoteHistoryByUnderlying.get(parsedMetadata.underlying_id), rawPrint.ts)
? selectAtOrBefore(
equityQuoteHistoryByUnderlying.get(parsedMetadata.underlying_id),
rawPrint.ts
)
: null;
const print = enrichOptionPrint(rawPrint, optionQuote, equityQuote, optionsSignalConfig);
@ -500,8 +508,16 @@ const run = async () => {
const pruneTimer = setInterval(() => {
const removed =
pruneContextHistory(nbboHistoryByContract, env.OPTION_CONTEXT_MAX_KEYS, env.OPTION_CONTEXT_TTL_MS) +
pruneContextHistory(equityQuoteHistoryByUnderlying, env.OPTION_CONTEXT_MAX_KEYS, env.OPTION_CONTEXT_TTL_MS);
pruneContextHistory(
nbboHistoryByContract,
env.OPTION_CONTEXT_MAX_KEYS,
env.OPTION_CONTEXT_TTL_MS
) +
pruneContextHistory(
equityQuoteHistoryByUnderlying,
env.OPTION_CONTEXT_MAX_KEYS,
env.OPTION_CONTEXT_TTL_MS
);
logger.info("option context cache summary", {
nbbo_context_keys: nbboHistoryByContract.size,
equity_quote_context_keys: equityQuoteHistoryByUnderlying.size,

View file

@ -1,6 +1,12 @@
import { mkdir } from "node:fs/promises";
export type EventCalendarKind = "earnings" | "dividend" | "corporate_action" | "m_and_a" | "news" | "other";
export type EventCalendarKind =
| "earnings"
| "dividend"
| "corporate_action"
| "m_and_a"
| "news"
| "other";
export type EventCalendarEntry = {
underlying_id: string;
@ -56,7 +62,8 @@ const asNumber = (value: unknown): number | null => {
return null;
};
const asString = (value: unknown): string | null => (typeof value === "string" && value.trim() ? value.trim() : null);
const asString = (value: unknown): string | null =>
typeof value === "string" && value.trim() ? value.trim() : null;
const parseCsvLine = (line: string): string[] => {
const values: string[] = [];
@ -139,9 +146,14 @@ export const parseEventCalendarEntries = (value: unknown): EventCalendarEntry[]
const record = row as Record<string, unknown>;
const underlying = asString(record.underlying_id ?? record.underlying ?? record.symbol);
const eventTs = asNumber(record.event_ts ?? record.event_time ?? record.event_date);
const announcedTs = asNumber(record.announced_ts ?? record.available_ts ?? record.as_of_ts ?? record.created_ts) ?? 0;
const announcedTs =
asNumber(
record.announced_ts ?? record.available_ts ?? record.as_of_ts ?? record.created_ts
) ?? 0;
const rawKind = asString(record.event_kind ?? record.kind ?? record.type) ?? "other";
const eventKind = EVENT_KINDS.has(rawKind as EventCalendarKind) ? (rawKind as EventCalendarKind) : "other";
const eventKind = EVENT_KINDS.has(rawKind as EventCalendarKind)
? (rawKind as EventCalendarKind)
: "other";
if (!underlying || eventTs === null || eventTs < 0 || announcedTs < 0) {
return [];
@ -162,7 +174,9 @@ export const parseEventCalendarEntries = (value: unknown): EventCalendarEntry[]
});
};
export const createStaticEventCalendarProvider = (entries: EventCalendarEntry[]): EventCalendarProvider => {
export const createStaticEventCalendarProvider = (
entries: EventCalendarEntry[]
): EventCalendarProvider => {
const byUnderlying = new Map<string, EventCalendarEntry[]>();
for (const entry of entries) {
const key = normalizeUnderlying(entry.underlying_id);
@ -184,15 +198,20 @@ export const createStaticEventCalendarProvider = (entries: EventCalendarEntry[])
}
const bucket = byUnderlying.get(key) ?? [];
const entry = bucket.find((candidate) => candidate.announced_ts <= asOfTs && candidate.event_ts >= asOfTs);
const entry = bucket.find(
(candidate) => candidate.announced_ts <= asOfTs && candidate.event_ts >= asOfTs
);
return entry ? { ...entry, days_to_event: (entry.event_ts - asOfTs) / MS_PER_DAY } : null;
}
};
};
export const createEmptyEventCalendarProvider = (): EventCalendarProvider => createStaticEventCalendarProvider([]);
export const createEmptyEventCalendarProvider = (): EventCalendarProvider =>
createStaticEventCalendarProvider([]);
export const loadEventCalendarProviderFromFile = async (path: string): Promise<EventCalendarProvider> => {
export const loadEventCalendarProviderFromFile = async (
path: string
): Promise<EventCalendarProvider> => {
const text = await Bun.file(path).text();
return createStaticEventCalendarProvider(parseEventCalendarEntries(JSON.parse(text)));
};
@ -212,7 +231,9 @@ export const fetchAlphaVantageEarningsCalendar = async (
const response = await (options.fetchFn ?? fetch)(url);
const text = await response.text();
if (!response.ok) {
throw new Error(`Alpha Vantage earnings calendar request failed: ${response.status} ${text.slice(0, 160)}`);
throw new Error(
`Alpha Vantage earnings calendar request failed: ${response.status} ${text.slice(0, 160)}`
);
}
if (/^(?:\s*\{|\s*Thank you for using Alpha Vantage)/i.test(text)) {
throw new Error(`Alpha Vantage returned a non-calendar response: ${text.slice(0, 200)}`);
@ -221,7 +242,10 @@ export const fetchAlphaVantageEarningsCalendar = async (
return parseAlphaVantageEarningsCalendar(text, options.nowTs ?? Date.now());
};
export const writeEventCalendarEntries = async (path: string, entries: EventCalendarEntry[]): Promise<void> => {
export const writeEventCalendarEntries = async (
path: string,
entries: EventCalendarEntry[]
): Promise<void> => {
const directory = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
if (directory) {
await mkdir(directory, { recursive: true });

View file

@ -12,9 +12,14 @@ const logger = createLogger({ service });
logger.info("service starting");
const eventCalendarPath = process.env.REFDATA_EVENT_CALENDAR_PATH ?? process.env.SMART_MONEY_EVENT_CALENDAR_PATH;
const eventCalendarProvider = process.env.REFDATA_EVENT_CALENDAR_PROVIDER ?? process.env.EVENT_CALENDAR_PROVIDER;
const refreshMs = Math.max(0, Number(process.env.REFDATA_EVENT_CALENDAR_REFRESH_MS ?? 86_400_000) || 0);
const eventCalendarPath =
process.env.REFDATA_EVENT_CALENDAR_PATH ?? process.env.SMART_MONEY_EVENT_CALENDAR_PATH;
const eventCalendarProvider =
process.env.REFDATA_EVENT_CALENDAR_PROVIDER ?? process.env.EVENT_CALENDAR_PROVIDER;
const refreshMs = Math.max(
0,
Number(process.env.REFDATA_EVENT_CALENDAR_REFRESH_MS ?? 86_400_000) || 0
);
const getAlphaVantageOptions = (): AlphaVantageEarningsCalendarOptions | null => {
const apiKey = process.env.ALPHA_VANTAGE_API_KEY;
@ -33,7 +38,9 @@ const getAlphaVantageOptions = (): AlphaVantageEarningsCalendarOptions | null =>
const refreshEventCalendar = async (): Promise<void> => {
if (!eventCalendarPath) {
logger.warn("event calendar refresh disabled; missing SMART_MONEY_EVENT_CALENDAR_PATH or REFDATA_EVENT_CALENDAR_PATH");
logger.warn(
"event calendar refresh disabled; missing SMART_MONEY_EVENT_CALENDAR_PATH or REFDATA_EVENT_CALENDAR_PATH"
);
return;
}
if (eventCalendarProvider !== "alpha_vantage") {

View file

@ -52,11 +52,7 @@ type ReplayStreamKind = "options" | "nbbo" | "equities" | "equity-quotes";
type ReplayEvent = OptionPrint | OptionNBBO | EquityPrint | EquityQuote;
type FetchAfter = (
afterTs: number,
afterSeq: number,
limit: number
) => Promise<ReplayEvent[]>;
type FetchAfter = (afterTs: number, afterSeq: number, limit: number) => Promise<ReplayEvent[]>;
type ReplayStream = {
kind: ReplayStreamKind;
@ -79,7 +75,12 @@ const STREAM_DEFS: Record<
subject: string;
streamName: string;
rank: number;
fetchAfter: (client: ReturnType<typeof createClickHouseClient>, afterTs: number, afterSeq: number, limit: number) => Promise<ReplayEvent[]>;
fetchAfter: (
client: ReturnType<typeof createClickHouseClient>,
afterTs: number,
afterSeq: number,
limit: number
) => Promise<ReplayEvent[]>;
}
> = {
options: {
@ -196,7 +197,9 @@ const getEventIngestTs = (event: ReplayEvent): number =>
const getEventSeq = (event: ReplayEvent): number => (Number.isFinite(event.seq) ? event.seq : 0);
const pickNextEvent = (streams: ReplayStream[]): { stream: ReplayStream; event: ReplayEvent } | null => {
const pickNextEvent = (
streams: ReplayStream[]
): { stream: ReplayStream; event: ReplayEvent } | null => {
let choice: { stream: ReplayStream; event: ReplayEvent } | null = null;
for (const stream of streams) {
@ -313,7 +316,8 @@ const run = async () => {
kind,
subject: def.subject,
streamName: def.streamName,
fetchAfter: (afterTs, afterSeq, limit) => def.fetchAfter(clickhouse, afterTs, afterSeq, limit),
fetchAfter: (afterTs, afterSeq, limit) =>
def.fetchAfter(clickhouse, afterTs, afterSeq, limit),
buffer: [],
cursor: { ...startCursor },
done: false,

View file

@ -8,6 +8,6 @@
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"noEmit": true,
},
"noEmit": true
}
}