Skip to content
v0.17.0 npm

What shipped in each release

Every release since v0.1.0, in the order they landed, with what each one added. The landing page introduces the library; this is what has happened to it since.

  • v0.1.0 2026-08-05
    shipped

    The full foundation in one unattended build: attribute data model, PCG32 + fields + five noise types, graph executor with budgeted cooking, 23-node standard library, streaming World, three.js adapter, five demos, agent docs. 429 tests; every phase independently audited.

  • v0.2.0 2026-08-05
    shipped

    The entire post-v0.1 backlog: serializable subgraphs and dataInput, trig + orient-along-vector in the field grammar, normalized noise with published raw ranges, exact worley, string attributes for declarative multi-asset spawns, sanctioned per-cell seeding, per-output cooking, 3D cube cells, unbounded levels without dummy radii. 562 tests; old graphs cook byte-identically.

  • v0.3.0 2026-08-05
    shipped

    The stretch tier: uv and raycast attribute transfer (barycentric interpolation, robust ray-triangle intersection, deterministic tie rules, acceleration grids provably equivalent to brute force) and the interactive graph editor — a Svelte node editor built entirely on the public registry, validation, and serialization APIs, with live cooking and byte-identical JSON round-trips. 597 tests.

  • v0.4.0 2026-08-05
    shipped

    Editor-grade graph APIs: removeNode / disconnect / removeOutput with exact cache surgery (delete a branch, everything else serves from cache), frozen describe() and getParams introspection, registry categories across all 25 node types, and per-instance subgraph pin introspection — proven by the graph editor adopting them wholesale. 627 tests.

  • v0.5.0 2026-08-06
    shipped

    WebGPU field kernels: the serializable field grammar compiles to WGSL compute kernels and cooks on a real device through pcg-ts/gpu — bit-exact u32 hash/random streams, measured per-op float budgets, cache provenance across the CPU/GPU toggle, cook stats with machine-readable fallback reasons, and a million-point live demo. The CPU stays the bit-exact reference. 769 tests.

  • v0.6.0 2026-08-06
    shipped

    Pervasive GPU + device-resident pipelines: five more nodes resolve their field params on the GPU, chunked dispatch retires the element-count ceiling, and pooled buffers bound the allocation churn. On top of that, maximal linear chains of count-preserving field nodes now fuse into device-resident runs — attribute columns stay in storage buffers across member kernels and only the run terminal reads back, under a strict cache contract (terminal-only entries, splits wherever the graph observes bytes). New counters: residentRuns, fusedNodes, readbacksSaved. The CPU stays the bit-exact reference. 842 tests.

  • v0.6.1 2026-08-06
    shipped

    Constant node params ride the run uniform instead of a device column: a plain translate: [0, 0, 0] now costs a 16-byte slot and no dispatch, cutting the demo chain's working set 23% and its member kernels from 12 to 9. Values live in the uniform and never in the generated WGSL, so editing a constant hits the pipeline cache instead of recompiling. 853 tests.

  • v0.7.0 2026-08-07
    shipped

    Instance transforms that never reach the CPU. spawnInstances can now terminate a device-resident run: a WGSL kernel composes the 4×4 matrices and the batch hands back an opaque buffer handle instead of a Float32Array. A renderer sharing the same GPUDevice adopts that buffer as a storage instance attribute, so a streamed world goes from compute kernel to draw call with no readback and no per-cell upload. Ownership is explicit — pool-owned, detached, then disposed by the holder — and handles are refcounted by identity, so a parent output aliased into several cells is freed once, in whichever order they evict. Single-asset only for now; assetAttr falls back to the CPU path with a named reason. The CPU path stays the reference. 955 tests.

  • v0.8.0 2026-08-07
    shipped

    Multi-asset spawns become device-resident too — composed on the GPU and never read back, closing v0.7's one limitation. A spawnInstances driven by assetAttr no longer falls back — and it needs no GPU sort, which is the interesting part. A resident run always starts from a host geometry and no resident node can produce a string attribute, so the asset key is host-resident by construction: the host plans the grouping with the same function the CPU spawner calls, uploads a permutation, and the device composes once per asset. Ordering is therefore identical by construction rather than by comparison, and it is now a documented contract — batches by ascending first-occurrence point index, ascending point index within each. A cell yields one buffer per asset, each detached and refcounted by identity. Remaining boundary, stated plainly: a string setAttribute is not resident-eligible, so a graph computing its key that way fuses only the spawn. 1023 tests.

  • v0.9.0 2026-08-08
    shipped

    Fields built with the ergonomic API stop being second-class. There were two ways to build a Field and only one of them was supported: fieldFromJson attached a spec, while component(position(), 1) attached nothing — so a graph holding a combinator field could not be serialized at all. Now every constructor derives its spec from its arguments, and those graphs round-trip. Only three cases still refuse, and they say which: a makeField closure, anything composed over one, and a tree past the spec depth limit — derivation refuses at exactly the depth the parser enforces, so a spec that could not be read back is never written. Device eligibility is the separate, narrower question: derived-spec fields reach the GPU only under acceptDerivedSpecs, off by default, because the device path is a documented approximation of the CPU one and no existing graph should change bytes just by upgrading. 1560 tests.

  • v0.10.0 2026-08-09
    shipped

    An agent can author, validate and cook a graph without writing TypeScript. A pcg CLI — nodes, fields, validate, cook, inspect, render — where --json selects a rendering of the same result rather than a separate code path, exit codes are 0 / 1 (failure) / 2 (misuse), and render writes a deterministic top-down SVG that diffs in git. Subgraph nodes gain exposed params, and a primitive becomes a registered subgraph cited from JSON as ref: { name, hash? } instead of an embedded payload — a name-only ref upgrades freely, a pinned one hard-errors on mismatch, so no mode warns and no mode cooks a near-miss. 29 named primitives ship as pcg-ts/primitives; node types go 25 → 32; 23 single-concept example graphs and two skills ship inside the package, every example cooking from JSON alone because an example an agent cannot run teaches nothing. The serialization format is now closed — an unknown key is a hard error at every object position, so a typo can no longer cook as something else. One cut names the gap it leaves: place/along-curve was dropped because splineSample needs a polyline and no node produced one. 2217 tests.

  • v0.11.0 2026-08-09
    shipped

    The polyline gap, closed from the producing side. The library had a polyline consumer, a type, a render branch and an inspect branch — and no in-graph producer, so no path could exist in a serialized graph at all; the gap had been sighted five times from five directions before it was fixed. pointsToPath, pathResample and writeTangents take node types 32 → 35 and restore place/along-curve. Primitives 29 → 34 with the first curve family, field-grammar functions 40 → 42, corpus 23 → 34 — including six staged pipeline graphs whose superset and edit-locality properties are machine-checked rather than asserted in prose. Silent truncation of a multi-geometry collection becomes a diagnostic, and a class of silent attribute clobber is refused across six reporting-slot params: a param naming an attribute the node shapes now refuses a differently-shaped existing column instead of destroying it. Breaking against the unpublished v0.10.0: shape/ring's count now means exactly count. CI arrives, on Node 20 and 22 — and the first run it ever performed failed both legs with 79 type errors that cannot appear on a developer machine, because editor/** and demos/** import the library by package name, which resolves through the exports map into dist/, and a clean checkout has none. 2463 tests.

  • v0.12.0 2026-08-09
    shipped

    Scale-aware declutter. selfPrune gains a field-capable minDistance, so the radius is per point rather than per graph — big trees need more room than bushes — under a max(rA, rB) symmetry rule, so no kept point ever has another kept point inside its own radius. A priority attribute settles a contested spot: higher wins, ties fall to the lower index, so an authored plot beats a procedural one because of a value it carries rather than where it sits in a merge. Rewiring the corpus onto it exposed that the lesson it had been teaching never fired — the authored stage's 8-unit exclusion already exceeded the prune's 7-unit radius, so no authored and procedural pair had ever contested anything, and with priority deleted all 12 authored plots still survived. The exclusion is now 3, the contest is real, and a test requires that deleting priority costs authored plots. The topology phase this release replaced was re-surveyed and split in three: no existing source could derive a halo, because pointScatterInBounds computes positions as a function of its bounds, so widening for a halo moves every point and reproduces nothing. 2484 tests.

  • v0.13.0 2026-08-09
    shipped

    World-anchored sources, and the networks they unblocked. pointScatterInWorld reads its bounds only to pick lattice cells and clip, so a halo is just a wider query — that was the hole in the runtime pillar, and every cross-partition op depended on it. Per-point randomness now keys on point identity rather than array index, so an op behaves the same however the work was split. On top of that, connectPoints emits one 2-vertex polyline primitive per edge over the same points that arrived, and the survey's most useful finding was that this needs no new domain: a 2-vertex polyline over shared points already is an edge, so a junction is genuinely one point shared by every edge meeting there, and no edge payload ever crosses a pin. filterPrimitivesByBounds is the one filter here that preserves topology, which is what makes a partitioned network cook expressible in a serialized graph rather than only in host TypeScript. Cut on measurement rather than taste: findPath and MST, because both are global — an MST edge belongs iff no lighter path connects its ends, so a chain of N plus a closing edge defeats any finite halo. The replacement is a relative-neighbourhood mode, halo-exact at the same bound, which contains the MST but leaves cycles — a network rather than a tree, which is what a road layout wants. Node types 35 → 38; the corpus gains a fifth pipeline stage: 10 segments over 9 district centres, degrees {1:1, 2:5, 3:3}, one component, two cycles. 2648 tests.

  • v0.14.0 2026-08-10
    shipped

    A gap v0.13.0 opened, closed. Per-edge values became a headline capability — a road carries a width promoted from its endpoints and a kind from the first — and every node that sampled a primitive down onto points dropped them on the floor, so the lamps placed along a road could not see the width of the road they stood on. splineSample, pathResample, surfaceSample and place/along-curve now carry the source primitive's attributes onto the samples through one shared helper; pathResample also keeps its own output primitives' attributes, which setPolylineTopology had been deleting, so a resampled road stays a road. The carry is automatic rather than opt-in, because the demand is that an author who set roadWidth gets it back without knowing a knob exists — and the cost is stated here rather than rediscovered as a bug later: every upstream primitive attribute becomes part of a sampler's output contract, so an unrelated lengthAttr widens the samples. A collision with a name the node itself owns is refused, naming node, attribute and fix. transferAttribute reads a primitive source in uv and raycast modes; nearest refuses and names the route instead. pcg render --attr reaches the primitive domain, colouring paths from primitive values. 46 of 46 planned phases; 2675 tests.

  • v0.15.0 2026-08-11
    shipped

    Cooks leave the main thread. A profile of a streamed world found the freeze nobody's budget could slice: budgetMs yields between nodes, and one noise field over a cell's points is a ~100 ms atom — so pcg-ts/worker ships a CookWorkerPool that cooks serialized graphs off-thread. The design was already the right shape: a graph crosses once per worker as JSON, each cook sends parameter patches, and outputs return with every typed array on the transfer list — the boundary costs under a millisecond against the cook it relocates. Byte-identity is tested rather than assumed: string tables cross in interning order so u32 index columns survive exactly, errors rehydrate into real classes with the node name and the fix intact, and a pooled World matches a no-pool World byte-for-byte along a streaming path with evictions. LevelDef gains bindPatches, the serializable sibling of bind(), applied by one shared implementation on both paths so they cannot drift. Two defects fixed with names: three r0.185 keys renderer state per instanced mesh and frees it only on a material's dispose event, so sharing one material across meshes leaks pipelines unboundedly — per-mesh clones disposed on every release path end it; and esbuild builds cross-chunk edges from used symbols, so a bare side-effect import of the node registry rode on chunk-ordering luck that two new entry points ran out of — an evaluation-order witness makes the edge real, and a twelve-case fresh-process smoke gate on the built package now runs before any publish. 2841 tests.

  • v0.16.0 2026-08-17
    shipped

    Fields you can read, and a rule for where they go. Which params accept a field had never been written down — it was a whitelist of 20 out of 180 that nobody could state, which is how it drifted. Writing the rule first and sweeping it second took the count to 44 across 25 node types, in a day against an estimate of a month, because a stated rule turns a judgement call per param into a lookup. The sweep then corrected the rule twice: one clause read “nothing that decides how many elements come out” and pathResample.spacing decides exactly that and qualifies anyway, because that node resamples each polyline on its own arc length, so the field resolves per primitive; another called a symmetric relation impossible to field when selfPrune was already doing it on the larger of two radii, which connectPoints now does too. Three of the five clauses are pinned against the live registry, each proven able to fail. A plain value stays byte-identical and the whole corpus golden is unchanged.

    A field expression is now readable as textprintFieldSpec and parseFieldText are a bidirectional view over the same spec tree, so a 38-line predicate is one line and the tree is still the format every programmatic edit touches. Text is never saved: it parses back, which is what makes the view safe when a knob turn, a World patch or a param patch would re-print it anyway. The correspondence is proven rather than argued — parse(print(s)) equals s for every expression in the corpus, and printing is idempotent. The interactive tool is called the editor now, shows an expression as text or as a read-only boxes-and-wires diagram, and shows no JSON at all; its hosted page moved from /pages/sandbox/ to /pages/editor/, the one breaking change here.

    Also: a saved noise answers the seed box through opts.seed: { from: "node", variant: N }, retiring a hand-written offset idiom that had spread to 39 places; cross, pow, sqrt and step join the grammar, three of their four designs chosen by a device probe rather than by reading the spec; graph-scoped params with targets drive the half of the format an expression cannot reach; and pcg assets reports what a graph requires without cooking it, saying plainly when the answer is a lower bound. 4323 tests.

  • v0.17.0 2026-08-28
    shipped

    A named per-instance channel, which is the whole ABI between a graph and its host. The argument came from an integrator and is worth restating: the field grammar has no time input, so a graph settles structure and the host animates it — and until now nothing but transforms and RGB could cross the spawner. InstanceBatch.attributes carries named columns with the dtype preserved rather than widened to f32, because CPU field evaluation rounds through Math.fround: an id past 224 cannot even be computed in a field, so it has to arrive in an integer column and an f32-only channel could never hold one. colors is now sugar for the reserved "color" entry rather than a sibling — one buffer, two spellings, one reader — and itemSize is derived rather than carried, so a mispacked producer fails a length check instead of quietly drawing a skew.

    Two silent losses turned up on the exact shape a host writes by hand. A batch setting attributes and colors dropped its colour, because the lift keyed on the record's presence rather than the channel's; on the device side that handle was then never retained, never drawn and never freed. And the WebGPU adapter validated colour from the record while adopting from batch.colors, so the documented attributes-only literal lost its colour. Geometry ownership became contractual with them: a batch carrying a non-reserved channel gets its own clone, disposed with the mesh and marked by ownsGeometry — and colour alone does not clone, because instanceColor is a mesh property, so the literal rule would have regressed every coloured spawn and defeated an integrator's instance pool.

    Then a draw call proved it, and falsified our own documentation. Every prior test asserted the attribute landed on the geometry; none rendered. The new suite reads pixels back byte-exact and shows that a u32 channel with gpuType at three's default and with gpuType = IntType are identicalWebGLBindingStates tests the array type first and short-circuits. The 224 collision is real but belongs to an f32 column, which is what demos/lanterns draws: a u32 seed drives hue, phase and rate through a shader, and a toggle switches to an f32 copy of the same id to show 5,894 of 6,000 ids altered and 256 distinct hues collapsing to 72.

    Also: pcg-ts/panels publishes the graph panel format with a validator that actually validates, which is the answer to a request for step/label/group on ParamSchema — declined, because grouping is per-graph presentation and not a node-type fact. toDeviceInstanceObjects gives device instancing a World-free path, symmetric with toInstancedMeshes. A node type now declares selfMetered, because onNodeDone.elapsedMs is a block for a leaf and wall time for the four composites that meter the budget themselves — a distinction that had a consumer over-reporting a 4.7 ms block as 450 ms and inverting its verdict. sideEffects ships as an array, never false, since all registration lives in content-hashed chunks and a blanket false would empty the registry; ./dist/three/index.js is deliberately absent from it, so the entry a three host pulls stays shakeable. The three peer range widens to ^0.185.0, which was a real exclusion rather than a nit.

    Breaking, and stated rather than discovered: nine values leave the root surface — ATTR_CTORS, FIELD_BRAND, MAX_INSTANCES, nextRev, makeDeviceInstancesItem, isDeviceInstanceBatch, getSubgraphPlumbing, paramSchemaError, paramValueError — along with the types AnyInstanceBatch and SubgraphPlumbing, and the worker protocol's EncodedInstanceBatch.colors becomes attributes. The limitation this release carries: device production of non-colour channels is not built — the compose kernel's widest form already binds seven storage buffers against a baseline of eight — so the planner rejects and the adapter refuses, each with a machine-readable reason. Rendering such a channel works, and cooking it off the main thread through pcg-ts/worker arrives zero-copy on the transfer list. 6,122 tests.

  • next unscheduled
    exploring

    All 46 planned phases are done, and the entry that stood here — a resident descriptor for filterByAttribute — was surveyed and declined. It is feasible, and cheaper than the entry assumed, but it does not compose with the zero-round-trip path: at a spawner terminal the surviving count sizes a retained buffer and there is no readback for it to ride, so a run could never hold both a count-changing member and an instances terminal. And the saving it was to buy turned out to be free — moving setAttribute("scale") ahead of the two filters in the forest recipe (now graphs/examples-forest.json) bought the same readback for no library work. What is left is small, and recorded in PLAN.md with the reasoning: fieldToJson still refuses with a message enumerating all three causes instead of naming the one that applies, and a maxDegree bounded within a radius is halo-exact but unbuilt.

Next

Where to go from here

The user manual documents what all of this adds up to. Architecture is how it is put together. The corpus gallery is every graph in the repository, cooked and shot.

← back to the landing page