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-05shipped
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-05shipped
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-05shipped
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-05shipped
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-06shipped
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-06shipped
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-06shipped
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-07shipped
Instance transforms that never reach the CPU.
spawnInstancescan 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 aFloat32Array. A renderer sharing the sameGPUDeviceadopts 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;assetAttrfalls back to the CPU path with a named reason. The CPU path stays the reference. 955 tests. -
v0.8.0 2026-08-07shipped
Multi-asset spawns become device-resident too — composed on the GPU and never read back, closing v0.7's one limitation. A
spawnInstancesdriven byassetAttrno 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 stringsetAttributeis not resident-eligible, so a graph computing its key that way fuses only the spawn. 1023 tests. -
v0.9.0 2026-08-08shipped
Fields built with the ergonomic API stop being second-class. There were two ways to build a
Fieldand only one of them was supported:fieldFromJsonattached a spec, whilecomponent(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: amakeFieldclosure, 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 underacceptDerivedSpecs, 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-09shipped
An agent can author, validate and cook a graph without writing TypeScript. A
pcgCLI —nodes,fields,validate,cook,inspect,render— where--jsonselects a rendering of the same result rather than a separate code path, exit codes are 0 / 1 (failure) / 2 (misuse), andrenderwrites a deterministic top-down SVG that diffs in git. Subgraph nodes gain exposed params, and a primitive becomes a registered subgraph cited from JSON asref: { 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 aspcg-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-curvewas dropped becausesplineSampleneeds a polyline and no node produced one. 2217 tests. -
v0.11.0 2026-08-09shipped
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,pathResampleandwriteTangentstake node types 32 → 35 and restoreplace/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'scountnow means exactlycount. 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, becauseeditor/**anddemos/**import the library by package name, which resolves through the exports map intodist/, and a clean checkout has none. 2463 tests. -
v0.12.0 2026-08-09shipped
Scale-aware declutter.
selfPrunegains a field-capableminDistance, so the radius is per point rather than per graph — big trees need more room than bushes — under amax(rA, rB)symmetry rule, so no kept point ever has another kept point inside its own radius. Apriorityattribute 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 withprioritydeleted all 12 authored plots still survived. The exclusion is now 3, the contest is real, and a test requires that deletingprioritycosts authored plots. The topology phase this release replaced was re-surveyed and split in three: no existing source could derive a halo, becausepointScatterInBoundscomputes 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-09shipped
World-anchored sources, and the networks they unblocked.
pointScatterInWorldreads 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,connectPointsemits one 2-vertexpolylineprimitive 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.filterPrimitivesByBoundsis 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:findPathand 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-10shipped
A gap v0.13.0 opened, closed. Per-edge values became a headline capability — a road carries a
widthpromoted from its endpoints and akindfrom 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,surfaceSampleandplace/along-curvenow carry the source primitive's attributes onto the samples through one shared helper;pathResamplealso keeps its own output primitives' attributes, whichsetPolylineTopologyhad 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 setroadWidthgets 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 unrelatedlengthAttrwidens the samples. A collision with a name the node itself owns is refused, naming node, attribute and fix.transferAttributereads a primitive source inuvandraycastmodes;nearestrefuses and names the route instead.pcg render --attrreaches the primitive domain, colouring paths from primitive values. 46 of 46 planned phases; 2675 tests. -
v0.15.0 2026-08-11shipped
Cooks leave the main thread. A profile of a streamed world found the freeze nobody's budget could slice:
budgetMsyields between nodes, and one noise field over a cell's points is a ~100 ms atom — sopcg-ts/workerships aCookWorkerPoolthat 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 sou32index columns survive exactly, errors rehydrate into real classes with the node name and the fix intact, and a pooledWorldmatches a no-poolWorldbyte-for-byte along a streaming path with evictions.LevelDefgainsbindPatches, the serializable sibling ofbind(), 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'sdisposeevent, 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-17shipped
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.spacingdecides 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 whenselfPrunewas already doing it on the larger of two radii, whichconnectPointsnow 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 text —
printFieldSpecandparseFieldTextare 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,sqrtandstepjoin the grammar, three of their four designs chosen by a device probe rather than by reading the spec; graph-scoped params withtargetsdrive the half of the format an expression cannot reach; andpcg assetsreports what a graph requires without cooking it, saying plainly when the answer is a lower bound. 4323 tests. -
v0.17.0 2026-08-28shipped
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.attributescarries named columns with the dtype preserved rather than widened to f32, because CPU field evaluation rounds throughMath.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.colorsis now sugar for the reserved"color"entry rather than a sibling — one buffer, two spellings, one reader — anditemSizeis 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
attributesandcolorsdropped 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 frombatch.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 byownsGeometry— and colour alone does not clone, becauseinstanceColoris 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
u32channel withgpuTypeat three's default and withgpuType = IntTypeare identical —WebGLBindingStatestests the array type first and short-circuits. The 224 collision is real but belongs to an f32 column, which is whatdemos/lanternsdraws: au32seed 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/panelspublishes the graph panel format with a validator that actually validates, which is the answer to a request forstep/label/grouponParamSchema— declined, because grouping is per-graph presentation and not a node-type fact.toDeviceInstanceObjectsgives device instancing a World-free path, symmetric withtoInstancedMeshes. A node type now declaresselfMetered, becauseonNodeDone.elapsedMsis 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.sideEffectsships as an array, neverfalse, since all registration lives in content-hashed chunks and a blanketfalsewould empty the registry;./dist/three/index.jsis deliberately absent from it, so the entry a three host pulls stays shakeable. Thethreepeer 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 typesAnyInstanceBatchandSubgraphPlumbing, and the worker protocol'sEncodedInstanceBatch.colorsbecomesattributes. 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 throughpcg-ts/workerarrives zero-copy on the transfer list. 6,122 tests. -
next unscheduledexploring
All 46 planned phases are done, and the entry that stood here — a
residentdescriptor forfilterByAttribute— 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 — movingsetAttribute("scale")ahead of the two filters in the forest recipe (nowgraphs/examples-forest.json) bought the same readback for no library work. What is left is small, and recorded in PLAN.md with the reasoning:fieldToJsonstill refuses with a message enumerating all three causes instead of naming the one that applies, and amaxDegreebounded 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.