Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .sobelow-skips
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ RCE.CodeModule: Code Execution in `Code.eval_file`,lib/ccxt_ocx/bundle_surface/m
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/bundle_surface/compile.ex:121,3835524
Traversal.FileModule: Directory Traversal in `File.mkdir_p!`,lib/ccxt_ocx/bundle_surface/manifest.ex:51,623409F
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/bundle_surface/compile.ex:165,69BB3C4
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/tiers/compile.ex:68,7844D5D
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/tiers/compile.ex:68,7844D5D
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/bundle_surface/compile.ex:127,3D28FF5
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/declarations/compile.ex:175,6A320B6
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/bundle_surface/compile.ex:83,7F523E6
Traversal.FileModule: Directory Traversal in `File.read!`,lib/ccxt_ocx/declarations/compile.ex:180,2BFD6B
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ All notable changes to `ccxt_ocx` are recorded here. The format follows

## [Unreleased]

### Phase 2: Macro-Driven Method Generation

#### Task 6: Discover and parse CCXT declaration sources (`CcxtOcx.Declarations`)
**Completed** | [D:6/B:8/U:8 → Eff:1.33] 📋

Compile-time parser that turns CCXT's real TypeScript declarations into rich
per-method terms (name, params, return type, owning surface, overrides) for
the Phase 2 macro layer to consume.

- New `CcxtOcx.Declarations` facade + `CcxtOcx.Declarations.Compile` parser — walks `js/src/base/Exchange.d.ts`, the per-exchange `*.d.ts`, and `pro/*.d.ts` with `OXC.parse/2` + `OXC.collect/2`, classifying methods by `:base`, `:exchange`, or `:pro` surface and capturing per-exchange overrides.
- Filter ownership (verb-prefix allowlist, internal-prefix denylist, exact-name denylist, bare-name trade-plane allowlist) centralized here. `CcxtOcx.BundleSurface.Compile` now delegates to `Declarations.Compile.public_unified_method?/1` so the "what gets a `defunified` wrapper" decision lives in one place.
- Overrides map keyed by `"surface:exchange_id"` so a method declared in both `js/src/<id>.d.ts` and `js/src/pro/<id>.d.ts` (e.g. `kucoinfutures.fetchBidsAsks`) keeps both override entries.
- Loud layout guard raises with actionable instructions if CCXT bumps and the three smoke methods (`fetchTicker`, `createOrder`, `watchTicker`) disappear from the base surface.

### Phase 5: Production Hardening

#### Task 21: PromEx plugin (`CcxtOcx.PromEx.Plugin`)
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

The public API is generated at compile time from CCXT's type definitions — **never hand-written**. CCXT's surface is ~100 exchanges × ~50 unified methods × (REST + WS) × (public + private); hand-written per-method wrappers don't scale. The macro layer is the contract; the adapter behind it (JS via QuickBEAM, or native Elixir for the venues that matter) is an implementation detail.

