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-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-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, 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; }; } 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 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; }