API reference

API stability

The architecture has stopped being fluid. The surface below is frozen for 0.x: each symbol stays exported, from the same package, with the same kind, and does not break for the life of the 0.x line. The freeze is enforced in CI by pnpm check:api — 14 entries that fail the build if they change.

Package names. Core publishes as @fundamental-engine/core; the rest are @fundamental-engine/platform, @fundamental-engine/elements, @fundamental-engine/react, @fundamental-engine/vanilla.

Stable — entry points & runtime

SymbolPackageWhat it is
createField value frozen · @fundamental-engine/core @fundamental-engine/core host-required primitive — throws without opts.host (the renderer-agnostic door).
compileRecipe value frozen · @fundamental-engine/core @fundamental-engine/core pure FieldRecipe → compiled plan (no DOM).
browserHost value frozen · @fundamental-engine/platform @fundamental-engine/platform the canonical DOM FieldHost for core.createField.
createFieldPlatform value frozen · @fundamental-engine/platform @fundamental-engine/platform wires the six native-first registries on a root.
applyRecipe value frozen · @fundamental-engine/platform @fundamental-engine/platform applies a recipe to a live platform (compileRecipe lives in core).
bindData value frozen · @fundamental-engine/platform @fundamental-engine/platform binds records → bodies; data drives the field.
createField value frozen · @fundamental-engine/vanilla @fundamental-engine/vanilla host-bundled convenience = createBrowserField (auto-supplies browserHost).
browserHost value frozen · @fundamental-engine/vanilla @fundamental-engine/vanilla re-export of the platform host for the no-framework path.

createField has two doors on purpose: the core primitive is renderer-agnostic and host-required; @fundamental-engine/vanilla re-exports the host-bundled convenience so the no-framework path stays one call. Both are frozen.

Stable — types

TypePackageWhat it is
FieldRecipe type frozen · @fundamental-engine/core@fundamental-engine/corethe recipe schema (recipes/schema.ts).
FieldHost type frozen · @fundamental-engine/core@fundamental-engine/corethe renderer-agnostic host contract createField requires; browserHost implements it.
FieldPlatform type frozen · @fundamental-engine/platform@fundamental-engine/platformthe surface createFieldPlatform returns.

Stable — elements & the body contract

SurfacePackageWhat it is
<field-root> element frozen · @fundamental-engine/elements@fundamental-engine/elementsone background field per page; scans the document for [data-body].
<field-cell> element frozen · @fundamental-engine/elements@fundamental-engine/elementsa scoped local field region.
data-body attribute frozen · attribute contract core BODY_SELECTOR The body contract. "Every element is a body" via the data-body attribute on ordinary elements.

There is no <field-body> element. Bodies are an attribute on ordinary elements, not a tag. <forces-field> / <forces-cell> are deprecated aliases (see the alias window in rule 6).

Experimental — not frozen

These carry no stability guarantee and may change shape or be removed in any release. Some have exported building blocks today — those are shipped-but-unfrozen: present in the package, but not part of the contract until promoted above.

