From 7027826e38520686df97a8bfb6112beed6a06b6e Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 15 Oct 2026 10:11:55 -0700 Subject: [PATCH] fix(ecs): index an inserted row by its stored values, not the insert payload An index keyed or ordered by a defaultFactory component missed rows whose insert omitted it: the archetype minted the value but the index was fed the raw payload. applyInsert now reads the stored row, like applyUpdate. Co-Authored-By: Claude Opus 5.5 --- 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-rpc/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 +- .../src/ecs/database/default-factory.test.ts | 42 +++++++++++++++++++ .../index-registry/create-index-registry.ts | 12 ++++-- .../data/src/ecs/store/public/create-store.ts | 9 ++-- 15 files changed, 68 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 1bab6bc03..9133fb572 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.27", + "version": "0.10.28", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 7ceb73ca9..39e6f46f3 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.27", + "version": "0.10.28", "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 386236ad0..6459baa90 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.27", + "version": "0.10.28", "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 59b474b59..782bdd22c 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.27", + "version": "0.10.28", "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 43d77f868..901a7bcd2 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.27", + "version": "0.10.28", "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 8ab7cf542..cc56f60f1 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.27", + "version": "0.10.28", "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 2e4ca0670..480ea1990 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.27", + "version": "0.10.28", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-rpc/package.json b/packages/data-rpc/package.json index cc12eec30..69fee63e7 100644 --- a/packages/data-rpc/package.json +++ b/packages/data-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-rpc", - "version": "0.10.27", + "version": "0.10.28", "description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.", "type": "module", "sideEffects": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 54f3dc22a..52a5ba62a 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.27", + "version": "0.10.28", "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 8305fa001..418c9bc87 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.27", + "version": "0.10.28", "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 51937b276..9c4ec1934 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.27", + "version": "0.10.28", "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 d8070422b..44b2c4cb0 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.27", + "version": "0.10.28", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/ecs/database/default-factory.test.ts b/packages/data/src/ecs/database/default-factory.test.ts index 598d1c8ff..ec3407768 100644 --- a/packages/data/src/ecs/database/default-factory.test.ts +++ b/packages/data/src/ecs/database/default-factory.test.ts @@ -75,6 +75,48 @@ describe("insert-time default-factory (full plugin chain)", () => { }); }); +// An index whose key or order reads a default-factory column must file the row +// under the value the archetype MINTED, not the omitted insert payload. +describe("indexes over a default-factory component", () => { + const indexedPlugin = Database.Plugin.create({ + components: { guid, value }, + archetypes: { Node: ["guid", "value"] } as const, + indexes: { + byValue: { key: "value", order: { by: ["guid"] } }, + byGuid: { key: "guid" }, + }, + transactions: { + addNode(t, args: { value: number }) { + return t.archetypes.Node.insert(args); + }, + adoptNode(t, args: { value: number; guid: number }) { + return t.archetypes.Node.insert(args); + }, + }, + }); + + const create = () => { + let next = 1000; + return Database.create(indexedPlugin, { defaultFactories: { guid: () => next-- } }); + }; + + it("orders a row with a minted default-factory value on insert", () => { + const db = create(); + const minted = db.transactions.addNode({ value: 1 }); // guid 1000 + const supplied = db.transactions.adoptNode({ value: 1, guid: 2000 }); + const mintedLater = db.transactions.addNode({ value: 1 }); // guid 999 + + expect(db.indexes.byValue.find({ value: 1 })).toEqual([mintedLater, minted, supplied]); + }); + + it("keys a row by a minted default-factory value on insert", () => { + const db = create(); + const minted = db.transactions.addNode({ value: 1 }); // guid 1000 + + expect(db.indexes.byGuid.find({ guid: 1000 })).toEqual([minted]); + }); +}); + // Compile-time checks — the payoff for consumers: inside a transaction / system // body (the mutable store `t`), default-factory components are optional at insert // while every other component stays required. This is the shape the ~30 insert diff --git a/packages/data/src/ecs/database/index-registry/create-index-registry.ts b/packages/data/src/ecs/database/index-registry/create-index-registry.ts index 5642013a5..33819ae1d 100644 --- a/packages/data/src/ecs/database/index-registry/create-index-registry.ts +++ b/packages/data/src/ecs/database/index-registry/create-index-registry.ts @@ -74,8 +74,10 @@ export interface IndexRegistry { /** * Reflect a single insert into the entity's archetype. Dispatches to only * the indexes applicable to that archetype — O(applicable), not O(indexes). + * Reads the STORED row rather than taking the insert payload: the archetype + * mints `defaultFactory` components the payload omits. */ - applyInsert(entity: Entity, archetype: IndexableArchetype, values: Readonly>): void; + applyInsert(entity: Entity, archetype: IndexableArchetype): void; /** * Reflect a single update by re-reading the entity's full values. `from` * is the archetype before the update, `to` the archetype after (they @@ -239,9 +241,13 @@ export const createIndexRegistry = ( return ""; }; - const applyInsert = (entity: Entity, archetype: IndexableArchetype, values: Readonly>): void => { + const applyInsert = (entity: Entity, archetype: IndexableArchetype): void => { if (indexes.size === 0) return; - for (const idx of indexesFor(archetype)) idx.add(entity, values); + const applicable = indexesFor(archetype); + if (applicable.length === 0) return; + const values = read(entity); + if (values === null) return; + for (const idx of applicable) idx.add(entity, values); }; const applyUpdate = (entity: Entity, from: IndexableArchetype | null, to: IndexableArchetype | null): void => { diff --git a/packages/data/src/ecs/store/public/create-store.ts b/packages/data/src/ecs/store/public/create-store.ts index 399c2e692..3e6fd4ef6 100644 --- a/packages/data/src/ecs/store/public/create-store.ts +++ b/packages/data/src/ecs/store/public/create-store.ts @@ -98,9 +98,10 @@ export function createStore< // delete). Higher layers (`db.indexes`, `t.indexes`) expose this same // map by reference. // - // The reader is only used for `applyUpdate` — the patch passed to - // `store.update` may omit components an index covers, so the registry - // re-reads the full record to recompute the key. Seeding goes through + // The reader serves `applyInsert` and `applyUpdate` — an insert payload may + // omit `defaultFactory` components the archetype mints, and an update patch + // may omit components an index covers, so the registry re-reads the stored + // record to compute the key. Seeding goes through // `seedIndexFromArchetypes` instead (see below). const indexRegistry = createIndexRegistry( // `core.read` already excludes `id` (the identity is never a useful index @@ -266,7 +267,7 @@ export function createStore< // the applicable indexes. indexRegistry.checkUniqueAvailableForInsert(archetype, values); const entity = rawInsert(values); - indexRegistry.applyInsert(entity, archetype, values); + indexRegistry.applyInsert(entity, archetype); return entity; }; };