API reference

The imperative API reference

Shipped · support rows generated · substrate reads experimental

The half of Fundamental you reach from code. createField and its 41 options, the FieldHandle — all 55 entries — the sampling and observable reads, the capability-scoped agent view, policy and budgets, the body handle, and the event bus. Every entry carries a per-platform support row, rendered from the generated parity matrix. What you write in markup lives on the declarative reference instead; this page links across rather than repeating it.

How to read a support row. JS ✓ means the capability exists on that plane — however that plane spells it. An idiom difference is an equivalent, not a gap: a setter and a constructor field are collapsed to one capability before comparing, the same way data-body and .fieldBody(tokens:) are on the declarative side. A Kotlin — is therefore a real gap. Nothing here is hand-typed: the chips are read out of data/parity-matrix.json, which pnpm check:docs regenerates and diffs on every CI run.

Creating a field

Every entry point hands back the same handle. @fundamental-engine/core is host-agnostic and needs an explicit host; @fundamental-engine/vanilla, the React FieldField and <field-root> wire browserHost() for you. On <field-root> the handle is proxied onto the element, so the element is the handle.

import { createField } from '@fundamental-engine/core';
import { browserHost } from '@fundamental-engine/dom';

// core is host-agnostic: it needs a host, and nothing else
const field = createField({ host: browserHost(), density: 1.2, render: 'dots' });

field.setFormation('wells');
field.setRender('links');
field.destroy();          // release the loop + listeners
PlaneHow a field is configuredConfiguration lanes at that layer
JSthe `FieldOptions` object (plus the handle’s `setX(…)` setters)43 of 51
Swiftthe `FieldOptions` struct (plus the protocol’s `setX(_:)` requirements)30 of 51
Kotlin`createField(host, …)` arguments plus the handle’s `setX(…)` setters24 of 51

Read that honestly. Kotlin has no options struct at all — it configures at createField(host, …) and through the handle's setters, so its column counts both. That is an idiom difference. What the counts do show is a real spread: the web plane carries the whole visual-tuning vocabulary (the gradient, grid and wave lanes, the overlay-canvas seam) because the engine owns the draw there, while the ports hand drawing to a host and expose the physics knobs instead.

createField — the 41 options

Each option is also a kebab-case <field-root> attribute; setting the attribute after mount calls the matching setter on the live handle. The support row is for the capability — whether that plane can configure it at all, at construction or live.

Where the field lives

`createField` throws without a host. `@fundamental-engine/vanilla`, the React component and `<field-root>` all supply one for you — reach for a host directly when you are writing your own surface.

