From 66a1b5015bdc5871a7c961a0f71733bb86900f70 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 3 Sep 2026 19:19:02 -0700 Subject: [PATCH 1/4] refactor(data): make Schema.interpolators serializable names, not functions A Schema must be pure JSON data. The animation interpolation overrides were the only function-valued field: Quat.schema declared { linear: slerp }. Replace the functions with serializable names (Quat: { linear: "slerp" }); data-gpu's animation-track resolves the name to a function via a small registry it owns, and throws on an unregistered name. Data declares intent; the consumer owns behavior. Adds interpolate unit tests (registry resolution, fallback, unknown-name throw). Co-Authored-By: Claude Opus 4.8 --- .../animation-track/interpolate.test.ts | 27 +++++++++++++++++++ .../animation/animation-track/interpolate.ts | 12 ++++++--- .../animation-track/interpolator-registry.ts | 14 ++++++++++ packages/data/src/math/quat/schema.ts | 5 ++-- packages/data/src/schema/schema.ts | 16 ++++++----- 5 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/data-gpu/src/graphics/animation/animation-track/interpolate.test.ts create mode 100644 packages/data-gpu/src/graphics/animation/animation-track/interpolator-registry.ts diff --git a/packages/data-gpu/src/graphics/animation/animation-track/interpolate.test.ts b/packages/data-gpu/src/graphics/animation/animation-track/interpolate.test.ts new file mode 100644 index 00000000..0f1b91e5 --- /dev/null +++ b/packages/data-gpu/src/graphics/animation/animation-track/interpolate.test.ts @@ -0,0 +1,27 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { describe, it, expect } from "vitest"; +import { Quat } from "@adobe/data/math"; +import type { Schema } from "@adobe/data/schema"; +import { interpolate } from "./interpolate.js"; + +describe("interpolate", () => { + it("resolves a schema's named interpolator via the registry (Quat linear ⇒ slerp)", () => { + const a: Quat = [0, 0, 0, 1]; // identity + const b: Quat = [0, Math.SQRT1_2, 0, Math.SQRT1_2]; // 90° about Y + // Must equal slerp exactly — proving it dispatched to the registered + // "slerp" function, not the componentwise-lerp fallback (which would + // produce a different, non-normalized result). + expect(interpolate(Quat.schema, "linear", a, b, 0.5)).toEqual(Quat.slerp(a, b, 0.5)); + }); + + it("falls back to componentwise lerp when no interpolator is named", () => { + const schema: Schema = { type: "array", items: { type: "number" } }; + expect(interpolate(schema, "linear", [0, 10], [10, 20], 0.5)).toEqual([5, 15]); + }); + + it("throws on a named interpolator that isn't registered", () => { + const schema: Schema = { type: "number", interpolators: { linear: "nope" } }; + expect(() => interpolate(schema, "linear", 0, 1, 0.5)).toThrow(/unknown interpolator "nope"/); + }); +}); diff --git a/packages/data-gpu/src/graphics/animation/animation-track/interpolate.ts b/packages/data-gpu/src/graphics/animation/animation-track/interpolate.ts index 8fdbcbc1..a6a57166 100644 --- a/packages/data-gpu/src/graphics/animation/animation-track/interpolate.ts +++ b/packages/data-gpu/src/graphics/animation/animation-track/interpolate.ts @@ -3,11 +3,13 @@ import type { Schema } from "@adobe/data/schema"; import type { InterpolationMode } from "../interpolation-mode/interpolation-mode.js"; import { componentwiseLerp } from "./componentwise-lerp.js"; +import { interpolatorRegistry } from "./interpolator-registry.js"; /** * Dispatches to a schema-declared interpolator if present, otherwise falls * back to a sensible default: `step` returns `next`, `linear` walks the schema - * and lerps numeric leaves. `cubicSpline` requires a schema override. + * and lerps numeric leaves. `cubicSpline` requires a schema override. A schema + * names its interpolator (pure JSON); the name is resolved via the registry. */ export function interpolate( schema: Schema, @@ -16,8 +18,12 @@ export function interpolate( next: any, t: number, ): any { - const custom = schema.interpolators?.[mode]; - if (custom) return custom(prev, next, t); + const name = schema.interpolators?.[mode]; + if (name !== undefined) { + const custom = interpolatorRegistry[name]; + if (!custom) throw new Error(`interpolate: unknown interpolator "${name}" for mode "${mode}"`); + return custom(prev, next, t); + } if (mode === "step") return prev; if (mode === "linear") return componentwiseLerp(schema, prev, next, t); throw new Error(`interpolate: schema type "${schema.type}" has no "${mode}" interpolator`); diff --git a/packages/data-gpu/src/graphics/animation/animation-track/interpolator-registry.ts b/packages/data-gpu/src/graphics/animation/animation-track/interpolator-registry.ts new file mode 100644 index 00000000..b47c14dc --- /dev/null +++ b/packages/data-gpu/src/graphics/animation/animation-track/interpolator-registry.ts @@ -0,0 +1,14 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { Quat } from "@adobe/data/math"; + +type Interpolator = (prev: any, next: any, t: number) => any; + +/** + * Resolves the serializable interpolator NAMES a pure-JSON `Schema` may declare + * (e.g. `Quat.schema`'s `interpolators.linear = "slerp"`) to their functions. + * The schema names the interpolator; the animation system owns the behavior. + */ +export const interpolatorRegistry: Readonly> = { + slerp: Quat.slerp, +}; diff --git a/packages/data/src/math/quat/schema.ts b/packages/data/src/math/quat/schema.ts index 2c95c3ba..3c732269 100644 --- a/packages/data/src/math/quat/schema.ts +++ b/packages/data/src/math/quat/schema.ts @@ -2,7 +2,6 @@ import { F32 } from "../f32/index.js"; import { Schema } from "../../schema/index.js"; -import { slerp } from "./slerp.js"; export const schema = { type: 'array', @@ -10,8 +9,10 @@ export const schema = { minItems: 4, maxItems: 4, default: [0, 0, 0, 1], // identity quaternion + // "slerp" (spherical linear interpolation) — the animation system resolves + // this name to Quat.slerp so quaternion tracks interpolate on the 4-sphere. interpolators: { - linear: slerp, + linear: "slerp", }, } as const satisfies Schema; diff --git a/packages/data/src/schema/schema.ts b/packages/data/src/schema/schema.ts index 0c9240f9..ba7b89ab 100644 --- a/packages/data/src/schema/schema.ts +++ b/packages/data/src/schema/schema.ts @@ -110,13 +110,15 @@ export interface Schema { const?: any; enum?: readonly any[]; layout?: Layout; // Memory layout for typed buffers (std140 or packed) - // Per-type interpolation overrides used by the animation system. Schemas omit - // this when the componentwise lerp / step default is correct (Vec3, scalar, …). - // Quat declares { linear: slerp } so quaternion tracks are interpolated on the - // 4-sphere instead of component-wise. + // Per-type interpolation overrides for the animation system, as serializable + // NAMES (not functions — a Schema is pure JSON data). Each name is resolved to + // an interpolator by the consuming animation system's registry (see + // `@adobe/data-gpu` animation-track). Schemas omit this when the componentwise + // lerp / step default is correct (Vec3, scalar, …); `Quat` declares + // `{ linear: "slerp" }` so quaternion tracks interpolate on the 4-sphere. interpolators?: { - readonly linear?: (prev: any, next: any, t: number) => any; - readonly step?: (prev: any, next: any, t: number) => any; - readonly cubicSpline?: (prev: any, next: any, t: number) => any; + readonly linear?: string; + readonly step?: string; + readonly cubicSpline?: string; }; } From b51de3cb7ea4c7e306fd8ee967b437aebce9c1df Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 3 Sep 2026 19:26:47 -0700 Subject: [PATCH 2/4] feat(data): make base Service.schema slot observable (Observe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A service's discoverable interface can change at runtime (e.g. once an iframe-projected service loads and reveals a different surface), so the schema slot is now Observe instead of a static Schema. It stays on the base Service, so it remains in the keyof-Service exclusion — IsValid never checks it, which is why schema need not be Data (Schema isn't Data, and forcing it to be deepens the type). createLazy exposes its static contract via Observe.fromConstant. Co-Authored-By: Claude Opus 4.8 --- .../async-data-service/create-lazy.test.ts | 8 ++++++-- .../service/async-data-service/create-lazy.ts | 5 +++-- packages/data/src/service/service.ts | 17 +++++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/data/src/service/async-data-service/create-lazy.test.ts b/packages/data/src/service/async-data-service/create-lazy.test.ts index 070f05de..0a0877fc 100644 --- a/packages/data/src/service/async-data-service/create-lazy.test.ts +++ b/packages/data/src/service/async-data-service/create-lazy.test.ts @@ -410,10 +410,14 @@ describe('createLazy', () => { const service = factory(); + // The schema slot is Observe; subscribing emits the constant synchronously. + let observed: unknown; + service.schema?.((s) => { observed = s; }); + assert({ given: 'a lazy service is created', - should: 'expose the same schema without triggering a load', - actual: `${service.schema === schema},${loaded}`, + should: 'expose its schema as a constant observable without triggering a load', + actual: `${observed === schema},${loaded}`, expected: 'true,false' }); }); diff --git a/packages/data/src/service/async-data-service/create-lazy.ts b/packages/data/src/service/async-data-service/create-lazy.ts index 9b61332c..6279a2d7 100644 --- a/packages/data/src/service/async-data-service/create-lazy.ts +++ b/packages/data/src/service/async-data-service/create-lazy.ts @@ -174,10 +174,11 @@ export function createLazy< } // Build lazy service object. Expose the schema up front (before load) so the - // lazy instance is introspectable without triggering a load. + // lazy instance is introspectable without triggering a load. The base slot is + // `Observe`, so publish the static contract as a constant observable. const lazyService: any = { serviceName: 'lazy-service', - schema, + schema: Observe.fromConstant(schema), }; // Wrap each member based on the strategy derived from its schema diff --git a/packages/data/src/service/service.ts b/packages/data/src/service/service.ts index 0ef35e5d..acf1253f 100644 --- a/packages/data/src/service/service.ts +++ b/packages/data/src/service/service.ts @@ -1,6 +1,7 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. import type { Schema } from "../schema/index.js"; +import type { Observe } from "../observe/index.js"; /** * A service is an object that provides functionality to an application. @@ -16,13 +17,17 @@ import type { Schema } from "../schema/index.js"; * A service's shape can be described by a `Schema` (an object schema whose * property schemas describe each member). The schema is authored beside the * service (e.g. `MyService.schema`) and validated with `IsValidWithCompleteSchema`; - * a factory may also attach it to the instance via the optional `schema` slot for - * runtime introspection. Both `serviceName` and `schema` are base-`Service` - * metadata, so they are excluded from `AsyncDataService.IsValid` and reserved as - * member names. See `async-data-service/is-valid-with-*-schema.ts`. + * a factory may also expose it at runtime via the optional `schema` slot for + * introspection. It is `Observe` — observable — because a service's + * discoverable interface can change at runtime (e.g. once an iframe-projected + * service loads and reveals a different surface). Both `serviceName` and `schema` + * are base-`Service` metadata, so they are excluded from `AsyncDataService.IsValid` + * (which is why `schema` need not be `Data`) and reserved as member names. + * See `async-data-service/is-valid-with-*-schema.ts`. */ export interface Service { readonly serviceName?: string; - /** Optional runtime copy of the service's schema (its authored contract). */ - readonly schema?: Schema; + /** Optional observable of the service's schema (its authored contract), which + * may change at runtime as the service's discoverable interface evolves. */ + readonly schema?: Observe; } From 6b2e67263c5e9090385bb65c47e3ca88aaac1ea7 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 3 Sep 2026 19:32:31 -0700 Subject: [PATCH 3/4] docs(data): note the observable Service.schema slot in create-lazy.md Co-Authored-By: Claude Opus 4.8 --- packages/data/src/service/async-data-service/create-lazy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data/src/service/async-data-service/create-lazy.md b/packages/data/src/service/async-data-service/create-lazy.md index 35f7091c..bb570073 100644 --- a/packages/data/src/service/async-data-service/create-lazy.md +++ b/packages/data/src/service/async-data-service/create-lazy.md @@ -2,7 +2,7 @@ ## Overview -`AsyncDataService.createLazy` provides a type-safe way to create lazy-loading wrapper factories for AsyncDataServices. The real service is only loaded when the first property is accessed. Wrapping is driven by the service's **sideloaded schema** — a `Schema` published beside the service (e.g. `MyService.schema`) rather than attached to instances. +`AsyncDataService.createLazy` provides a type-safe way to create lazy-loading wrapper factories for AsyncDataServices. The real service is only loaded when the first property is accessed. Wrapping is driven by the service's **sideloaded schema** — a `Schema` authored beside the service (e.g. `MyService.schema`) and passed to `createLazy`. (Separately, a service may also expose that schema at runtime via the optional base-`Service` `schema` slot, typed `Observe` so it can change as the interface evolves.) ## Import From 3cb96e3566ed6b7f625c62f4d874e629434abb2c Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 3 Sep 2026 19:33:24 -0700 Subject: [PATCH 4/4] chore(data): bump publishable packages to 0.10.9 Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- packages/data-ai/.claude-plugin/plugin.json | 2 +- packages/data-ai/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data-testing/package.json | 2 +- packages/data/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 8d5f086b..ca0e555a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.8", + "version": "0.10.9", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 62a20bf4..6943da17 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.10.8", + "version": "0.10.9", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index 72a50bcc..5f45b3da 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.8", + "version": "0.10.9", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index 0364e43b..e637c416 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.8", + "version": "0.10.9", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index c9c522e1..f0c3f1cd 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.8", + "version": "0.10.9", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 81e196cb..6d5d3bf1 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.8", + "version": "0.10.9", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 2ae887a1..d7f7a1a5 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.8", + "version": "0.10.9", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 0587e437..2239b05a 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.8", + "version": "0.10.9", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index e1d2e770..dd30c769 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.8", + "version": "0.10.9", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data-testing/package.json b/packages/data-testing/package.json index 781198c6..7431781b 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.8", + "version": "0.10.9", "description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index acb781b3..62cf0719 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.8", + "version": "0.10.9", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false,