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:
- 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. depth/z is a first-class coordinate, not a 3-D feature.- The plane split and the descriptors are compilation targets, not conventions.
- Registries accept script-registered entries in the same shape as built-ins.
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
}
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.
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:
// 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:
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)- Referencing —
series(key)returns a record whose columns carry the asset tag derived from the host’s metadata:open/high/low/close : price[base, quote],volume : volume[base]. The key is a host-allowlisted resolution key, distinct from the tag it yields. - Alignment is as-of, and causal — the foreign series is aligned onto the chart’s ordinal
axis by taking the most recent foreign bar with timestamp ≤ the current bar’s time (the
same floor-containing rule as I2). No future bar is ever visible; a gap holds the last known
value; before the first foreign bar the value is
na. No-repaint is inherited, not re-argued. - Kinds make it safe —
price[BTC,USD] + price[ETH,USD]is[ErrDim], and so isprice[BTC,USD] + price[BTC,EUR]. A dollar is not a euro (asset & currency). - Compilation — the graph declares its asset dependencies; the host fetches and aligns them and hands them to the engine as additional input columns. More inputs; no new substrate.
Reserved extension seams
Every future capability enters through one of two doors, which is what keeps the analysis core untouched:
- Input — every signal from the world (a key, a pointer, a tick, a price, a pick) becomes either a readable stream or an event, through a single ingestion point. Replay and determinism stay uniform because everything is journaled.
- Output — every heavy render or computation is executed by the host under a capability, on the presentation side, contained by the firewall. GPU and wall-clock non-determinism never propagates into the core.
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
- Kinds — why
boxmust be alevel, and how the lattice forces the physics. - Time and state — clocks,
@, and the floor-containing rule (I2). - App plane — contributions, slots, and the layout boundary.
- display — scenes, panes, the draw-list chain, the 3-D model.
- Compiler and runtime — I6 and I7, and what “byte-identical” is checked against.
- Packages — how a third-party representation is distributed and pinned.