host FieldHost · default required JS ✓ Swift ✓ Kotlin ✓
The environment seam (viewport, scroll, rAF, canvas). createField throws without it — pass browserHost() from @fundamental-engine/dom, or use @fundamental-engine/vanilla / the web component, which wire it for you.
bounds HTMLElement · default window web surface only
Contained fields (#540): scope the field to a single element instead of the viewport. A vanilla createField / new FieldField option — the field measures and renders inside the given element (a card, a panel), so a component can carry its own local field. Omit for the default full-window field. (containerHost() is the lower-level door.)

Matter and motion

How much matter there is, how it moves, and how it comes to rest.

density number · default 1 JS ✓ Swift ✓ Kotlin —
Particle-count multiplier.
depth number · default 0 JS ✓ Swift — Kotlin ✓
Optional z volume. 0 (the default) is the flat field, byte-identical to the 2D engine; > 0 opens a shallow depth the matter drifts through, projected as a size/alpha recession. Purely additive — no API requires z.
depthFocal number · default 480 JS ✓ Swift — Kotlin —
DECLARED render reference point (Wallpaper Rule, #975): the perspective focal length (CSS px) of the depth mode's camera — larger flattens the perspective, smaller exaggerates it. Formerly a hardcoded FOCAL = 480; the default reproduces it exactly, so the depth render is byte-identical. Only affects the depth render mode (pairs with depth > 0). Experimental.
separation number · default 0 JS ✓ Swift ✓ Kotlin ✓
Strength of short-range particle-to-particle separation/repulsion force ∈ [0,1] to break up clumps in high-density areas. <field-root separation>.
mass boolean · default false JS ✓ Swift — Kotlin —
First-class mass: particle mass ∝ size and body forces accelerate by a = F/m; dynamic bodies also get inertial mass ∝ rendered area (#872).
integrator 'legacy' | 'fixed' | 'velocity-verlet' · default 'legacy' JS ✓ Swift ✓ Kotlin —
Integration scheme (substrate doc 04 §Step 3, #659). 'fixed' opts into the partially frame-rate-corrected integrator: the FRICTION/HEAT_DECAY decays scale with dt (FRICTION^dt), so damping is consistent across frame rates — but by design it does NOT yet dt-scale force impulses (unsound for pair forces that mutate a neighbour in the same pass; deferred to the force-contract change, integrator.ts applyForce / doc-04 §Step 3); at the reference rate (dt === 1, and every golden/conformance run) it is byte-identical to the default 'legacy' semi-implicit Euler, so opting in never moves the golden. 'velocity-verlet' opts into the second-order velocity-Verlet scheme: the position full-step uses the previous step's stored acceleration (x += v·dt + ½·a·dt²), the force pass evaluates a′ at the updated position, and the velocity takes the half-step average (v += ½·(a + a′)·dt); kinematic velocity-replacing forces (jet/wall/lens/gate/warp) bypass the average as discontinuities, and decays are dt-scaled like 'fixed'. Higher positional accuracy, NOT a conservation guarantee (energy/momentum stay non-conserved by design; the per-step decay keeps it non-symplectic) — trajectories differ from 'legacy' by design once opted in. The default path never engages either mode. <field-root integrator>. Experimental.
restingMotion { mode: 'thermal' | 'flow'; strength?: number } · default undefined (off) JS ✓ Swift ✓ Kotlin —
The resting-motion floor (DECLARED, default OFF). Signals-first defaults leave a DRAWN field (render: 'dots') settling into its attract wells and freezing when idle; this is the honest, global alternative to painting waves — a small per-particle impulse the field MEASURES as --temperature, with nothing drawn. 'thermal' is the thermal force's Langevin kick applied field-wide (Box–Muller through the injected rng, so a seeded run reproduces); 'flow' is a divergence-free curl (the stream function ψ = sin(kx+φ)·sin(ky−φ), one eddy ≈ 400px) with a slow phase drift so it never settles. strength multiplies the impulse (default 1; 0 = off). Both scale with dt, so reduced motion (dt = 0) contributes exactly nothing. Default-off ⇒ the cross-plane conformance golden is untouched. <field-root resting-motion="thermal"> / <field-root resting-motion="flow 0.5">. Mirrored on Swift (FieldOptions.restingMotion) and Kotlin (createField(restingMotion =)).
ambientOrbit number · default 0.1 JS ✓ Swift ✓ Kotlin —
DECLARED ambient bias (Wallpaper Rule, #978): the resting ambient formation's tangential swirl injected into attract bodies — the gentle spiral free matter traces around a well at rest. Formerly a hardcoded 0.1 painted into the default formation preset; now a documented dial defaulting to that value (resting behavior unchanged). Set 0 for a purely radial attract (no spiral). Applies to the ambient formation only. <field-root ambient-orbit>.
ambientWander number · default 1.0 JS ✓ Swift ✓ Kotlin —
DECLARED ambient bias (Wallpaper Rule, #978): the resting ambient formation's wander term — the per-particle drift that keeps resting matter alive rather than frozen. Formerly a hardcoded 1.0 in the default formation preset; now a documented dial defaulting to that value (behavior unchanged). Lower for a calmer rest, 0 to still the drift. Applies to the ambient formation only. <field-root ambient-wander>.
rng () => number · default Math.random JS ✓ Swift ✓ Kotlin —
Random source for ALL engine randomness — particle seeding, spawn scatter, jitter, release angles (#371). Supply a seeded generator to make a run reproducible (the record/replay seam).
now () => number · default performance.now JS ✓ Swift — Kotlin —
Wall-clock source for input-idle tracking (#371) — one of the three engine clocks (wall / frame / simulation). Defaults to performance.now; override in tests or replay harnesses.

Colour and the substrate

What the field is drawn with. The palette lane is tracked as its own parity dimension after a port collapsed it to a single accent (#1090).

accent string · default palette's first stop JS ✓ Swift ✓ Kotlin ✓
The travelling accent color (a hex string).
palette string | string[] · default 'ours' JS ✓ Swift ✓ Kotlin ✓
Accent template — a built-in name or custom hex stops.
theme string · default 'warm' JS ✓ Swift — Kotlin —
Ambient theme preset — 'warm' | 'cool' | 'mono'. Sets the free-particle heat ramp and the background-wave baseline (distinct from the travelling accent). 'warm' reproduces the shipped default.
background 'opaque' | 'transparent' · default 'opaque' JS ✓ Swift ✓ Kotlin ✓
Substrate background. 'transparent' clears to transparent instead of painting the near-black substrate, so the underlay composites over light content (an image, a 3D scene, a light page) — trails fade to transparent rather than to black. Also a <field-root background> attribute and live via setBackground.
gradientWarm string · default theme's JS ✓ Swift — Kotlin —
Hex for the warm (energized) end of the heat ramp — overrides the theme. <field-root gradient-warm>.
gradientCool string · default theme's JS ✓ Swift — Kotlin —
Hex for the cool (resting) end of the free-particle heat ramp — overrides the theme. <field-root gradient-cool>.
redshiftObserver { x: number; y: number } · default { x: 0.5, y: 0.5 } JS ✓ Swift — Kotlin —
DECLARED render reference point (Wallpaper Rule, #975): the observer the redshift mode measures each particle's radial velocity against (receding reddens, approaching blues), as viewport fractions ∈ [0,1]. Formerly a hardcoded (W/2, H/2); the default reproduces it exactly, so the redshift render is byte-identical. Only affects the redshift render mode. Experimental.

The draw layers

What is painted, and onto what. An overlay is a silent no-op without an overlay canvas — the single most common "nothing happened" in the imperative path.

render 'none' | 'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'knockout' | 'redshift' | 'blackbody' | 'depth' · default 'none' JS ✓ Swift ✓ Kotlin ✓
Render mode for the underlay surface (behind content) — the same physics drawn differently. The default is 'none' (#538): the signals-only engine runs the full simulation + feedback (--d, --load, capture events) but draws nothing — the field is behavior first. Opt into a visible surface with 'dots' (particles) or another mode.
overlay OverlayMode | OverlayMode[] · default 'off' JS ✓ Swift ✓ Kotlin ✓
Field Surfaces: the overlay READING(S) drawn in front of content (see the overlay-readings table) — one reading or an additive stack. Set alongside render. <field-root> manages the front canvas (space-separated tokens in the attribute); for createField directly, pass overlayCanvas.
overlayCanvas HTMLCanvasElement · default undefined JS ✓ Swift — Kotlin —
Field Surfaces: a caller-provided canvas for the overlay surface (drawn in front of content). The web component creates/manages this for you; pass it only when calling createField directly — createOverlaySurface() in @fundamental-engine/dom creates and places the same fixed, click-through, aria-hidden canvas (configurable blend / z-index) so you need not hand-roll it (#721).
overlayCanvasProvider () => HTMLCanvasElement | null · default undefined JS ✓ Swift — Kotlin —
Field Surfaces: a lazy alternative to overlayCanvas (#676). When no overlayCanvas is set, core calls this once — the first time an overlay reading actually goes active — to obtain the surface. Lets a host defer creating its full-viewport mix-blend canvas until an overlay is switched on, so the common overlay:off path never adds a canvas to the compositing tree at boot. <field-root> uses this internally; return null to decline. Ignored when overlayCanvas is set.
overlayBackend RenderBackend · default Canvas 2D JS ✓ Swift — Kotlin —
Drawing backend for the overlay surface (#373) — the structural seam a WebGL/WebGPU surface implements. Defaults to the Canvas 2D implementation over overlayCanvas. See render-backend.ts.
heatmap boolean · default false JS ✓ Swift ✓ Kotlin ✓
Density heatmap — a glow layer of where matter pools, sampled back to bodies as --field-heatmap-density.
heatmapFade { start: number; span: number } · default { start: 0.3, span: 0.85 } JS ✓ Swift — Kotlin —
DECLARED page-layout reference (Wallpaper Rule, #975): how the density heatmap glow fades with scroll position — full above start·H, gone by (start+span)·H (H = viewport height). Formerly a hardcoded (1.15 - scrollY/H)/0.85 "content = first viewport" assumption; the default reproduces it exactly (full through 0.3 viewports, gone by 1.15), so the heatmap glow is byte-identical. Set a large span to disable the scroll fade. Only affects the density heatmap layer. Experimental.
heatCenter { x: number; y: number } · default { x: 0.5, y: 0.4 } JS ✓ Swift — Kotlin —
DECLARED render reference point (Wallpaper Rule, #975): the center of the cool→warm heat vignette the dots/depth swarm is tinted against, as viewport fractions ∈ [0,1] (resolved to px each frame). Formerly a hardcoded (W/2, H·0.4); the default reproduces it exactly, so the default render is byte-identical. Only affects the dots/depth render modes. Experimental.
gridIntensity number · default 0.16 JS ✓ Swift — Kotlin —
Stroke intensity (opacity) of the grid overlay lattice ∈ [0,1] — turns the warped grid from a faint diagnostic into a visual centerpiece (#552). 0.16 is the calibrated default; raise it (~0.4) to make the bending space the hero of a section. Only affects the grid overlay mode. <field-root grid-intensity>.
gridWarp number · default 1 JS ✓ Swift — Kotlin —
Distortion multiplier for the grid overlay (the reference lattice displaced by the field). 1 is the calibrated "legible, never chaotic" deflection; 2–3 exaggerates the deformation for demos; 0 flattens the lattice. Only affects the grid overlay mode. <field-root grid-warp>.
waves boolean · default false JS ✓ Swift ✓ Kotlin ✓
Draw the background Currents (the wave layers + bound shimmer). Opt-in (#979): a bare field has no carrier waves; pass true for the ambient resting structure.
waveStyle 'linear' | 'circular' · default 'linear' JS ✓ Swift ✓ Kotlin ✓
The wave current pattern. linear draws parallel horizontal waves; circular draws concentric orbits circling around the waveCenter or the star body. <field-root wave-style>.
waveCenter Vec2 | (() => Vec2) | null · default null JS ✓ Swift ✓ Kotlin ✓
The center coordinate (or coordinate-provider fn) for circular waves. Defaults to the first body tagged with star or vortex, or the center of the field. <field-root wave-center> (space-separated, e.g. "200 300").
waveBaseline string[] · default theme's JS ✓ Swift — Kotlin —
Hex stops for the background-wave baseline — overrides the theme. <field-root wave-baseline> (space-separated).

The two-way loop

Where the field writes its readings back to. The default DOM sink writes CSS custom properties; a `feedbackSink` takes the same readings as plain data, which is how a non-DOM host — or a test — consumes them.

feedbackSink FeedbackSink · default undefined JS ✓ Swift ✓ Kotlin —
Advanced: route per-body density/feedback writes to the platform FeedbackRegistry instead of letting the engine write the DOM (Phase D3).
reaction boolean · default false JS ✓ Swift — Kotlin —
Newtonian own-emission reaction (#873): a dynamic body feels the equal-and-opposite of the net impulse it imparts to nearby matter (a directional emitter recoils like a rocket). Best paired with mass. Experimental.
attention boolean · default false JS ✓ Swift ✓ Kotlin ✓
Conserved attention — one finite strength budget; engaging a body starves the others.
causality boolean · default false JS ✓ Swift ✓ Kotlin ✓
Cross-boundary causality — a saturated body spills density to its neighbours.

Cost, identity and permission

The three lanes that are about governance rather than looks.

dprCap number · default 2 JS ✓ Swift ✓ Kotlin ✓
Backing-store device-pixel-ratio ceiling (#410) — the dominant fill-rate lever. Effective DPR = min(devicePixelRatio, dprCap); capping at ~1.5 buys ~1.8× headroom on retina for a small perceptual softening. Runtime-settable via setDprCap. <field-root dpr-cap>.
identify (el: HTMLElement) => FieldBodyIdentity | undefined · default undefined JS ✓ Swift ✓ Kotlin ✓
First-class body identity resolver: derive a stable, structured FieldBodyIdentity ({ id, namespace?, kind?, host? }) from a DOM-scanned body's element, called once per body. The identity is surfaced on query()/snapshot() body readings and used as the stable key for diff/replay/relationships. Return undefined to fall back to the default derivation (the element DOM id, else a monotonic body-N). Purely additive — a field without identify behaves exactly as before.
policy FieldPolicy · default undefined JS ✓ Swift ✓ Kotlin ✓
Runtime FIELD POLICY — what THIS host/session/user/app PERMITS at runtime (distinct lane from governance, which is what doctrine allows via static lint). FieldPolicy { allowBodyDataInSnapshots?, allowMotionProjection?, maxMotionBudget? (0..1), budgets?: Partial<FieldBudgets> }; FieldBudgets bounds consumable resources: motion/force/attention/thermal/render/privacy/accessibility/agentRead. WIRED today: the motion budget folds (via min) with reduced-motion + perf pressure into the effective motion the integrator/easing reads — reduced-motion always wins (a policy can lower motion, never raise it); the privacy budget (+ allowBodyDataInSnapshots) gates body data in snapshot(). Other budgets are declared-not-yet-enforced. Change it live with setPolicy. Purely additive — a field with no policy behaves exactly as before.

The FieldHandle

55 entries covering the handle's 52 callable members and 4 properties — one entry documents setPolicy together with the policy it reads back. The handle is not at full parity across the planes, and it never was: the JS surface has grown faster than the ports. The numbers below are computed from the ports themselves.

PlaneCallable membersProperties
JS524
Swift533
Kotlin5916

Those totals are not a ranking: the union is 61 members and every plane is missing some of it. The divergence runs both ways — see what the ports have that the web does not below.

The handle itself — lifecycle and provenance

What the object is, what it promises, and how to take it down. `destroy()` is not optional in a single-page app: the field owns a rAF loop and window listeners.

guarantees JS ✓ Swift — Kotlin —
Read-only reproducibility envelope: determinism classification, which inputs are controlled, which are not, the requirements for repeatability, and the cross-plane numeric tolerance. Read this before building replay, shared state links, or server-authoritative simulation — the answer is conditionally-deterministic, and the conditions are listed rather than implied.
version (property) JS ✓ Swift — Kotlin —
Readonly string — the running engine version (= FIELD_VERSION), i.e. which build this field is on (#547). For a consumer that wants to assert or log the engine it loaded. Shipped-but-unfrozen.
scan() JS ✓ Swift ✓ Kotlin ✓
Re-scan the document for [data-body] bodies after a DOM change.
rescan() JS ✓ Swift ✓ Kotlin ✓
Alias of scan().
setVisible(on) JS ✓ Swift ✓ Kotlin ✓
Element-level visibility hint: setVisible(false) skips all draw work (render + overlay) each frame while the simulation and its feedback signals stay live — scrollV(), --d, --load, capture events keep flowing. Distinct from the tab-level pause (visibilitychange already stops the loop entirely). <field-root> wires it automatically from an IntersectionObserver on the host. Shipped-but-unfrozen.
scrollV() experimental JS ✓ Swift ✓ Kotlin ✓
The engine's eased page-scroll velocity — the same EMA the scrolling condition gate reads: (prev × 0.7) + (|Δscroll| × 0.3) per frame. Units are px/frame at the display refresh rate (refresh-rate dependent — roughly half on 120 Hz; may normalize to px/ms before 1.0). Mirrored to --field-scroll-v on :root by the platform runtime. Pull-based: read on demand, don't poll in tight loops. Shipped-but-unfrozen.
destroy() JS ✓ Swift ✓ Kotlin ✓
Stop the loop and release listeners.

Appearance and field shape

The live twin of the `<field-root>` attributes — every one of these setters is what the element calls when you change the matching attribute after mount. Markup and runtime control are the same surface.

setAccent(hex) JS ✓ Swift ✓ Kotlin ✓
Recolor the travelling accent.
setPalette(name | hex[]) JS ✓ Swift ✓ Kotlin ✓
Swap the accent color template live.
setFormation(name) JS ✓ Swift ✓ Kotlin ✓
Switch the global formation.
setWaveStyle(style) JS ✓ Swift ✓ Kotlin ✓
Switch the wave current layout style live ('linear' | 'circular').
setWaveCenter(center) JS ✓ Swift ✓ Kotlin ✓
Set the custom wave center coordinate ({x, y} or function) live.
setSeparation(strength) JS ✓ Swift ✓ Kotlin ✓
Set particle-to-particle separation/repulsion force strength live.
setBackground(mode) JS ✓ Swift ✓ Kotlin ✓
Switch the substrate live: 'transparent' clears to transparent so the underlay composites over light content; 'opaque' restores the near-black substrate. Additive.
setRender(mode) JS ✓ Swift ✓ Kotlin ✓
Switch the underlay render mode (behind content): dots / trails / links / metaballs / voronoi / streamlines / flow / knockout / redshift / blackbody / depth.
setOverlay(mode | mode[]) JS ✓ Swift ✓ Kotlin ✓
Field Surfaces: render overlay reading(s) in front of content — one reading or an additive stack (the readings compose). The vocabulary: streamlines / force-vectors / field-lines / grid / temperature / energy / path / data, or off. Pairs with setRender.
setHeatmap(on) JS ✓ Swift ✓ Kotlin ✓
Toggle the density heatmap layer live (a glow of where matter pools).
threads(list | null) JS ✓ Swift ✓ Kotlin ✓
Wire glowing connector lines between an engaged set, or clear with null.

Behaviour, cost and permission

The levers that change what the field is allowed to spend. `setDprCap` and `setQualityTier` are the wired performance levers; `setPolicy` is the permission lane — what this host, session or user permits.

setAttention(on) JS ✓ Swift ✓ Kotlin ✓
Toggle conserved attention live (one finite strength budget).
setCausality(on) JS ✓ Swift ✓ Kotlin ✓
Toggle cross-boundary causality live (density spills to neighbours).
setDprCap(cap) / opt dprCap JS ✓ Swift ✓ Kotlin ✓
Backing-store DPR ceiling (#410) — the dominant fill-rate lever. Effective DPR = min(devicePixelRatio, dprCap), default 2; capping at ~1.5 buys ~1.8x headroom on retina for a small softening. As a FieldOptions key (createField / <field-root dpr-cap>) or the runtime setDprCap setter (re-sizes immediately). Shipped-but-unfrozen.
setQualityTier(tier) JS ✓ Swift ✓ Kotlin ✓
Adaptive quality (#413): drop the field to a cheaper tier (0 full → 3 lightest) reversibly. Maps the tier to the engine's own fill levers — caps the effective backing-store DPR (1.5 / 1.25 / 1) and skips the heaviest ambient layer (the heatmap glow) at tier 2+. <field-root> wires this automatically from the QualityGovernor on sustained frame-budget overruns; call it yourself to drive quality from your own signal. Shipped-but-unfrozen.
setPolicy(policy) / policy / opt policy JS ✓ Swift ✓ Kotlin ✓
Runtime FIELD POLICY — what THIS host/session/user/app PERMITS (distinct lane from governance = what doctrine allows via static lint). setPolicy REPLACES (not merges) the live policy ({} clears to the unbounded default); field.policy reads a frozen copy. FieldPolicy carries allowBodyDataInSnapshots / allowMotionProjection / maxMotionBudget (0..1) / budgets (Partial<FieldBudgets>: motion, force, attention, thermal, render, privacy, accessibility, agentRead). WIRED: the motion budget folds (via min) with reduced-motion + perf pressure into the effective motion the integrator/easing reads — reduced-motion always wins (a policy can lower motion, never raise it); the privacy budget (+ allowBodyDataInSnapshots) gates body data in snapshot(). Other budgets declared-not-yet-enforced. Purely additive. Shipped-but-unfrozen.

Driving the field

Push on the simulation directly — a shove, a moving focus, a data binding onto the particles.

burst(x, y, hex?) JS ✓ Swift ✓ Kotlin ✓
A one-shot shove + heat near a point, optionally tinting the matter.
flowTo(x, y, opts?) JS ✓ Swift ✓ Kotlin ✓
Place/move a dynamic flow focus the field bends toward — pulls matter in and curves the streamlines. Retarget it each frame to follow the pointer, an element, or a path. opts: { strength?, radius? }.
clearFlow() JS ✓ Swift ✓ Kotlin ✓
Remove the flow focus — the field relaxes back to its bodies-only shape.
seed(atoms) JS ✓ Swift ✓ Kotlin ✓
Bind a data record to each base particle, round-robin. Each record's weight ∈ [0,1] scales that particle's mass + size. Re-applied across resize/density rebuilds.

Bodies, edges and agents — participation without an element

The non-DOM counterpart of the `[data-body]` scan. A Three.js mesh, a native view, a game entity or a headless model joins the field through these.

addBody(spec) JS ✓ Swift ✓ Kotlin ✓
Add a programmatic body (no DOM) from a spec — the sanctioned alternative to the [data-body] scan for a non-DOM host (Three.js mesh, native view). { tokens, strength?, range?, spin?, angle?, color?, rect:()=>box, data?, onFeedback? }; rect() samples the box in field px each frame. The body carries a data record and takes per-body feedback (channels demuxed from the global sink); survives rescan. Returns { data, channels, set(params), remove() } — set({ strength?, range?, angle?, spin?, color? }) mutates the force params live on the measure cadence (no rescan, no remove+re-add; a token change still needs remove+addBody). Shipped-but-unfrozen.
addEdge(a, b, opts?) JS ✓ Swift ✓ Kotlin ✓
Relate two programmatic bodies — the non-DOM relationship counterpart of addBody (a, b are addBody handles). The edge carries a live RelationshipAgent: it STRENGTHENS while its source body is salient (gathering matter) and decays while idle, accumulating memory — so a non-visual consumer (an agent modelling file↔meeting↔app) gets the relationship layer + its longitudinal warmth, with no DOM. opts: { type?, strength?, direction? }. Returns an EdgeHandle { set({ strength?, type? }), remove() }; read the live graph back with readEdges(). Shipped-but-unfrozen.
readEdges() JS ✓ Swift ✓ Kotlin ✓
The live programmatic-edge read-out (addEdge) for a non-visual consumer — an array of { from, to (the endpoint bodies' data records), type, strength, memory, active }. Pure, read-only: the relationship graph + its dynamics the way readParticles is the swarm. Shipped-but-unfrozen.
addAgent(spec) JS ✓ Swift ✓ Kotlin ✓
Add an engine-stepped agent — a participant the integrator MOVES (vs sample(), where you integrate yourself). It lives in the particle pool, so it feels every force the swarm feels (body forces AND particle-level hunt/align/cohesion); each step its report(p) fires so an external transform (a THREE.Object3D) follows it. spec: { x, y, z?, mass?, maxSpeed?, species?, report }. maxSpeed caps it, species lets tagged bodies (data-affects) steer it selectively; it edge-bounces (not wraps) and is excluded from readParticles. Returns { particle, remove() }. The creatures primitive @fundamental-engine/three's layer.addAgent binds over. Shipped-but-unfrozen.

Sampling — what the field is doing at a point

Pure, read-only probes in field-pixel space. They work headless and under `render: none`, which is what makes an invisible field useful.

sample(x, y) JS ✓ Swift ✓ Kotlin ✓
The net field force a still test particle would feel at (x, y), as { x, y } in field-pixel space — every visible body superposed (wells, dipole structure, flow bias). Pure and read-only, samplable at any resolution; the seam external visualizers consume for vector grids, streamline tubes, or mesh displacement. Shipped-but-unfrozen.
sampleScalar(x, y) JS ✓ Swift ✓ Kotlin ✓
The smooth diffused density scalar ∈ [0,1] at (x, y) — the heatmap grid, bilinear-sampled, so its gradient stays meaningful at a source (forage-by-gradient), unlike a nearest-body readout. Requires the heatmap layer (createField({ heatmap: true }) / setHeatmap(true)); returns 0 when off. Read-only, updated each frame including under render:none. Shipped-but-unfrozen.
sampleGradient(x, y) JS ✓ Swift ✓ Kotlin ✓
The gradient ∇ {x,y} of the density field at (x, y) — direction + steepness (1/px) of increasing matter density. The analytic companion to sampleScalar, off the same diffused heatmap grid, so it stays non-degenerate at a source (a real uphill slope where a nearest-body density flattens to zero) — the cue reliable forage-/flee-by-gradient steers by. Requires the heatmap layer; returns { x: 0, y: 0 } when off or empty. Pure, read-only, maintained under render:none. Shipped-but-unfrozen.
grid(name) JS ✓ Swift ✓ Kotlin ✓
Open a named host-authorable ScalarGrid — the engine field-buffer primitive (the same one diffuse/memory/propagate run on), promoted to a public surface for application fields the simulation composes with (a scent map, a wear/desire-path layer, a goal attractor). { sample, deposit, gradient, decay, clear } in field px. Created on first access, kept viewport-sized, advanced each frame by its mode (wave… = wave, memory… = slow decay, else diffuse); a same-named force shares the buffer. Shipped-but-unfrozen.
addField(name, sampler) JS ✓ Swift ✓ Kotlin ✓
Register a named field CHANNEL — an external scalar field the engine samples on its own read path (terrain height, soil moisture, a heat map). The open INPUT analog of the render surfaces (setRender/setOverlay are bundled output layers): instead of bolting a parallel grid alongside the field, hand it a pull-based sampler (x, y) => number and read it back through sampleField, so a consumer queries ONE field, not two. The sampler is called on demand — keep it cheap. AMENDED at #443 (was "never cached"): sampleField still caches nothing, but a body declaring relief admits the channel as a potential and the engine rasterises it into a held grid, invalidated by addField/set/remove/resize (never on a frame cadence). With no such body, nothing is cached. Returns a FieldChannelHandle { name, set(sampler), remove() } to swap the sampler live or unregister. (Force coupling — a force reading a channel as a potential — is a separate opt-in; this is the read substrate.) Shipped-but-unfrozen.
sampleField(name, x, y) JS ✓ Swift ✓ Kotlin ✓
Sample a channel registered with addField at (x, y) in field-pixel space; returns 0 for an unregistered channel. Pure, read-only. Shipped-but-unfrozen.
atomAt(x, y) JS ✓ Swift ✓ Kotlin ✓
The seeded record on the nearest particle to (x, y) within ~24 px, or null. For hover-to-inspect.
focusAt(x, y) JS ✓ Swift ✓ Kotlin ✓
Hold + highlight the nearest seeded particle; return its record — the dwell affordance before a click. Returns null if no particle is in range.
clearFocus() JS ✓ Swift ✓ Kotlin ✓
Release the focused particle; it resumes drifting.

The swarm read-out

Zero-allocation copies of live particle state into caller-owned typed arrays — the seam an alternative renderer draws from without touching the internal pool.

particleCount() experimental JS ✓ Swift ✓ Kotlin ✓
Live size of the particle pool. Use for external budget monitors or debug overlays without walking the particle array. Shipped-but-unfrozen.
energy() experimental JS ✓ Swift ✓ Kotlin ✓
Per-frame energy snapshot: { kinetic, thermal, total, count }. Forwards to energyReport() without requiring a reference to the internal particle array. Shipped-but-unfrozen.
readParticles(out) JS ✓ Swift ✓ Kotlin ✓
Copy live particle state into a caller-owned Float32Array (stride 5: x, y, z, heat, size — z is the optional depth lane, 0 in a flat field); returns the count written = min(particleCount(), floor(out.length/5)). Zero-alloc and read-only — the render-agnostic swarm read-out an alternative surface (e.g. @fundamental-engine/three) draws from. Shipped-but-unfrozen; the stride may widen (a color lane) before 1.0.
readParticleIds(out) JS ✓ Swift ✓ Kotlin ✓
Copy each live particle's stable id into a Uint32Array, parallel to readParticles (same order, same agent skip), so ids[i] is the identity of the particle at stride offset i*5. Lets a host track a seeded entity across frames and key its own opaque payload off the id. Zero-alloc, read-only. Shipped-but-unfrozen.
readParticleChannels(channels, out) JS ✓ Swift ✓ Kotlin ✓
Read multiple named channels from live particles into caller-owned Float32Array buffers (column-wise: all particles' first channel, then second, etc.). Returns the particle count written. Channels: 'x' | 'y' | 'z' | 'vx' | 'vy' | 'heat' | 'size' | 'm' | 'id' | 'age' | 'charge'. Unknown channels write 0. Mirrors readParticles() agent-exclusion behavior. Zero-alloc alternative to readParticles when you need a subset of channels. Shipped-but-unfrozen.

The observable surface — structured, serializable reads

Plain data instead of pixels: ask the field a question, capture what it is doing, compare two captures, and narrate the difference. This is the surface a test, a tool or an agent consumes.

query(q?) JS ✓ Swift ✓ Kotlin ✓
Ask the live field a structured question and get back plain, serializable data — the agent-/tool-/test-readable surface. q = { at?, radius?, include? }: at is a point ({x,y}), a DOMRect-shaped rect ({x,y,width,height}, so el.getBoundingClientRect() drops straight in), or omitted for a whole-field query; radius (default 240) sizes a point query; include picks sections (bodies | metrics | relationships | influences). Returns { frame, time, region?, bodies, metrics, relationships, influences } — bodies carry id/rect/tokens/metrics/dimensions/activeFormations, relationships are the edge graph by id, and influences attribute per-force contribution at the point (from the impulse accumulator) — each carries a `channel` (`linear` Δv or `thermal` heat, doc 04 §Step 6). Read-only and render-agnostic (works headless). EXPERIMENTAL — not yet in the frozen surface.
snapshot(opts?) JS ✓ Swift ✓ Kotlin ✓
Capture what the field is DOING at this frame — a portable, serializable FieldSnapshot (vs a screenshot of what it looked like). Returns { id, createdAt, frame, version, formations, bodies, relationships, metrics, particles? } — bodies carry id/rect/position/tokens/metrics/dimensions (+ data with includeData). opts: { includeParticles?, includeRelationships? (default true), includeData? (default false), includeInfluences? (per-body force attribution, for replay force steps), profile? }. profile (SnapshotProfile = debug | agent | bug-report | public) is a concrete inclusion preset composed with the explicit include* flags + the privacy policy, always resolving to the TIGHTEST (most private) result — a profile can never widen past what policy or an explicit deny allows (debug = everything; agent = ids + metrics + relationships + influences + projections, NO opaque body data; bug-report = structural + versions; public = ids + shape). Read-only; works headless; format is versioned (FIELD_VERSION). Pair with diff(). EXPERIMENTAL.
diff(a, b) JS ✓ Swift ✓ Kotlin ✓
Compare two snapshots and report what changed in the field, by lane: { from, to, bodyChanges (added/removed/changed metrics), relationshipChanges (strength/active deltas), metricChanges, formationChanges (activated/deactivated) }. Pure (operates on the two snapshots, ignores live state) — the standalone diffFieldSnapshots(a, b) is also exported. EXPERIMENTAL.
replay(a, b, opts?) JS ✓ Swift ✓ Kotlin ✓
Explain HOW the field changed between two snapshots — an ordered, narrated sequence of causes derived from the diff: { from, to, focus?, steps } where each step is { frame, time, cause (formation | relationship | metric | measurement | force), source?, target?, description, contribution? }. e.g. "Formation 'wells' activated", "Relationship A→B strengthened 0.10→0.40", "Body claim-3 density rose 0.20→0.60". opts.focus scopes it to one body id. Pure (derived from the two snapshots) — the standalone replayFieldSnapshots(a, b, opts) is also exported. When both snapshots were captured with includeInfluences, replay also emits cause=force steps (which force grew/weakened/engaged/released, by channel incl. thermal). The substrate explainability layer (Causal Replay). EXPERIMENTAL.
forAgent(opts) JS ✓ Swift ✓ Kotlin ✓
Derive a scoped, READ-ONLY AgentFieldView — the safe surface a Software Agent uses to read the field (agent-readable is NOT agent-writable). forAgent({ capabilities, redactions? }) returns a facade exposing ONLY a scoped query() (+ snapshot() only when read:snapshots is granted, + replay() only when read:replay is granted); it has NO mutation methods (no applyForce/addBody/setPolicy), enforced by the facade shape. AgentCapability = read:metrics | read:relationships | read:influences | read:snapshots | read:body-data | read:projections | read:diagnostics | read:replay | read:focus — an allow-list; a granted dimension is the only way a reading carries it (tightens, never widens: no read:influences → influences stripped; no read:body-data → body.data withheld even if a profile/includeData asked for it). Three members are SHAPE gates rather than redactions — snapshot (read:snapshots), replay (read:replay) and focusState (read:focus) are simply absent from the facade without their grant; withholding read:snapshots CLOSES the capture surface rather than emptying it, because an empty capture is indistinguishable from an empty field. read:diagnostics gates the raw particle pool inside a capture: without it includeParticles is forced off even under the debug profile (#1160). redactions?: string[] strips dotted paths (body.data, host.user, metrics.temperature) AFTER capability scoping. Respects FieldPolicy: budgets.agentRead === 0 closes the surface to the most-restricted view (the fractional gradient is a declared seam). EXPERIMENTAL.
projections JS ✓ Swift ✓ Kotlin ✓
The projection registry (substrate 05) — a property, not a method. A projection maps field STATE to an output surface (CSS / dom-attribute / annotation / agent-json / reduced-motion / sound / haptic / …), declaring its channels, surfaces, and reducedMotion/accessibility equivalents. register(p) → unregister fn; unregister(id); get(id); list() → serializable metadata; apply(id, reading, target) writes the reading to a surface; lint() runs governance accessibility checks over the registry (field/reduced-motion-equivalent-required = error, field/accessibility-equivalent-required = warning; the standalone lintProjections() is also exported). Governance principle: projection REVEALS state, it never changes it (no forces). query()/snapshot() report the registered projections. EXPERIMENTAL.

Focus and attention

The write side and the read side of the shared attention channel — experimental, shipped 0.9.4.

focus(target, input?) JS ✓ Swift — Kotlin —
EXPERIMENTAL focus/attention substrate. Deposit source-tagged, decaying focus onto a body by identity — the WRITE side of the shared attention channel. focus('file:src/auth.ts', { source: 'operator' }); operator/host attention is an input, an agent's is an output (the host relays field.focus(id, { source: 'agent' }) so the read-only agent view can never write). input: { amount?=1, source?='system', halfLife?≈8s (env.t seconds), at?=env.t }. Deposits accumulate decay-then-add per source (decay = temporal.freshness, the env.t clock — no Date.now); each fires the `focus` event and surfaces as metrics.salience + in focusState(). A string target always records (retained identity-keyed until a body with that id appears, then binds). On DOM custom elements reach it via el.handle (HTMLElement.focus stays DOM focus). Shipped-but-unfrozen.
focusState(opts?) JS ✓ Swift — Kotlin —
EXPERIMENTAL focus/attention substrate. Read the current-focus digest: the ranked, thresholded, capped SHARP TIP (a few hundred bytes), small enough to push into an agent turn. opts: { limit?=8, threshold?=0.05, source? }. Each FocusEntry is { target, identity, salience 0..1, sources (per-source provenance), updatedAt }. Net breadth — any body's focus magnitude — rides query()/snapshot() via metrics.salience under the base grant; this is the tip, and the per-source split (who is focused) is gated in an agent view by the read:focus capability. Shipped-but-unfrozen.

Events and custom overlays

The two extension points: subscribe to occurrences, or publish a named overlay the host can switch in.

on(type, cb) experimental JS ✓ Swift ✓ Kotlin ✓
Subscribe to a discrete field event — the host-agnostic push bus, for reacting to occurrences instead of polling feedback channels each frame. Returns an unsubscribe fn; plain data, no DOM. Discrete events are thresholded, debounced, and lazy. `captured`/`released` report sink accretion; `enter`/`exit`/`met` report proximity and contact; `focus` reports a focus() deposit (the write-back channel); additional `field:*` events report threshold crossings (attention, entropy, memory, saturation) where supported. `settle` remains reserved/planned. Shipped-but-unfrozen.
registerOverlay(name, drawFn) JS ✓ Swift ✓ Kotlin ✓
Register a named custom overlay function — extends setOverlay beyond the built-in reading stack. Called each frame when name is in the active overlay stack (via setOverlay). drawFn receives the active RenderBackend, current Env, and canvas W/H. Returns an unregister function. Lets third-party packages (e.g. @fundamental-engine/three) publish custom overlay modes the host switches in by name. Shipped-but-unfrozen.
On the element. With <field-root> every one of these is proxied onto the element itself — document.querySelector('field-root').setFormation('wells'). One exception: focus(target) does not shadow HTMLElement.focus(); reach the field's focus through el.handle.focus(id).

Reading the field

The field is a source of state, not only of pixels — and the read paths are pure, headless and serializable, so they work under render: 'none' and in a test. There are three of them, at three grains.

1 · Probes — the field at a point

Synchronous, allocation-free, no snapshot involved. sampleScalar and sampleGradient read the diffused heatmap grid, so they need heatmap: true and return zero without it — that is the usual cause of a forage-by-gradient agent that will not steer.

Probes
// the pure probes — these work headless and under render: 'none'
field.setHeatmap(true);                             // sampleScalar/sampleGradient need the heatmap grid

const force   = field.sample(x, y);                  // { x, y } — net force a still test particle feels
const density = field.sampleScalar(x, y);            // 0..1, bilinear, non-degenerate at a source
const uphill  = field.sampleGradient(x, y);          // { x, y } in 1/px — steepest ascent

// bind your own data to the particles, then identify one under the pointer
field.seed(records);
const atom = field.atomAt(e.clientX, e.clientY);     // nearest seeded particle within ~24px, or null

2 · query() — a structured question about a region

Plain, serializable data about bodies, metrics, relationships and per-force influences, scoped to a point, a rect, or the whole field. at is DOMRect-shaped, so el.getBoundingClientRect() drops straight in.

query
// ask the live field a structured question — plain, serializable data
const answer = field.query({
  at: el.getBoundingClientRect(),          // a point, a DOMRect-shaped rect, or omit for the whole field
  radius: 240,
  include: ['bodies', 'metrics', 'relationships', 'influences'],
});

answer.bodies[0].metrics.density;          // what THIS body is gathering
answer.influences;                         // which force contributed what, at that point

3 · snapshot()diff()replay() — change over time

A snapshot captures what the field is doing at a frame. diff reports what changed between two captures, by lane; replay turns that diff into an ordered, narrated chain of causes. Both are pure — they read the two capture objects and never touch live state, so you can replay captures from another session, or from another machine.

snapshot / diff / replay
// capture what the field is DOING this frame (not what it looked like)
const before = field.snapshot({ profile: 'agent' });
await userDoesSomething();
const after = field.snapshot({ profile: 'agent' });

// compare the two captures — pure, no live state involved
const changes = field.diff(before, after);

// and narrate the difference as an ordered chain of causes
for (const step of field.replay(before, after).steps) {
  console.log(step.cause, step.description);
  // "formation  Formation 'wells' activated"
  // "relationship  Relationship A→B strengthened 0.10→0.40"
}

Snapshot profiles

A profile is an inclusion preset. It composes with the explicit include* flags and the privacy budget and always resolves to the tightest result: a profile can never widen past what policy or an explicit deny allows.

debug JS ✓ Swift ✓ Kotlin ✓
Everything the call can legally include — body data still gated by the privacy budget. For a local inspector, not for export.
agent JS ✓ Swift ✓ Kotlin ✓
Ids, metrics, relationships, influences and projections; NO opaque body data. The shape meant to enter an agent turn.
bug-report JS ✓ Swift ✓ Kotlin ✓
Structural only — versions, formations, relationships, influences. No user data, so it can be attached to an issue.
public JS ✓ Swift ✓ Kotlin ✓
Minimal: ids and shape. No relationships, influences, metrics detail or data — safe to publish.

All four profiles ship on all three planes — this is one of the parts of the substrate that did port whole.

The agent view — forAgent

forAgent derives a read-only facade over the field. It is not a redacted copy of the handle: it has no mutators by shape — no addBody, no applyForce, no setPolicy — so "agent-readable" can never quietly become "agent-writable". Capabilities are an allow-list applied to every reading, then redactions strip dotted paths, and nothing can widen past the field's policy.

Three capabilities are shape gates rather than redactions: without read:snapshots the view has no snapshot, without read:replay no replay, and without read:focus no focusState. query is always present and is scoped from the inside instead — a denied agent is barred from the capture, never blinded.

forAgent
// a read-only, capability-scoped view — agent-readable is NOT agent-writable
const view = field.forAgent({
  capabilities: ['read:metrics', 'read:relationships'],
  redactions: ['body.data'],               // stripped AFTER capability scoping
});

view.query({ include: ['metrics'] });      // scoped + redacted
view.snapshot({ profile: 'agent' });       // present, scoped to the grant
view.replay;                               // undefined — read:replay was not granted (a SHAPE gate)
view.addBody;                              // undefined — the facade has NO mutators, by shape
read:metrics WIRED JS ✓ Swift ✓ Kotlin ✓
Per-body and field metrics (density, load, temperature, salience, …). Without it every reading's metrics lane is stripped.
read:relationships WIRED JS ✓ Swift ✓ Kotlin ✓
The relationship graph — the edges between bodies, by id, with their strength and memory. Without it the lane is emptied and snapshots are forced to includeRelationships: false.
read:influences WIRED JS ✓ Swift ✓ Kotlin ✓
Per-force attribution at a point: which force contributed what, by channel (linear Δv or thermal heat), from the impulse accumulator.
read:body-data WIRED JS ✓ Swift ✓ Kotlin ✓
A body's opaque data record. Withheld even when a profile or includeData asked for it unless this is granted AND the privacy budget allows it.
read:projections WIRED JS ✓ Swift ✓ Kotlin ✓
The registered projections (substrate 05) reported by query() and snapshot() — metadata only, never a write seam.
read:replay WIRED (shape gate) JS ✓ Swift ✓ Kotlin ✓
The replay() method itself. One of the two members the facade omits entirely rather than redacting: without this grant, view.replay is undefined.
read:focus WIRED (shape gate) · JS only JS ✓ Swift — Kotlin —
EXPERIMENTAL. The focusState() digest and the per-source provenance split (WHO is focused) — the other shape gate: without it, view.focusState is undefined. The aggregate per-body salience still rides query()/snapshot() under the base grant. The Swift and Kotlin AgentCapability enums carry the other eight; this one is not ported.
read:snapshots WIRED (shape gate) JS ✓ Swift ✓ Kotlin ✓
The snapshot() method itself — the portable capture. Without this grant view.snapshot is undefined: the surface is CLOSED, not emptied, because an empty capture is indistinguishable from an empty field. query() is unaffected, so a denied agent is barred from the capture, never blinded. Mirrored on Swift and Kotlin, where the member returns nil/null instead — a protocol or class member cannot vanish.
read:diagnostics WIRED JS ✓ Swift ✓ Kotlin ✓
The raw particle pool inside a capture (includeParticles) — engine internal state rather than a modelled reading of bodies, relationships or metrics. Without it the pool is withheld even when an explicit flag or the debug profile asked for it (tightens, never widens). The port snapshots carry no particle lane yet, so on Swift and Kotlin the gate is wired at the option boundary and becomes observable when that lane lands.

All nine are wired. read:snapshots and read:diagnostics were part of the AgentCapability contract on all three planes while forAgent consulted neither, so withholding them granted access anyway; #1160 wired both, and each plane now carries a test asserting the deny direction. The status on each row above is read off the implementation, not assumed.

The one gap. One capability is JS-only today — read:focus — because the focus/attention substrate has not been ported yet. The other 8 grants exist verbatim, as the same read:* tokens, on all three planes.

Policy and budgets

FieldPolicy is the permission lane: what this host, session, user or app permits, evaluated live. It is a distinct lane from governance (what doctrine allows, checked statically by the lints). Policy can only tighten: reduced-motion always wins, so a policy may lower the motion allowance but never raise it.

setPolicy
// what THIS host/session/user permits — distinct from what doctrine allows
field.setPolicy({
  allowBodyDataInSnapshots: false,
  budgets: { motion: 0.4, privacy: 0.2 },
});

field.policy;                    // a frozen copy of the live policy
field.setPolicy({});             // REPLACES, not merges — {} clears to the unbounded default

setPolicy replaces rather than merges — {} clears to the unbounded default — and field.policy reads back a frozen copy. All 8 budget lanes exist on all three planes; what differs is how many are wired rather than declared, which the rows below state per lane rather than implying.

budgets.motion WIRED JS ✓ Swift ✓ Kotlin ✓
Cap on how much motion the field may express; 0 behaves as reduced-motion (frozen). Folds via min with the host reduced-motion state and perf pressure into the effective motion the integrator and easing read — reduced-motion always wins, so a policy can lower motion but never raise it.
budgets.privacy WIRED JS ✓ Swift ✓ Kotlin ✓
Below PRIVACY_DATA_THRESHOLD (0.5) snapshots withhold body data — regardless of includeData, the profile, or a read:body-data grant. The tightest-wins rule in one number.
budgets.force DECLARED JS ✓ Swift ✓ Kotlin ✓
Cap on applied force magnitude — the share of the impulse budget matter may absorb.
budgets.attention DECLARED JS ✓ Swift ✓ Kotlin ✓
Cap on conserved-attention spend — the finite focus budget allocateAttention divides.
budgets.thermal DECLARED JS ✓ Swift ✓ Kotlin ✓
Cap on thermal/heat accumulation the field may carry.
budgets.render DECLARED JS ✓ Swift ✓ Kotlin ✓
Cap on render cost the field may spend (draw layers / fill). The policy-lane sibling of dprCap and qualityTier, which ARE wired.
budgets.accessibility DECLARED JS ✓ Swift ✓ Kotlin ✓
Accessibility floor — the minimum non-motion legibility the field must preserve.
budgets.agentRead DECLARED (gate wired at 0) JS ✓ Swift ✓ Kotlin ✓
Cap on how much field state agent readers may consume. The fractional gradient is a declared seam; the zero case IS enforced — budgets.agentRead === 0 closes forAgent to the most-restricted view.

Identity — naming a body so a reading can refer to it

Snapshots, diff, replay, the relationship graph and focus() all address bodies by id, not by handle — otherwise a capture could not survive being serialized and read back somewhere else. Identity is therefore first-class rather than derived from the DOM.

BodySpec.identity JS ✓ Swift ✓ Kotlin ✓
A stable id (plus optional namespace / kind / host) supplied when you add the body. Give it the id your own domain already uses — file:src/auth.ts, a row key, a ticket number — and every later reading names the thing you care about.
FieldOptions.identify JS ✓ Swift ✓ Kotlin ✓
The resolver for bodies you did not add by hand — a scanned [data-body] element, a native view. Return an identity for the element and the same id flows through every read path. Return nothing and the engine derives a deterministic body-N.

Deriving an id is the fallback, not the design: a derived body-3 is stable within a session and meaningless across one. Supply an identity for anything you intend to diff, replay, focus or relate.

The body handle

addBody(spec) is the non-DOM half of the body contract — the same physics, the same forces, no element required. The declarative reference documents the spec; what comes back is documented here.

addBody
const body = field.addBody({
  tokens: 'attract swirl',
  strength: 0.8,
  rect: () => mesh.getBoundingBox(),        // the position source, sampled each frame
  identity: { id: 'file:src/auth.ts' },     // so snapshots/diff/replay can name it
  data: { path: 'src/auth.ts' },
  onFeedback: (ch) => (mesh.scale = 1 + ch.density * 0.5),
});

body.set({ strength: 0.2 });                // live, on the measure cadence — no rescan
body.channels.density;                      // this body's latest readings, updated in place
body.remove();
data JS ✓ Swift ✓ Kotlin ✓
The carried record (BodySpec.data), opaque to the engine. Mutable: assign to it and the next query()/snapshot() that includes data sees the new value.
channels JS ✓ Swift — Kotlin —
This body's latest feedback channels, updated IN PLACE each frame — the same seven readings the DOM sink writes to CSS variables, handed to a non-DOM host directly. Read it; do not hold a copy.
set JS ✓ Swift ✓ Kotlin ✓
Mutate force params live — { strength?, range?, angle? (degrees), spin?, color? } — applied within a frame on the measure cadence with no rescan(). Only the keys you pass change. A STRUCTURAL change (different tokens) still needs remove + addBody: tokens are classified once, not reactive.
remove JS ✓ Swift ✓ Kotlin ✓
Remove the body from the field. The handle is inert afterwards.

The ports carry two members the web does not. Swift and Kotlin body handles expose load (a sink's fill ∈ [0,1]) and drain() (take the accretion and reset the body) directly; on the web the same reading arrives through channels.load and the captured / released events instead. Kotlin additionally exposes the body's resolved identity on the handle. Those rows say so above rather than hiding it.

Discrete events

on(type, cb) is the host-agnostic push bus — for reacting to occurrences instead of polling the feedback channels every frame. It returns an unsubscribe function (a Subscription on the ports); subscribing to a type with no active bodies costs nothing. Events are thresholded, debounced, and coalesced per source per frame, so a same-frame fill-and-release delivers once.

const off = field.on('captured', ({ body, count }) => {
  // a sink body reached the rising edge of accretion
  badge.textContent = String(count);
});

field.on('met', ({ a, b }) => link(a, b));           // two bodies touched
off();                                                // unsubscribe
captured { body, count } JS ✓ Swift ✓ Kotlin ✓
A sink body captured matter — the rising edge of accretion. count is the matter held at that edge. Coalesced per (body, type) per frame, so a same-frame fill+release delivers once.
released { body, count } JS ✓ Swift ✓ Kotlin ✓
A sink body released what it held — the falling edge, or a supernova. count is what was held at the rising edge (or the number of particles ejected).
enter { body, other } JS ✓ Swift — Kotlin —
Another body crossed INTO this body's range (#441) — the “entered radius” trigger. Directed: the pair is keyed (body, other), so two counterparties in one frame stay distinct.
exit { body, other } JS ✓ Swift — Kotlin —
Another body crossed OUT of this body's range (#441). Thresholded and debounced, so hover jitter does not spam the bus.
met { a, b } JS ✓ Swift — Kotlin —
Two bodies came into contact — their boxes touched or overlapped (#441), delivered once on the rising edge. Keyed on the unordered pair.
focus FocusEvent JS ✓ Swift — Kotlin —
EXPERIMENTAL (focus/attention substrate). A focus() deposit landed — the write-back channel and its append-only provenance receipt. The payload is flat (no Body reference) so it serializes and mirrors 1:1 to the ports. Keyed by (target, source), so an operator and an agent deposit on the same body in one frame both survive.

This bus diverges on purpose, and in both directions. The web bus carries enter, exit, focus, met — the proximity trio and the focus write-back — which the ports have not taken yet. The ports carry body-add, body-remove, tick: lifecycle events a native host needs because it drives the frame loop itself, and which the web engine handles internally instead of publishing. captured and released are the shared pair, aligned across all three planes in #1020.

Renamed. These events were once absorb and release. They are captured and released now, on every plane. If you have a subscriber on the old name it is not firing — on() takes a typed key and an unknown one is simply never delivered.

What each plane has that the others do not

The parity story is usually told as "the ports trail the web". On the handle that is only half true, and the lists below are computed from the port sources rather than asserted.

Web only 2 members
focus, focusState — the focus/attention substrate, whose ports are a batched follow-up.
Ports only 9 members
attach, effectiveMotion, feedReducedMotion, feedScrollV, pause, removeOverlay, resume, setWaves, tick — almost all of it is the seam a native host needs because it drives its own frame loop and owns its own input sources: loop control (pause / resume / tick / attach) and the feed methods (feedScrollV, feedReducedMotion, effectiveMotion). The remainder are idiom differences, not capability gaps: removeOverlay is the disposer registerOverlay already returns on the web, and setWaves is a live setter for what the web takes as the waves option.
Read-back Kotlin: 16 properties
Kotlin exposes a getter for nearly every setter (accent, renderMode, qualityTier, …) where the web and Swift handles are write-only. It does not expose version or guarantees, which the web does.

For the per-method table across Three.js as well, see the platform parity matrix; for the honest per-platform story, the Swift and Kotlin guides.