From 2fdf0f73df4cbd656bd388bba12b130a29b511d4 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Fri, 14 Aug 2026 21:58:46 -0700 Subject: [PATCH 1/2] probe: fix combined-facets action inference; add BlobRef namespace + type tests Failure B: a computed factory declared alongside actions in one Database.Plugin.create collapsed action inference to ToActionFunctions<{}>. The computed-factory db type referenced this call's own AD (inferred from the sibling actions property), making AD un-inferable. Drop local AD from the computed db (base actions only), matching the action factory db and property order. Zero as casts. Also: additive namespaced BlobRef (BlobRef.schema / BlobRef.is) mirroring BlobHandle/BlobMeta, plus red/green type tests for both plugin failures and a BlobRef namespace proof. Co-Authored-By: Claude Opus 4.8 --- .../src/cache/blob-ref-namespace.type-test.ts | 31 +++++ packages/data/src/cache/blob-store.ts | 15 +++ packages/data/src/cache/index.ts | 2 +- ...bined-facets-action-inference.type-test.ts | 116 ++++++++++++++++ .../computed-factory-services.type-test.ts | 127 ++++++++++++++++++ .../data/src/ecs/database/create-plugin.ts | 12 +- 6 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 packages/data/src/cache/blob-ref-namespace.type-test.ts create mode 100644 packages/data/src/ecs/database/combined-facets-action-inference.type-test.ts create mode 100644 packages/data/src/ecs/database/computed-factory-services.type-test.ts diff --git a/packages/data/src/cache/blob-ref-namespace.type-test.ts b/packages/data/src/cache/blob-ref-namespace.type-test.ts new file mode 100644 index 00000000..6622ea68 --- /dev/null +++ b/packages/data/src/cache/blob-ref-namespace.type-test.ts @@ -0,0 +1,31 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { BlobRef } from "./blob-store.js"; +import { Schema } from "../schema/index.js"; +import { Nullable } from "../schema/nullable.js"; +import type { Assert } from "../types/assert.js"; +import type { Equal } from "../types/equal.js"; + +/** + * The namespaced `BlobRef` exposes `schema` and `is` under the type's own name, + * so consumers reach them without importing the standalone `BlobRefSchema` / + * `isBlobRef` — mirroring `BlobHandle` / `BlobMeta`. + */ + +// `BlobRef.schema` is a Schema whose `ToType` is exactly `BlobRef`. +type _SchemaMatchesType = Assert, BlobRef>>; + +// `BlobRef.is` is the type guard. +function narrows(value: unknown) { + if (BlobRef.is(value)) { + const _ok: BlobRef = value; + } +} + +// An ECS component that stores a BlobRef has no natural empty value; the honest +// model is a nullable column with a `null` default — no `as unknown as` cast. +const imageComponent = { ...Nullable(BlobRef.schema), default: null } as const; +type _NullableType = Assert, + BlobRef | null +>>; diff --git a/packages/data/src/cache/blob-store.ts b/packages/data/src/cache/blob-store.ts index c14f7301..ad4fad1a 100644 --- a/packages/data/src/cache/blob-store.ts +++ b/packages/data/src/cache/blob-store.ts @@ -55,6 +55,21 @@ export function isBlobRef(value: unknown): value is BlobRef { return isRemoteBlobRef(value) || isLocalBlobRef(value); } +/** + * Namespaced surface for {@link BlobRef}, mirroring `BlobHandle` / `BlobMeta` + * in `../blob/`. Lets consumers reach the schema and guard through the type's + * own name — `BlobRef.schema`, `BlobRef.is(x)` — instead of importing the + * standalone `BlobRefSchema` / `isBlobRef`. An ECS component that stores a + * BlobRef has no natural empty value, so model the column as + * `Nullable(BlobRef.schema)` with a `null` default (no cast) rather than a + * `null as unknown as BlobRef` placeholder. + */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace BlobRef { + export const schema = BlobRefSchema; + export const is = isBlobRef; +} + function isRemoteUrl(url: string): url is RemoteUrl { return url.startsWith(remoteUrlPrefix); } diff --git a/packages/data/src/cache/index.ts b/packages/data/src/cache/index.ts index 69e099b9..fe0d5c88 100644 --- a/packages/data/src/cache/index.ts +++ b/packages/data/src/cache/index.ts @@ -2,7 +2,7 @@ export { type BlobStore, - type BlobRef, + BlobRef, blobStore, isBlobRef, BlobRefSchema, diff --git a/packages/data/src/ecs/database/combined-facets-action-inference.type-test.ts b/packages/data/src/ecs/database/combined-facets-action-inference.type-test.ts new file mode 100644 index 00000000..0fa654ea --- /dev/null +++ b/packages/data/src/ecs/database/combined-facets-action-inference.type-test.ts @@ -0,0 +1,116 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { createPlugin } from "./create-plugin.js"; +import { Database } from "./database.js"; +import type { Observe } from "../../observe/index.js"; +import type { Assert } from "../../types/assert.js"; +import type { Equal } from "../../types/equal.js"; + +/** + * Regression: action inference must survive when `computed` and `actions` are + * declared in the SAME `Database.Plugin.create` call. + * + * The bug: the `computed` factory's `db` type referenced the plugin's OWN + * action declarations (`AD`) — the same type parameter being inferred from the + * `actions` property. That self-reference made `AD` un-inferable whenever a + * sibling `computed` was present in the same call, so it collapsed to `{}` and + * every action vanished (`db.actions.x` → TS2339 on `ToActionFunctions<{}>`). + * Splitting the facets across layered `create`s hid the bug because each layer + * inferred `AD` in isolation. + * + * The fix: a `computed` factory's `db` surfaces only the BASE plugin's actions + * (`XP['actions'] & IP['actions']`), never the same-call `actions`. Those are + * declared AFTER `computed` in the enforced property order, so reading one from + * a sibling computed was always a forward reference — the action factory `db` + * already excludes them for the same reason. + */ + +// ============================================================================ +// 1. POSITIVE — computed + actions in one create; actions stay fully typed. +// ============================================================================ + +const combined = createPlugin({ + components: { count: { type: "number" } }, + computed: { + doubled: (_db): Observe => (() => () => {}), + }, + actions: { + bump: (_db, _n: number) => { }, + }, +}); + +function combinedActionsAreCallable() { + const db = Database.create(combined); + db.actions.bump(1); // was TS2339 before the fix +} + +// The declared action survives into the resolved database surface — before the +// fix this key was absent (the action map collapsed to `{}`). +type _ActionResolved = Assert["actions"] ? true : false, + true +>>; + +// Negative guard: a genuinely-absent action is still rejected. +function combinedRejectsUnknownAction() { + const db = Database.create(combined); + // @ts-expect-error — `nope` was never declared. + db.actions.nope(); +} + +// ============================================================================ +// 2. Layered create (control) — always worked; must keep working. +// ============================================================================ + +const base = createPlugin({ components: { count: { type: "number" } } }); +const withComputed = createPlugin({ + extends: base, + computed: { doubled: (_db): Observe => (() => () => {}) }, +}); +const withActions = createPlugin({ + extends: withComputed, + actions: { bump: (_db, _n: number) => { } }, +}); + +function layeredActionsAreCallable() { + const db = Database.create(withActions); + db.actions.bump(1); +} + +// ============================================================================ +// 3. Semantics — a computed sees BASE actions but not same-create siblings. +// ============================================================================ + +const actionBase = createPlugin({ + components: { count: { type: "number" } }, + actions: { baseAct: (_db) => { } }, +}); + +createPlugin({ + extends: actionBase, + computed: { + // A computed CAN compose on a base plugin's already-resolved action. + readsBaseAction: (db): Observe => { + db.actions.baseAct(); + return (() => () => {}); + }, + }, + actions: { + siblingAct: (_db) => { }, + }, +}); + +createPlugin({ + components: { count: { type: "number" } }, + computed: { + noSiblingActions: (db): Observe => { + // @ts-expect-error — a same-create sibling action is a forward + // reference (actions are declared after computed) and must be absent. + db.actions.siblingAct2(); + return (() => () => {}); + }, + }, + actions: { + siblingAct2: (_db) => { }, + }, +}); diff --git a/packages/data/src/ecs/database/computed-factory-services.type-test.ts b/packages/data/src/ecs/database/computed-factory-services.type-test.ts new file mode 100644 index 00000000..10d2170a --- /dev/null +++ b/packages/data/src/ecs/database/computed-factory-services.type-test.ts @@ -0,0 +1,127 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { createPlugin } from "./create-plugin.js"; +import { Database } from "./database.js"; +import type { Observe } from "../../observe/index.js"; +import type { Assert } from "../../types/assert.js"; +import type { Equal } from "../../types/equal.js"; + +/** + * A declared service must keep its full type when read from a `computed` + * factory's `db.services.`. + * + * This was reported as lost — a `computed` factory reading a service off the + * extended chain saw it as `unknown`, forcing a cast at the boundary. On the + * current type surface the service type is threaded correctly (the + * computed-factory `db` is built by `FullDBForPlugin`, whose service slot is + * `FromServiceFactories & XP['services']>`), so these positive + * checks hold. This file pins that behavior so the loss cannot silently return. + * + * The check is exercised across every shape that reported the loss: the service + * on an `extends` base, on a combined base, declared locally in the same call, + * and imported — including the case where the service factory itself reads a + * further chain service. The asymmetry originally cited (a `systems` `create` + * body and the fully-resolved `Database.FromPlugin<...>['services']` typed the + * same service correctly) is pinned too. + */ + +type Thing = { readonly id: number }; +type HostThing = { readonly hostId: number }; + +const servicePlugin = createPlugin({ + services: { + imageCompositor: (_db): Observe => (() => () => {}), + }, +}); + +// 1. Service on an `extends` base — read from a computed factory. +createPlugin({ + extends: servicePlugin, + computed: { + probe: (db): Observe => { + type Svc = typeof db.services.imageCompositor; + type _Ok = Assert>>; + // Negative guard: a service that was never declared is absent. + // @ts-expect-error — `missing` is not a declared service. + db.services.missing; + return (() => () => {}); + }, + }, +}); + +// 2. Service declared LOCALLY in the same create as the computed. +createPlugin({ + services: { + imageCompositor: (_db): Observe => (() => () => {}), + }, + computed: { + probe: (db): Observe => { + type Svc = typeof db.services.imageCompositor; + type _Ok = Assert>>; + return (() => () => {}); + }, + }, +}); + +// 3. Service via `imports`. +createPlugin({ + imports: servicePlugin, + computed: { + probe: (db): Observe => { + type Svc = typeof db.services.imageCompositor; + type _Ok = Assert>>; + return (() => () => {}); + }, + }, +}); + +// 4. Faithful multi-layer chain: a service on a plugin that `extends` a +// `combine`, whose factory reads a further chain service; the type must +// still survive one layer down in a computed. +const hostPlugin = createPlugin({ + services: { + host: (_db): HostThing => { throw new Error("inject"); }, + }, +}); +const scenePlugin = createPlugin({ + components: { sceneName: { type: "string" } }, +}); +const servicesPlugin = createPlugin({ + extends: Database.Plugin.combine(scenePlugin, hostPlugin), + services: { + imageCompositor: (db): Observe => { + const _h: HostThing = db.services.host; // reads a chain service + return (() => () => {}); + }, + }, +}); +createPlugin({ + extends: servicesPlugin, + computed: { + status: (db): Observe => { + type Svc = typeof db.services.imageCompositor; + type _Ok = Assert>>; + return (() => () => {}); + }, + }, +}); + +// 5. Asymmetry pins — the same service typed correctly on a `systems` create +// body and on the fully-resolved FromPlugin services surface. +createPlugin({ + extends: servicePlugin, + systems: { + probeSys: { + create: (db) => { + type Svc = typeof db.services.imageCompositor; + type _Ok = Assert>>; + return () => { }; + }, + }, + }, +}); + +type _FromPluginServices = Assert["services"]["imageCompositor"], + Observe +>>; diff --git a/packages/data/src/ecs/database/create-plugin.ts b/packages/data/src/ecs/database/create-plugin.ts index 653d1aec..9764e401 100644 --- a/packages/data/src/ecs/database/create-plugin.ts +++ b/packages/data/src/ecs/database/create-plugin.ts @@ -194,7 +194,15 @@ export function createPlugin< const AD, const S extends string = never, const SVF extends ServiceFactories>> = {}, - const CVF extends PluginComputedFactories, RemoveIndex, RemoveIndex, RemoveIndex, S, RemoveIndex & XP['actions'] & IP['actions'], AmbientPlugin, RemoveIndex, RemoveIndex>> = {}, + // The computed-factory `db` surfaces only the BASE plugins' actions + // (`XP['actions'] & IP['actions']`), never this call's own `AD`. `AD` is + // inferred from the sibling `actions` property, so referencing it in the + // `computed` contextual type made `AD` un-inferable whenever both facets + // shared one `create` — it collapsed to `{}` and every action vanished. + // Excluding it is also correct semantically: `actions` are declared after + // `computed`, so a computed reading a same-call action is a forward + // reference — the action factory `db` already omits `AD` for the same reason. + const CVF extends PluginComputedFactories, RemoveIndex, RemoveIndex, RemoveIndex, S, XP['actions'] & IP['actions'], AmbientPlugin, RemoveIndex, RemoveIndex>> = {}, >( plugins: { imports?: IP, @@ -206,7 +214,7 @@ export function createPlugin< resources?: RS, archetypes?: A, indexes?: IX, - computed?: CVF & PluginComputedFactories, RemoveIndex, RemoveIndex, {}, string, RemoveIndex & XP['actions'] & IP['actions'], AmbientPlugin, RemoveIndex, RemoveIndex>>, + computed?: CVF & PluginComputedFactories, RemoveIndex, RemoveIndex, {}, string, XP['actions'] & IP['actions'], AmbientPlugin, RemoveIndex, RemoveIndex>>, transactions?: TD, actions?: AD & { readonly [K: string]: (db: Database< From 209b0e0bcdb3bb84a73a2d366e5b40d7dd6b38aa Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Fri, 14 Aug 2026 22:06:09 -0700 Subject: [PATCH 2/2] Bump version to 0.9.99 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-hopper/package.json | 2 +- packages/data-gpu-samples/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit-space-rock-game/package.json | 2 +- packages/data-lit-tictactoe/package.json | 2 +- packages/data-lit-todo/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-p2p-tictactoe/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react-hello/package.json | 2 +- packages/data-react-pixie/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid-dashboard/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 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index a9385cc9..4db6ae10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.98", + "version": "0.9.99", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 6eb95efc..072c8435 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.9.98", + "version": "0.9.99", "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 c41308fa..b6e7ff02 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.9.98", + "version": "0.9.99", "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-hopper/package.json b/packages/data-gpu-hopper/package.json index a41d38f7..d9949a5d 100644 --- a/packages/data-gpu-hopper/package.json +++ b/packages/data-gpu-hopper/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-hopper", - "version": "0.9.98", + "version": "0.9.99", "description": "Hopper sample - real-time ECS game rendered as colored cubes via @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu-samples/package.json b/packages/data-gpu-samples/package.json index 78e5d6d1..658bc36c 100644 --- a/packages/data-gpu-samples/package.json +++ b/packages/data-gpu-samples/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-samples", - "version": "0.9.98", + "version": "0.9.99", "description": "WebGPU samples built on @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index c55f126e..48fcca18 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.9.98", + "version": "0.9.99", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-space-rock-game/package.json b/packages/data-lit-space-rock-game/package.json index 4e75e34b..fb0e7846 100644 --- a/packages/data-lit-space-rock-game/package.json +++ b/packages/data-lit-space-rock-game/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-space-rock-game", - "version": "0.9.98", + "version": "0.9.99", "description": "Space Rock Game sample - real-time ECS game with Lit and @adobe/data", "type": "module", "private": true, diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index 17d36bf6..0b992734 100644 --- a/packages/data-lit-tictactoe/package.json +++ b/packages/data-lit-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-tictactoe", - "version": "0.9.98", + "version": "0.9.99", "description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService", "type": "module", "private": true, diff --git a/packages/data-lit-todo/package.json b/packages/data-lit-todo/package.json index 6ea9de8c..c72da5d7 100644 --- a/packages/data-lit-todo/package.json +++ b/packages/data-lit-todo/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-todo", - "version": "0.9.98", + "version": "0.9.99", "description": "Todo application - Lit web components with @adobe/data ECS", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 0576605a..34ab43f3 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.98", + "version": "0.9.99", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-p2p-tictactoe/package.json b/packages/data-p2p-tictactoe/package.json index ec6dc2d0..920de966 100644 --- a/packages/data-p2p-tictactoe/package.json +++ b/packages/data-p2p-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-p2p-tictactoe", - "version": "0.9.98", + "version": "0.9.99", "description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync", "type": "module", "private": true, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 58dd9b7b..f16a3c8c 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.98", + "version": "0.9.99", "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-hello/package.json b/packages/data-react-hello/package.json index 5c36aaf0..7addf12e 100644 --- a/packages/data-react-hello/package.json +++ b/packages/data-react-hello/package.json @@ -1,6 +1,6 @@ { "name": "data-react-hello", - "version": "0.9.98", + "version": "0.9.99", "description": "Hello World sample - click counter using @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react-pixie/package.json b/packages/data-react-pixie/package.json index d731f4d2..c362120c 100644 --- a/packages/data-react-pixie/package.json +++ b/packages/data-react-pixie/package.json @@ -1,6 +1,6 @@ { "name": "data-react-pixie", - "version": "0.9.98", + "version": "0.9.99", "description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index e3d2094d..806254f7 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.98", + "version": "0.9.99", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid-dashboard/package.json b/packages/data-solid-dashboard/package.json index 1f2918d4..13b184b3 100644 --- a/packages/data-solid-dashboard/package.json +++ b/packages/data-solid-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "data-solid-dashboard", - "version": "0.9.98", + "version": "0.9.99", "description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS", "type": "module", "private": true, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 8d44e842..60c0dcec 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.98", + "version": "0.9.99", "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 b607d438..0b34d244 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.98", + "version": "0.9.99", "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 959b8cde..257ed0db 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.9.98", + "version": "0.9.99", "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 3a16ece0..35fa7675 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.98", + "version": "0.9.99", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false,