[Architecture Decision Records]
ADR-001 through ADR-014 for AXIS: registry, AXIS≠engine, Solid+Vite, dual engines, configSchema, URL load, storage, CF project id, proxies, DO fan-out, migration, CORS, results export, transport preference + telemetry.
Architecture Decision Records
Formal ADRs for AXIS (product + frontend/). Status values: Accepted unless noted. Dates are conceptual product epochs (SuperChart → AXIS), not git archaeology.
Index
| ID | Title |
|---|---|
| ADR-001 | Pluggable unified registry |
| ADR-002 | AXIS ≠ engine |
| ADR-003 | Solid + Vite product UI |
| ADR-004 | Dual engines (server + Pyodide) |
| ADR-005 | Declarative configSchema |
| ADR-006 | URL-loadable plugins |
| ADR-007 | Storage as a plugin |
| ADR-008 | Frozen CF project id pynescript-superchart |
| ADR-009 | Worker proxies first |
| ADR-010 | Durable Object stream fan-out |
| ADR-011 | State namespace migration |
| ADR-012 | CORS as deploy concern |
| ADR-013 | Results export in AXIS |
| ADR-014 | Transport preference + connection telemetry |
ADR-001: Pluggable unified registry
Context
Early SuperChart registered sources/streams/engines in separate ad hoc maps. Operators and authors needed one mental model and one install path.
Decision
Adopt a unified PluginRegistry (frontend/src/plugins/registry.ts) with kinds:
source | stream | engine | storage | component(reserved)
Every plugin shares PluginBase (id, name, kind, description, version, builtIn, configSchema, capabilities, lifecycle hooks). Registration is ordered; listeners observe register/unregister.
Consequences
- Active selection lives in the Solid store, not the registry.
- Built-ins protected from casual unregister.
- Catalogs (
sources/catalog.ts, …) become registration facades. - Contract docs under Plugins.
ADR-002: AXIS ≠ engine
Context
Closed chart hosts couple rendering and language runtime. That blocks offline mode, alternate evaluators, and headless reuse of PYNE.
Decision
AXIS never embeds a closed interpreter. All evaluation goes through EnginePlugin.run. UI modules call getActiveEngine() / runAndApply, not Python bindings directly (except inside the pyodide engine module).
Consequences
- Language fidelity owned by PYNE.
- AXIS docs do not duplicate grammar theory.
- New engines (tiny demo, remote Worker, future WASM) are additive plugins.
ADR-003: Solid + Vite product UI
Context
Legacy vanilla JS shell (main.js, style.css) mixed DOM wiring with business logic and aged TV-blue tokens.
Decision
Product path is SolidJS + Vite + Tailwind 4, entry src/index.tsx / app.tsx. Imperative chart library (lightweight-charts) is isolated in ChartHost / PaneManager so Solid reconciliation does not thrash canvas DOM.
Consequences
- Legacy files retained for smoke/static tests only (
LEGACY.md). - Icons via
lucide-solid. - Deploy artifact is Vite
dist/, not repo-root static tree.
ADR-004: Dual engines (server + Pyodide)
Context
Researchers need server fidelity and offline demos. A single transport cannot satisfy both without compromise.
Decision
Ship two built-in engines:
| Id | Role |
|---|---|
server | POST {endpoint}/run?mode= with { script, data: bars } |
pyodide | Browser Python + vendored pynescript / antlr wheels |
Both implement the same EnginePlugin contract and return RunResult.
Consequences
- Endpoint field only for server-like engines.
- Pyodide requires correct static asset deployment (ZIP integrity checks).
- Timeouts unified at runner layer.
ADR-005: Declarative configSchema
Context
Per-plugin settings UIs do not scale; custom React/Solid forms per plugin forks the manager.
Decision
Plugins declare optional configSchema: map of field name → { type, default, label, options, min, max, ... }. Settings and library panels resolve defaults + pluginsConfig overrides via shared resolve helpers.
Consequences
- New plugins get settings UI without shell changes (within field types).
- Types:
string | number | boolean | selectinitially. - Invalid user values remain a plugin responsibility at runtime.
ADR-006: URL-loadable plugins
Context
Third-party sources/engines should not require rebuild of the PWA for experiments.
Decision
Support loadPluginFromUrl: fetch ES module, validate shape, register, persist install metadata for restore on boot. Example plugins published under public/plugins/.
Consequences
- Browser origin trust model: URL plugins are code execution.
- Security tests constrain schemes and storage-via-URL footguns.
- Offline restore needs prior successful fetch/cache.
ADR-007: Storage as a plugin
Context
Script library began as localStorage-only; cloud and git were bolted differently.
Decision
Storage is a first-class plugin kind with list/read/write/remove (+ optional draft/sync/status). Built-ins: local (IDB), cloud (Worker /api/scripts), git (GitHub/GitLab Contents API). Active storage in activePlugins.storage.
Consequences
- Manager Script Library is backend-agnostic.
- Git commits only on explicit save; drafts local.
- Cloud uses Bearer Pro keys and optional optimistic concurrency.
ADR-008: Frozen CF project id pynescript-superchart
Context
Renaming Cloudflare Pages/Worker projects severs KV/D1/R2 bindings, custom domains, and CI secrets. Product brand evolved to AXIS.
Decision
Keep infrastructure name pynescript-superchart. Brand in UI/manifest/docs is AXIS. Health JSON may identify pynescript-axis-worker. Optional DNS aliases (axis.*) point at the same project without rename.
Consequences
wrangler.tomlnameand Pages--project-namestay frozen.- Docs call out the freeze explicitly (this ADR).
- npm package name may lag brand.
ADR-009: Worker proxies first
Context
In-Worker Pyodide is attractive but heavy; production needed a path immediately.
Decision
Worker /api/run proxies to an external Python backend first (EXTERNAL_BACKEND). In-Worker runtime remains scaffolded/gated. Keys, usage metering, scripts API, and stream DO can ship independently of full edge evaluation.
Consequences
- AXIS
serverengine talks to Worker URL transparently. - Operational dependency on Flask (or equivalent) until edge runtime matures.
- Clear layering: Worker is data plane + auth, not necessarily the interpreter.
ADR-010: Durable Object stream fan-out
Context
N browser clients each opening venue WebSockets waste connections and risk rate limits.
Decision
SessionDO Durable Object: one upstream WS per session key; fan-out to N client sockets via /api/stream. Hibernation-friendly design on CF.
Consequences
- Stream plugins may target DO URLs (example plugin provided).
- Session/symbol/interval query params are part of the contract.
- Not a historical source—live only.
ADR-011: State namespace migration
Context
Rename SuperChart → AXIS would brick user layouts and libraries if keys changed hard.
Decision
Canonical key pynescript.axis.v1. On read, fall back to pynescript.superchart.v2 / v1 (and parallel library/editor keys), then write forward to AXIS keys. Do not keep dual-write forever.
Consequences
- Seamless upgrade for existing browsers.
- Persistence strips bars/lastRun/logs.
- Tests cover migration (
tests/state.test.tspatterns).
ADR-012: CORS as deploy concern
Context
Browser AXIS on origin A calling Pro API on origin B fails without CORS. Hardcoding origins in AXIS is wrong.
Decision
CORS is a backend/deploy configuration (ALLOWED_ORIGINS on Flask; Worker headers as deployed). AXIS documents required origins; ops sets explicit lists in production (avoid perpetual * outside demos).
Consequences
- Localhost regex for dev.
- Troubleshooting centers on preflight, not engine code.
- Static file server does not replace API CORS.
ADR-013: Results export in AXIS
Context
Researchers need artifacts (trades CSV, run JSON) without a separate analytics service.
Decision
Results panel owns export: download JSON of lastRun, CSV of closed trades, clipboard helpers. Strategy pairing and stats run client-side from engine events (results/strategy.ts, results/events.ts).
Consequences
- Export fidelity bounded by engine event quality.
- No server-side report store required for MVP.
- Viewer metrics ≠ broker statements (documented for operators).
ADR-014: Transport preference + connection telemetry
Context
Operators need to see how data and calculation move (WS vs REST vs local vs brokered), whether the live socket is actually open, and engine run latency. Naively “prefer WebSocket everywhere” is wrong: venues expose history over REST and live klines over WSS. Server engines batch-run over HTTP; Pyodide is local WASM.
Decision
- Transport policy
- History → REST (or local mock/CSV).
- Live → WebSocket first, paired per source via
defaultStreamForSource(binance-rest→binance-ws, …). - Engine → prefer
WS /ws/runon the Pro API when available (flask-sock); fall back to HTTPPOST /run. Pyodide remains local. - Brokered → CF Durable Object /
needsProxystreams surface as transport classbroker.
- Honest stream state —
connectinguntilonStatus({ state: 'open' }); reconnect usesreconnecting/ degraded telemetry, not false green. - Venue streams use reconnect with exponential backoff (
streams/reconnect-ws.ts). - Connection HUD — StatusBar second row (
ConnectionHud) reads ephemeralstore.telemetry(SRC/STR/ENG/STO chips, tick pulse, engine latency). Persist onlytelemetry.hudprefs +live.preferAfterLoad/live.rerunOn. - Live re-run modes —
every-tick(default) orbar-close(venueBar.closedor bar time advance). - Chart stability — full
setDataToChartonly on history load (chartDataGen); live usesappendBar; overlay/script drawings update in place to avoid hide/show flash.
Consequences
- UI never claims WS for historical load.
- Auto-live after Load is opt-in (
preferAfterLoad, default off). - Plugin capabilities may declare
transport; otherwise HUD classifies from id/capabilities. - See Streams, UI shell, Overview.
ADR lifecycle
New ADRs should:
- State context, decision, consequences.
- Cross-link code paths.
- Avoid re-litigating ADR-002 without supersession note.