◆ Flux

Host integration — descriptors, registries and extension seams

Flux does not draw anything. It expresses content — indicators, representation transforms, drawing geometry, scenes, depth values — and the host applies runtime modes: a 2-D or 3-D projection, the chart type the user picked, the pane layout, the persistence. The language produces the artifacts; the modes consume them.

That split is the whole architecture, and it has a sharp practical consequence: a script and a built-in must be indistinguishable to the host. If a user’s Point & Figure implementation registers itself in the same table, in the same shape, with the same hooks as the native candle renderer, then extensibility is not a feature bolted on the side — it is the same road the first-party code already drives on.

This page specifies the contracts at that boundary: what a descriptor is, what the registries promise, which two gaps in the host must close for representations to be scriptable at all, and which seams are deliberately held open for what comes next.

New here? Start with Guide §8 — The four planes →

The scope boundary

Inside the language — content, consumed by a registry or a descriptor:

indicators · representations (chart types) · authored drawings and custom drawing tools · canvas scenes and overlays · transitions · alerts · depth and 3-D values · panes, declaratively (inferred from kinds — there is no createPane()) · parameter UI (derived from input).

Outside the core — mutating application state: enabling another script, persisting, reconfiguring the application. That is the host’s job. A script’s interactivity stays cosmetic (on click -> spawn/tween/flash), bounded, and repaint-free; toggling the visibility of its own output is allowed.

A command layer — buttons that activate scripts, change the layout — exists, but as a separate declarative plane (the APP plane), never on the analysis plane. That is what preserves totality, the firewall, and no-repaint no matter how rich the surrounding application becomes.

Four locks

Four decisions are expensive to retrofit and are therefore frozen up front:

  1. The x axis is an ordinal index plus a time mapping — never “the time”. Position is dataX(i); the timestamp never enters the x computation.
  2. depth/z is a first-class coordinate, not a 3-D feature.
  3. The plane split and the descriptors are compilation targets, not conventions.
  4. Registries accept script-registered entries in the same shape as built-ins.

The five descriptors

The five descriptors Figure — four contracts tie the language to the registries and the clock; a fifth, cosmetic, drives transitions.

① Clock / ordinal

A clock is a producer of a series: an ordinal index, a length, the bar store, and two mappings — timeAt(i) (index → time, the source of the time stream) and idxAt(sec) (time → index, round-to-nearest and clamped, used to anchor drawings).

Constructors: tf(token) is time-coarse aggregation; renko(box), pnf(box, rev) and range(r) are price re-bucketizers — the same slot, with a price threshold instead of a time one. @ routes to one of three paths: same step (a native no-op), coarser (a remap), finer (a sample at close).

Seven codegen invariants govern everything that compiles through this contract:

Invariant
I1 position is the index — never the timestamp
I2 idxAt is only a seed: the resample locator is a floor-containing pointer (Tₖ ≤ t < Tₖ₊₁), never idxAt(t) − 1. Round-to-nearest is look-ahead, and look-ahead is repaint.
I3 causal: a closed unit only, never one still forming ⇒ repaint is inexpressible
I4 the grid is real (timeAt), never assumed uniform
I5 one clock per series in v1 — a clock of a clock is not expressible
I6 a leaf node mapped to a native kernel is byte-identical to it, warm-up included: a Flux indicator on a clock is the same citizen as a built-in
I7 the interpreter and the compiled WASM produce the same bytes, checked at every compilation

I2 deserves its own sentence, because it is the one an implementer gets wrong: anchoring a drawing wants the nearest bar; resampling an indicator wants the last closed one. Using the anchoring mapping for the resample silently reads the future.

I6 and I7 are byte-equality invariants the clock contract relies on rather than owns: a leaf mapped to a native kernel matches it byte for byte, warm-up included, and the interpreter and the compiled WASM agree on every compilation. The harness that proves this, and what “byte-identical” is checked against, is canonical in Verification and Compiler and runtime — summary here, definitions there.

② Depth / z

The firewall here is not an argument — it is a property of the code. The overlay collector takes no 3-D parameter; only the host knows the camera angle, through a depth factor in [0,1] applied downstream by the shader. So an angle of zero is pixel-identical to plain 2-D, by construction rather than by care.

Flux emits a depth node (an ordinary analysis series) and the at z: binding; the host projects it and owns the window, the slider, the camera and the collapse. Every overlay instance pivots in z — line, band, cloud, profile — so 2-D and 3-D consume the same artifact.

at z: accepts any scalar and auto-normalizes it according to the source kind; a depth value, already a normalized fraction, shunts the normalization. Honest status: no kernel in the catalogue produces depth today, so the kind is theoretical in v1 while the z space is real — it is frozen now because retrofitting a coordinate is expensive.