**Planned macro surface** (no macros implemented yet — Phase 1 foundation modules (`Runtime`, `RuntimePool`, `Error`, `Tiers`, `BundleSurface`, `Telemetry`) are in place; status per macro in [ROADMAP.md](ROADMAP.md)):
**Planned macro surface** (no macros implemented yet — Phase 1 foundation modules (`Runtime`, `RuntimePool`, `Error`, `Tiers`, `BundleSurface`, `Telemetry`) are in place, plus Phase 2's `Declarations` (Task 6) as the compile-time data source the macros below will consume; status per macro in [ROADMAP.md](ROADMAP.md)):

| Macro | Phase | Role |
|---|---|---|
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<!-- TASKS:BEGIN phase=2 -->
| Task | Status | Notes |
|------|--------|-------|
| Task 6 | | 🎁 **macros** · 🚀 **v0_1** · Discover and parse CCXT declaration sources with OXC [D:6/B:8/U:8 → Eff:1.33] 📋 |
| Task 6 | | 🎁 **macros** · 🚀 **v0_1** · Discover and parse CCXT declaration sources with OXC [D:6/B:8/U:8 → Eff:1.33] 📋 |
| Task 6b | ⬜ | 🎁 **macros** · 🚀 **v0_1** · `use CcxtOcx` — exchange-scope entrypoint [D:5/B:9/U:9 → Eff:1.8] 🚀 |
| Task 7 | ⬜ | 🎁 **macros** · 🚀 **v0_1** · `defunified` macro [D:7/B:10/U:9 → Eff:1.36] 📋 |
| Task 8 | ⬜ | 🎁 **macros** · 🚀 **v0_1** · Typed structs for unified return shapes (with declarative field mapping) [D:5/B:8/U:8 → Eff:1.6] 🚀 |
Expand Down
62 changes: 7 additions & 55 deletions lib/ccxt_ocx/bundle_surface/compile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ defmodule CcxtOcx.BundleSurface.Compile do
See the quickbeam skill for the canonical pattern.
"""

alias CcxtOcx.Declarations

@bundle_relative "node_modules/ccxt/dist/ccxt.browser.min.js"
@exchange_dts "node_modules/ccxt/js/src/base/Exchange.d.ts"
@load_timeout to_timeout(second: 30)
Expand All @@ -32,50 +34,10 @@ defmodule CcxtOcx.BundleSurface.Compile do
# Keeps verification fast while still exercising the important signing/WS paths.
@default_sample_exchanges ["binance", "bybit", "okx", "deribit", "coinbaseexchange"]

# Verb prefixes that mark a method as part of the public unified surface.
@verb_prefixes ~w(fetch create watch cancel edit loadMarkets set close describe)

# Prefixes that mark a method as internal/base-class helper machinery and
# MUST be filtered out even if they collide with a verb prefix (e.g. `parse`,
# `handle`, `safeMarket`).
@internal_prefixes ~w(parse handle sign request safe market nonce define extend)

# Exact-name denylist for CCXT base-class helpers that *do* match a verb
# prefix and *don't* match an internal prefix (so the prefix heuristic alone
# captures them) but are not part of the unified public surface. Pollution
# source: CCXT base Exchange ships these as plumbing — pagination, partial
# balance access, HTTP retry, webpage fetch, safe-dict construction, and the
# `loadMarkets` internal helper. Letting them stay in the manifest would mean
# Phase 2's `defunified` macro generates wrappers for internal helpers with no
# documented contract. Extend when future CCXT versions add new helpers that
# slip past the prefix filter.
@additional_denied ~w(
fetch2
fetchPaginatedCallCursor
fetchPaginatedCallDeterministic
fetchPaginatedCallDynamic
fetchPaginatedCallIncremental
fetchPartialBalance
fetchWebEndpoint
createSafeDictionary
loadMarketsHelper
)

# Trade-plane methods that don't match the verb-prefix filter but are part of
# the public unified surface (CCXT exposes them as bare-name methods on
# exchanges that support them — withdrawals, transfers, isolated/cross
# margin adjustments). Without this allowlist they'd be dropped from the
# snapshot and never get a defunified wrapper.
@additional_unified_methods ~w(
withdraw
transfer
addMargin
reduceMargin
borrowCrossMargin
borrowIsolatedMargin
repayCrossMargin
repayIsolatedMargin
)
# Filter ownership moved to CcxtOcx.Declarations.Compile (Task 6).
# The four lists and the predicate now live in one place so the "public unified
# surface" decision is the single source of truth for both the legacy name
# extractor and the rich declaration parser.

@doc """
Absolute path to the CCXT browser bundle (same logic as Tiers.Compile).
Expand Down Expand Up @@ -124,7 +86,7 @@ defmodule CcxtOcx.BundleSurface.Compile do
names =
OXC.collect(ast, fn
%{type: :method_definition, key: %{name: name}} = _node ->
if public_unified_method?(name), do: {:keep, name}, else: :skip
if Declarations.Compile.public_unified_method?(name), do: {:keep, name}, else: :skip

_ ->
:skip
Expand Down Expand Up @@ -197,16 +159,6 @@ defmodule CcxtOcx.BundleSurface.Compile do

# --- Private helpers ------------------------------------------------------

@spec public_unified_method?(String.t()) :: boolean()
defp public_unified_method?(name) do
has_good_prefix = Enum.any?(@verb_prefixes, &String.starts_with?(name, &1))
has_bad_prefix = Enum.any?(@internal_prefixes, &String.starts_with?(name, &1))
in_allowlist = name in @additional_unified_methods
in_denylist = name in @additional_denied

(has_good_prefix or in_allowlist) and not has_bad_prefix and not in_denylist
end

@spec apply_browser_stubs(pid()) :: :ok
defp apply_browser_stubs(rt) do
{:ok, _} =
Expand Down
51 changes: 51 additions & 0 deletions lib/ccxt_ocx/declarations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule CcxtOcx.Declarations do
@moduledoc """
Public compile-time surface for CCXT declaration parsing (Task 6).

This module (and its `Compile` submodule) are the single source of truth for
the unified method surface extracted from CCXT's own TypeScript declarations.

Consumers in Phase 2 (Task 6b `use CcxtOcx`, Task 7 `defunified`, Task 9
`defexchange`) call `parse_unified_surface/0` at compile time to obtain the
rich per-method terms that drive macro expansion.

The underlying parser uses OXC (`OXC.parse/2 + OXC.collect/2`) against the
real declaration files inside `node_modules/ccxt/js/src/...` (not the barrel
`ccxt.d.ts`). It distinguishes `:base`, `:exchange`, and `:pro` surfaces and
captures overrides.

## Layout stability

The parser contains loud guards that raise with actionable messages if the
expected CCXT file layout changes or the smoke-test methods disappear. This
turns "CCXT bumped and broke our codegen" into a fast, obvious failure instead
of a mysterious macro bug.
"""

alias CcxtOcx.Declarations.Compile

# Recompile the facade when ANY discovered declaration source changes — base,
# per-exchange, or pro. Limiting this to the base file would let
# `parse_unified_surface/0` return stale terms after `mix npm.update ccxt`
# touches only an exchange-specific or pro .d.ts.
for path <- [Compile.base_dts_path()] ++ Compile.exchange_dts_paths() ++ Compile.pro_dts_paths() do
@external_resource path
end

@doc """
Parse the discovered CCXT declaration surface and return one rich term per
public unified method.

See `CcxtOcx.Declarations.Compile.method_term/0` for the exact shape.
"""
@spec parse_unified_surface() :: [Compile.method_term()]
def parse_unified_surface, do: Compile.parse_unified_surface()

@doc "Delegates to `Compile.public_unified_method?/1` (the single source of truth)."
@spec public_unified_method?(String.t()) :: boolean()
def public_unified_method?(name), do: Compile.public_unified_method?(name)

@doc "Absolute path to the core Exchange.d.ts (convenience for docs / tests)."
@spec base_dts_path() :: String.t()
def base_dts_path, do: Compile.base_dts_path()
end
Loading
Loading