AreaStatusNotes
FieldHandle — full shapepartialThe entry points that return FieldHandle are frozen; the handle shape itself is not. New methods may be added in any patch.
FieldHandle.particleCount() / .energy()partialShipped in @fundamental-engine/core and proxied on <field-root>. Safe to use; signatures may refine before 1.0 as FieldPerf is designed.
FieldHandle.readParticles(out)partialShipped in @fundamental-engine/core. Copies live particle state into a caller-owned Float32Array (stride 5: x, y, z, heat, size, in CSS-pixel field coords; z is the optional depth lane, 0 in a flat field) and returns the count written = min(particleCount(), floor(out.length/5)). Zero-alloc, read-only; the render-agnostic swarm read-out @fundamental-engine/three's particle bridge consumes. Additive to the (unfrozen) handle shape; stride may widen further before 1.0.
FieldHandle.readParticleIds(out)partialShipped in @fundamental-engine/core. Copies each live particle's STABLE id into a caller-owned Uint32Array, parallel to readParticles (same pool order, same agent exclusion) so ids[i] belongs to the particle at stride offset i*5 there. Identity is what pooled particles otherwise lack: a host that seeds entities (wind-borne seeds, tagged motes) reads ids back each frame to track which is which and key its own opaque payload off them (engine carries identity, not payload). Particle.id added (optional, engine always sets it). Zero-alloc, read-only. Mirrored on vanilla / elements / three. Additive to the (unfrozen) handle.
FieldHandle.readParticleColors(out)partialShipped in @fundamental-engine/core. Copies each live particle tint into a Uint8Array as packed [r,g,b] (0-255), parallel to readParticles (same order, same agent skip) — the carried pigment color (pigment force / Particle.color, conserved color transport) an external swarm renderer blends with heat; white when uncolored. Hex parsed once per distinct color (memoized), zero-alloc on the hot path. Companion to readParticles — the stride-5 geometry read-out is unchanged (#424). Mirrored on vanilla / elements / three. Additive. (three ParticlePool aColor shader + /pollinate demo is the visual follow-up.)
cssFeedbackSinkpartialShipped in @fundamental-engine/core. The public name for the CSS-variable feedback adapter — the DOM write path (--d/--field-density/--load/--lit + field:lit/dim events) the default sink uses. Feedback is plain data first (FeedbackChannels); this is one adapter the DOM door (createField/vanilla/<field-root>) installs by default, and a non-DOM host (three FieldLayer) opts out by passing its own feedbackSink. Additive export; behavior identical to the historical default.
FieldHandle.sample(x, y)partialShipped in @fundamental-engine/core. Returns the net field force a still test particle would feel at (x, y) as { x, y } in field-pixel space — a thin wrapper over forceAt(bodies, forces, env). Pure, read-only, samplable at any resolution; the seam external visualizers (@fundamental-engine/three vectorField / streamlineTubes, mesh displacement) consume. Additive to the (unfrozen) handle shape; may gain a structure-only companion before 1.0.
FieldHandle.sampleScalar(x, y)partialShipped in @fundamental-engine/core. Returns 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). Requires the heatmap layer (createField({ heatmap: true }) / setHeatmap(true)); returns 0 when off. Read-only, updated each frame incl. under render:none. Additive to the (unfrozen) handle shape.
FieldHandle.sampleGradient(x, y)partialShipped in @fundamental-engine/core. The analytic companion to sampleScalar: returns the gradient ∇ {x,y} (direction + steepness in 1/px) of the diffused density field at (x, y), pointing up-density. Computed from the same heatmap grid (central difference, normalized by the eased peak), so it stays non-degenerate at a source where a nearest-body density flattens to zero — the smooth cue reliable forage-/flee-by-gradient steers by. Requires the heatmap layer (createField({ heatmap: true }) / setHeatmap(true)); returns { x: 0, y: 0 } when off or empty. Pure, read-only, maintained under render:none. Mirrored on vanilla / elements / three. Additive to the (unfrozen) handle shape.
FieldHandle.grid(name)partialShipped in @fundamental-engine/core. Opens 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(x,y), deposit(x,y,amount), gradient(x,y), decay(rate), clear() } in field px. Created on first access (allocating nothing until then), kept viewport-sized, advanced once per frame by its mode inferred from the name (wave… = wave scheme, memory… = slow decay, else diffuse). A same-named force shares the buffer; pick a distinct name to keep an authored field independent. ScalarGrid gained decay()/clear() (additive). Mirrored on vanilla / elements / three. Additive to the (unfrozen) handle shape.
FieldHandle.on(type, cb)partialShipped in @fundamental-engine/core. A host-agnostic discrete EVENT BUS: subscribe to occurrences (push, plain data, no DOM) instead of polling the continuous feedback channels — for non-DOM hosts (3D/native/headless) and clean gameplay triggers. Returns an unsubscribe fn. Events: absorb / release — a sink body captured / let go of matter (rising / falling edge of accretion), { body, count }. Detection is lazy (a type with no listener costs nothing). contact / settle / per-particle enter·exit are the next slice (#441). Distinct from the data-on CustomEvent bindings (DOM-only). Mirrored on vanilla / elements / three. Additive to the (unfrozen) handle shape.
FieldHandle.addAgent(spec)partialShipped in @fundamental-engine/core. Adds an engine-stepped agent — a participant the integrator MOVES (vs sample(), where you integrate yourself). It lives in the conserved particle pool, so it feels every force the swarm feels (body forces and the particle-level hunt/align/cohesion); each step its report(p) fires so an external transform (a THREE.Object3D, a label) follows it. spec: { x, y, z?, mass?, maxSpeed?, species?, report }. maxSpeed is a hard clamp; species lets tagged bodies (data-affects) steer it selectively; it edge-bounces rather than wrapping toroidally, and is counted by particleCount() but excluded from readParticles(). Returns { particle, remove() }. The creatures primitive @fundamental-engine/three's layer.addAgent binds a mesh over. Additive to the (unfrozen) handle shape.
FieldHandle.addBody(spec)partialShipped in @fundamental-engine/core. Adds a programmatic body (no DOM) from a spec — the sanctioned alternative to the [data-body] scan for a non-DOM host (a Three.js mesh, a native view): no fake document, no querySelectorAll duck-typing. spec: { tokens, strength?, range?, spin?, angle?, color?, rect:()=>{left,top,width,height}, data?, onFeedback? }. rect() is sampled each frame for the box in field px. The body CARRIES a data record (the Body-level analog of a particle atom — Field Agent Consumption Model) and takes per-body feedback (its channels demuxed from the global sink); it survives rescan. Returns BodyHandle { data, channels, remove() }. Mirrored on vanilla / elements / three (overloaded with the mesh form). Additive to the (unfrozen) handle shape; three FieldBodyRegistry collapse onto it is a follow-up.
FieldHandle.scrollV()partialShipped in @fundamental-engine/core; returns the engine's EMA scroll velocity in px/frame (refresh-rate dependent — reads roughly half on a 120 Hz display). Mirrored as --field-scroll-v on :root by the platform runtime. Signature stable; semantics (the unit may normalize to px/ms, EMA factor) may refine before 1.0.
FieldOptions.dprCap / FieldHandle.setDprCap(cap)partialShipped in @fundamental-engine/core. Backing-store device-pixel-ratio ceiling (#410): the effective DPR is min(devicePixelRatio, dprCap), default 2. The dominant fill-rate lever — the ambient field is soft, so capping at ~1.5 buys ~1.8x headroom on retina for a small softening. setDprCap(cap) re-sizes immediately. Mirrored: FieldOptions.dprCap (createField / vanilla / react), <field-root dpr-cap> attribute (live), the setter on all planes. Additive to the (unfrozen) handle + options.
FieldHandle.setSurfaces(plan) / getSurfaces()partialShipped in @fundamental-engine/core. One declarative verb for the whole surface state: setSurfaces({ underlay, overlay, heatmap }) — matter behind content (render mode), readings in front (overlay stack), the density accumulation layer — naming the three surfaces as one concept. Full-state: an omitted key resets to its default (dots / off / false), exactly like a recipe, so it is idempotent, snapshot-able, and restorable; getSurfaces() returns the current Required<SurfacePlan> and setSurfaces(getSurfaces()) is a no-op. The single-surface verbs (setRender/setOverlay/setHeatmap) remain for surgical pokes. Mirrored on vanilla / elements / three. Additive to the (unfrozen) handle shape. (#385)
render mode 'none' (signals-only engine)partialShipped in @fundamental-engine/core (#297): createField({ render: 'none' }) runs the full simulation + feedback pipeline but never acquires a canvas context, never sizes the backing store (it stays 0×0), and never draws — the field exists purely as signals (--d, --load, --lit, capture events, scrollV). setRender FROM 'none' acquires the context lazily; setRender TO 'none' at runtime stops drawing but keeps an already-acquired context. Accepted by <field-root render="none">. Shipped-but-unfrozen; may refine before being added to the frozen list.
QualityGovernor / field:quality-tierpartialTier detection (0-3) ships in @fundamental-engine/platform; the <field-root> runtime throttles its own tick cadence at tiers 2-3 and emits field:quality-tier. Engine-side degradation (render simplification, particle caps) is the embedder's to wire; unfrozen.
performance budget (inspectBudget / DEFAULT_BUDGET / createFieldPerf)partialinspectBudget(), withinBudget(), BudgetFinding, and DEFAULT_BUDGET ship in @fundamental-engine/core. QualityGovernor covers adaptive tier detection. The FieldPerf frame-duration split now ships in @fundamental-engine/platform as createFieldPerf() — pure timing math lifted from the site DataConsole prototype (rolling delta window of 180, nearest-rank-floor percentiles, budget = median of the first 30 clean deltas, dropped = delta > budget×1.5, gaps > 500 ms skipped as discontinuities; callers feed rAF timestamps — no rAF of its own). The LoAF / long-task split stays page-side (no PerformanceObserver in this slice). Unfrozen — option/snapshot shapes may refine before 1.0.
advanced diagnosticspartialDIAGNOSTICS / DIAGNOSTIC_LENS / draw* primitives ship today but are shipped-but-unfrozen until added here.
visual recipe editorabsentno editor UI; the authoring toolkit (compileRecipe/recipeAuthoring/validateRecipe) is the substrate to build one on.
GPU / WebGPU backendplanneda named direction (VisualBindingRegistry mentions WebGL); the six shipped drawing render modes are CPU/canvas (the seventh, 'none', draws nothing).
multi-root bridgeabsentno API for coordinating multiple <field-root> instances yet.
AI evidence fieldspartialEVIDENCE_FIELD + the agent API ship as a substrate, but no packaged feature; unfrozen.
custom render backendspartiala custom backend is possible via opts.host, but there is no stable backend-registration API.
withFlip()partialShipped in @fundamental-engine/platform; a pure DOM FLIP reflow helper (measure → mutate → invert → release) extracted from the invisible-fields example runtimes (#295). No registry dependencies; honors prefers-reduced-motion (the mutation still runs). Unfrozen — the options shape may refine before 1.0.
allocateAttention()partialShipped in @fundamental-engine/core; a pure conserved-attention allocator (water-filling: Σw pinned to one finite budget, per-item cap defaulting to 1, capped excess re-flows, pinned items take exactly cap off the top) extracted from the Inbox example runtime (#296). Pure and deterministic — no DOM. Unfrozen — the item/options shapes may refine before 1.0.
textBodies()partialShipped in @fundamental-engine/platform — the Range-geometry slice of #257: samples a text element's rendered line/word boxes (document.createRange + getClientRects) into aria-hidden wall/shear boundary spans bound back to the source via data-field-visual-for (role representation), so the field flows around/along the words. Box geometry, not glyph contours — glyph-outline sampling is the planned next slice; callers re-annotate on resize. Unfrozen — the options/handle shapes may refine before 1.0.
threadOverlay()partialShipped in @fundamental-engine/platform; the hover-thread SVG overlay extracted from three hand-rolled copies in the example family (Evidence wireThreads/centerIn, Backlog, Dependencies). Geometry + classes only — one absolutely-positioned aria-hidden pointer-events:none SVG prepended into a host (viewBox from the host rect), one host-relative center-to-center cubic bezier per target (midpoint-y shape), --thread set from draw()'s color, .lit/.cited marks on the endpoints. NO event wiring (pages own hover semantics) and no layout observation (callers re-draw on layout changes). Unfrozen — shapes may refine before 1.0.
applyRecipe renderless / extraMetrics optionspartialShipped in @fundamental-engine/platform; the scoped invisible-field idiom the twelve example runtimes hand-spread (render: [] + metrics dedupe-append) lifted into ApplyRecipeOptions. Both derive an EFFECTIVE recipe copy inside applyRecipe — the caller's (possibly shared catalog) recipe object is never mutated; the returned handle's recipe/compiled reflect the effective one. Additive — existing call shapes unchanged. Unfrozen — option names may refine before 1.0.
bindFieldNav() + classifyMetric() / lintInertFeedbackpartialShipped in @fundamental-engine/platform; the navigation-chrome idiom the site hand-spread across ~12 surfaces (top nav, chapter rail, docs sidebar/outline/search, breadcrumbs, pagers, footer, filter rosters) lifted into bindFieldNav(root, recipe, { pin, visited, extraMetrics, reducedMotion }): runs a recipe signals-only (render: []) over the <a href> links, pins the current as the well (data-field-attention=1), marks caller-flagged visited links (data-field-memory=1 + a nav-visited class), and returns a teardown; reduced-motion → null (plain links). Paired guard: classifyMetric(name) splits a lane into computed / supplied-only / designed (COMPUTED_METRICS + SUPPLIED_ONLY_METRICS partition METRIC_KINDS), and the new lintInertFeedback rule (in lintPlatform) flags a feedback binding to a DESIGNED --field-<m> lane the host never supplies — declared but never written, the same silent-contract class as lintSinkFeedback. Unfrozen — option names + the designed/computed split may refine before 1.0.
`screen` modifier (quiet zones)partialShipped in @fundamental-engine/core (physics workover v0.3): a body with `screen` in data-body damps OTHER bodies' forces on matter inside its data-range — clamp(1 − S·(1 − d/r)², data-screen-min, 1), applied in the integrator force pass (smooth edge, no NaN at zero range, never global). Passported (truth mode: designed, class modifier) with a conformance scenario. The data-body attribute contract itself is frozen; this TOKEN and its data-screen-* attrs are unfrozen — data-screen-mode (inside/outside/behind) is planned and the attenuation curve may refine before 1.0.
measured thermodynamics (--entropy / --coherence / --temperature)partialShipped in @fundamental-engine/core (physics workover v0.3): per-body LOCAL measurements on data-feedback bodies — entropy = (1 − R)·min(1, s̄/1.5) with R = |Σv|/Σ|v| (velocity alignment), coherence = 1 − entropy, temperature = ½·meanHeat + ½·min(1, s̄²/9) — accumulated in the existing density pass (core/thermo.ts is the pure math), eased like --d, and written through both feedback sinks as the bare names --entropy/--coherence/--temperature. Distinct from the platform's inferred --field-entropy/--field-coherence lanes and from the --coherence palette COLOR cssTokens() sets on :root. Unfrozen — the formulas' reference constants and the FeedbackChannels fields may refine before 1.0.
source budget (data-life / data-cap + the unbudgeted-source guard)partialShipped in @fundamental-engine/core (physics workover v0.3): a class-[S] source body (spawn) must declare one of data-life / data-cap / data-budget / data-sink; otherwise the scanner warns in dev (naming the element) and applies the safe defaults data-life="300" / data-cap="120". data-life sets each emission's mortal age; data-cap clamps the emission rate to cap/life so the body's live population is bounded at ~cap. Conformance pins the bound. Unfrozen — data-budget/data-sink carry presence-only semantics today and may gain richer ones.
weight primitives (logNormalize/weightToStrength)partialShipped in @fundamental-engine/core (core/weights.ts): the page-weight → body-strength contract extracted from ~38 hand-rolled call sites in the example family. logNormalize(value, max) is the family's log "consensus" shape — ln(value+1)/ln(max+1), clamped 0..1; heavy tails compress, zero stays zero, value === max reads exactly 1 (bit-for-bit the pages' Math.log(x+1)/Math.log(max+1) for max > 0). logNormalizeAll(values) runs the set in one pass and returns the max for live re-normalization. weightToStrength(w) maps the 0..1 weight onto the attract-body data-strength range: 0.4 + w·1.6 → 0.4..2.0, with WEIGHT_STRENGTH_BASE/WEIGHT_STRENGTH_SPAN exported so the magic numbers have ONE definition (returns the number; callers .toFixed(2) at the attribute write). Pure and deterministic — no DOM, NaN-safe on degenerate inputs. Unfrozen — names and shapes may refine before 1.0.
temporal kernels (imminence/freshness/retention/phase) + data-field-atpartialShipped in @fundamental-engine/core (core/temporal.ts): the world-time clock — pure, deterministic kernels extracted from the example family. imminence(at, now, horizon) log-ramps to 1 at T−0 (the calendar page); freshness(at, now, halfLife) is exponential newness, exactly 0.5 one half-life out (staleness = 1 − freshness; the backlog/inbox recency shape); retention(anchor, since, opts) is the Ebbinghaus curve with τ growing with anchor strength (the memory page); phase(now, period, offset) is cyclical 0..1 (consumer-less today, shipped for completeness). Riding on them in @fundamental-engine/platform: a declared data-field-at (ISO 8601 or epoch ms; half-life via data-field-halflife, default 7 days) GROUNDS the metric pipeline's recency lane in world time — without it, recency stays interaction-inferred. World time is the third clock, alongside simulation time (env.t/dt) and experiential time (the metric pipeline); see docs/canonical/time.md. Unfrozen — names, option shapes, and the attribute contract may refine before 1.0.

Compatibility rules

  1. Pre-1.0 semver: in 0.x the MINOR is the breaking position. A breaking change to any frozen symbol bumps 0.MINOR (e.g. 0.2 → 0.3); additive and fix-only changes bump PATCH. Consumers should pin to ~0.MINOR.
  2. The stable surface is additive-only within a 0.MINOR line: new exports, new optional fields, and new recipes/modes may land in a PATCH; renaming, removing, or changing the signature/shape of a frozen symbol requires a MINOR bump and a migration note.
  3. createField is frozen in BOTH Fundamental (host-required primitive; throws without opts.host) and @fundamental-engine/vanilla (host-bundled convenience). Both contracts are preserved; the vanilla door must keep auto-supplying browserHost.
  4. Package ownership is part of the contract and must not drift within 0.x: compileRecipe / FieldRecipe / FieldHost are core (Fundamental); createFieldPlatform / applyRecipe / bindData / FieldPlatform / browserHost are @fundamental-engine/platform; field-root / field-cell are @fundamental-engine/elements.
  5. Bodies are a stable ATTRIBUTE contract: [data-body] on ordinary elements is the frozen authoring surface. There is no <field-body> tag and none will be introduced as the body mechanism.
  6. The experimental surface carries no semver guarantee. Diagnostics/agent/render-mode exports that happen to ship today are shipped-but-unfrozen — treat them as experimental until explicitly added to the frozen list.
Enforced. scripts/api-surface.ts (typechecked) locks every value and type; scripts/check-api-surface.mjs locks the element tags and the data-body contract; both run via pnpm check:api in CI. The full contract is in docs/canonical/api-stability.md; publish order is in PUBLISHING.md.