③ Representation descriptor

A chart type is an id, a class, six hooks, and one metadata member:

RepresentationDescriptor = {
  id, klass: 'A1' | 'A2' | 'B',
  transform(raw, params)  -> Series          // A1 = identity ; A2 = a same-length derived store ; B = re-binned COLUMNS
  reduce(bars, …)         -> aggregate       // the LOD decimator — breach #1
  renderPrimitive(frame)  -> elements        // authored as `render`
  updateLastUnit(el, …)   -> bool            // in-place mutation of the head unit
  liveReduce(state, tick) -> extend | append // A = in place ; B = extend a column, or reverse → append
  persistKey(unit)        -> key             // the non-lossy anchor — breach #2
  capabilities { … }                         // METADATA — the 7th member, not a hook
}

Representation hooks Figure — the six hooks and where each one meets the host: the class is derived from the transform, not from the render primitive.

klass and capabilities are derived, never authored. The grammar admits the id and the six hooks; the compiler deduces the rest — exactly as pane and scale are deduced from a kind:

What the hooks do klass seriesKind persistence
the transform / clock re-bins price (an ordinal x, a non-injective time mapping) B follows the render primitive (column, or line for a polyline) its own slot
the transform derives a same-length store (a line, a Heikin-Ashi) A2 ohlc shared
the transform is the identity (a raw candle) A1 ohlc shared

The distinction is worth stating precisely because it is easy to get backwards: the class follows the RE-BIN, not the render primitive. A Kagi draws a polyline and a Renko draws a brick, yet both are class B — because both re-bin price. That is what routes them to the column-correct decimator and to their own persistence slot, rather than to the verbatim aggregation path a candle uses.

④ Registries open to scripts

The three registries — indicators, drawings, representations — are already type-agnostic. Nothing in them tests a “is this a script?” flag. An entry written in Flux is indistinguishable from a built-in the moment it has the same shape:

Registry Entry shape
indicators { id, label, category, mode, defaults, params, series, compute } + a recursive/windowed/batch descriptor
drawings { barExtent, priceExtent, render, hitTest, + LOD } — hitTest and the LOD are derived by the host from the render geometry, never authored
representations the descriptor above

What must be built is a dynamic registration mechanism — the tables are static literals frozen at boot. The recommended shape is a second table consulted after the built-in one, so the native hot path is not touched at all. This is an opening, not a new substrate.

⑤ Transition descriptor

Cosmetic, and deliberately outside the registries. Today the morph is driven by an ad-hoc plan object; the contract reifies it into a named type — duration, easing, wave, stagger spread, wick lead, surplus policy, chrome fade, hold deadline, flip timing — plus per-call overrides (over D, stagger, surplus:) and a per-representation morph: hook.

The heavy per-candle morph stays native. Flux orchestrates it: it injects the plan once.

How Flux compiles to these contracts

Nothing new is introduced under the language. Each construct lands on a seam that already exists:

Construct Compiles to
clock + @ a series producer; the @ node routes no-op / remap / sample, the expression itself computed by the native engine
depth, at z: an analysis node exposed as a series key, consumed by the z-source and the projector
representation an entry in the render-series table; morph fills the transition plan
an indicator a registry entry (label, params, series inferred from the inputs and the kinds) plus a recursive/windowed/batch descriptor, accepted with no flag — and therefore served exactly like a built-in, byte for byte

The hot path — the stepper kernels, the columnar aggregation, the candle renderer, the depth packing, the morph — stays native and byte-identical. Flux generates the artifacts the seams already consume.

The two breaches

Two gaps in the host must close before any price-driven representation — script or native — can work. They were identified and costed independently of Flux; Flux merely rides on them.

Breach #1 — the per-type reduce hook (level of detail). The chart decimates bars for the zoom level by merging them, blind, through one columnar aggregator. For an OHLC series that is correct. For a re-binned column series it is wrong: merging by min/max collapses an alternation of up-columns and down-columns into one fat body with a false direction, off the grid. The fix is small and byte-safe: the bar store gains a kind; the aggregation call is gated on it; ohlc keeps the existing aggregator verbatim (zero pixels change), while column routes to the descriptor’s own decimator — same signature, same return, a drop-in.

Breach #2 — persistence scoped by type. The drawings key is (asset, timeframe) with no discriminant, and the anchor is a raw timestamp. Both break for a re-binned representation: several columns can share a bar’s time (the time mapping is non-injective, so a drawing lands on the wrong column), and one key mixes the drawings of two different chart types. The fix adds a representation discriminant to the key and routes anchoring through the persistKey hook — timestamps for class A (unchanged), a representation-stable anchor (price plus a box ordinal) for class B.

The decisive test: Point & Figure as a script

The question that settles whether the architecture works is simple: can a fully price-driven chart type be written as a library script, with no change to the core? Point & Figure is the hardest case, so it is the one to answer.

FLUX
representation pnf(box, rev) {
  transform:      rebin(close, box, rev)                  // price → X/O columns: a price clock
  render:         column{ at: (clock.index, lo), h: hi - lo, color: if dir == 1 then up else down }
  reduce:         columnDecimate(bars, k)                 // the column-correct decimator (breach #1)
  liveReduce:     extendOrAppend(state, tick)             // extend the head column, or reverse → append
  updateLastUnit: mutateHead(el, unit)                    // mutate the head column in place
  persistKey:     (lo, clock.index)                       // a price + box-ordinal anchor (breach #2)
}

Each hook takes a value — an expression, a block, or a render primitive. (The bodies above are named for readability; a real implementation inlines them.)

Every piece types, and the type system forces the physics — the box must be a level, a displacement, because anchor + count * box only type-checks that way (see Kinds). The column state is an ordinary bounded scan:

FLUX
// the column state: a record whose kind is  record{ dir: dir, extreme: price, count: num }
def column(box, rev) =
  scan({ dir: 1, extreme: close, count: 0 }, (p) -> advance(p, box, rev))

count is dimensionless, so count * box is a level and extreme + count * box is a price. Causality holds: the column advances on closed price, and a past column is frozen.

Piece Contract Status
pnf(box, rev) as a clock ① the re-bucketizer must be built (it depends on breach #1)
transform / render / updateLastUnit ③ the shapes already exist in the host
klass: 'B', seriesKind: 'column', own-slot persistence ③ derived from the hooks — the deduction must be built
reduce ③ + breach #1 the gate must be built
liveReduce + a length guard ③ to build
persistKey ③ + breach #2 to build
the column state the lattice a pure script — the record kind makes the scan typable
optional depth: (z proportional to column volume) ② inherited for free — the projection is generic
the registry entry ④ dynamic registration must be built

The honest tension. A single time bar can cross many boxes in a flash move, so cost per bar is not trivially constant. That is capped — maxBricksPerBar — and beyond the cap the host aggregates rather than blowing the budget. The cap is a design constant, not something the lattice can derive.

Point & Figure is therefore entirely expressible as a library script, with the two breaches as the only core modifications. Renko, Kagi, three-line-break and Range follow a fortiori — they are strictly simpler instances of the same class.

Cross-series and multi-asset

The chart is multi-asset and multi-currency, so the language expresses cross-series work from the start, with no new grammar:

FLUX
btc    = series("BTC-USD")
eth    = series("ETH-USD")
spread = btc.close / eth.close                                   // ratio — plottable
corr   = stat.correl(returns(btc.close), returns(eth.close), 30) // osc(-1,1)

Reserved extension seams

Every future capability enters through one of two doors, which is what keeps the analysis core untouched:

The seams held open, with their honest status. This table records which seam is open and where it stands; the capability verbs behind the namespaces — what each grants, and how a script requests it — are catalogued in host-services, their canonical home.

Seam Status
First-class input (input.key, input.pointer, edge events, a focus/ownership model) v1 covers pointer and touch; keyboard, pointer-lock and gamepad plug in without a rewrite
A retained scene with pluggable targets — one renderer for 2-D, chart and 3-D; a world3D space; declarative vetted 3-D primitives Held open and inert: v1 ships 2-D and the chart’s 3-D; a general 3-D scene rolls out after it
Parallelism Realized in v1 — the scheduler runs the pure graph; it adds no power to the core
Assets and kernels by handle (asset:load, a vetted-kernel escape hatch) designed
The capability namespace (input:*, gpu:*, net:*, data:source, app:launch, wallet:*, social:* …) extensible by the same mechanism
External data through consent (net:fetch), typed by a declared schema v1, client-side; a server proxy rolls out after v1
A third-party asset source (data:source) — registering a series producer Rolls out after v1, vendor-verified; ingestion is causal and append-only, so no-repaint survives
Module visibility (private / package / pub) v1
An embeddable chart API that accepts Flux scripts as arguments, sandboxed v1
Chain and wallet (wallet:* / chain:*) — the script builds an intent, the wallet signs v1 for connect/read/simulate/send; contract calls roll out after v1
Identity and social (social:* / present:*) — host-resolved, pairwise-opaque handles v1 for contacts, invite and the data channel; vendor-verified tier, never anonymous — the same tier as data:source and chain:send. A/V media is gated on the deferred capture consent

The principle behind the table: no seam ever adds power to the core. It adds an input stream, or an output target mediated by a capability. Games, live spreadsheets, a 3-D scene — all of them are special cases of those two doors.

See also