@adobe/data-ai 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,8 @@ The `global/` rules (`namespace.md`, `cohesion.md`, `type-casts.md`,
35
35
  `paths:` globs (`**/*.ts` etc.) apply everywhere, by design, so the folder name
36
36
  makes that explicit. Alongside `features/`, the remaining rules-root `.md` files
37
37
  hold the other **cross-cutting** patterns the feature rules reference —
38
- `data-modelling.md`, `archetypes.md` (row iteration), `plugin-modelling.md`, and
38
+ `data-modelling.md`, `archetypes.md` (row iteration), `plugin-modelling.md`,
39
+ `versioning.md` (schema history & upgrade-on-load), and
39
40
  the UI file rules (`element.md`, `lazy-element.md`, `presentation.md`). They live
40
41
  here so the bundle is self-contained; this repo symlinks them into its own
41
42
  `.claude/rules/` (as it does `features/`).
@@ -0,0 +1,59 @@
1
+ ---
2
+ paths:
3
+ - '**/*-plugin.ts'
4
+ - '**/*-plugin/**/*.ts'
5
+ - '**/*-database.ts'
6
+ - '**/*-database/**/*.ts'
7
+ ---
8
+
9
+ # Archetypes — the ECS query & iteration model
10
+
11
+ General to all `@adobe/data` ECS use, feature-organized or not. The
12
+ `core-database/archetypes.ts` *file* conventions live in
13
+ `features/services/main-service/archetypes.md`.
14
+
15
+ An archetype is a named set of component keys — a kind of entity — and also a
16
+ **query**: `queryArchetypes(include, { exclude })` (and the `select` / `count`
17
+ built on it) returns every archetype whose columns are a **superset** of
18
+ `include`. So `count(archetype.components)` counts that kind and every kind that
19
+ extends it, and a base archetype no entity is inserted into is a first-class
20
+ query, not dead weight.
21
+
22
+ ## Model each logical supertype as a base archetype
23
+
24
+ Give a state-model supertype (`Layer` ⊃ `ImageLayer` / `GroupLayer`) its own
25
+ archetype over the shared components — with a matching `data/` type — and build
26
+ the subtypes off it:
27
+
28
+ ```ts
29
+ const layer = ["placement", "name", "visible", "opacity"] as const;
30
+ const archetypes = { Layer: layer, ImageLayer: ["image", ...layer, "asset"], GroupLayer: ["group", ...layer] };
31
+ ```
32
+
33
+ `count(archetypes.Layer.components)` then spans every layer kind, current and
34
+ future — correct by construction. Skip a supertype archetype only when nothing
35
+ queries over it.
36
+
37
+ ## Row iteration
38
+
39
+ **Select in the query, not the loop.** `queryArchetypes(include, { exclude })` takes
40
+ required and excluded component lists — use them; don't query wider then skip rows or
41
+ archetypes with an `if`.
42
+
43
+ **When every row migrates out, iterate tail → head.** Archetypes are densely
44
+ packed: removing a non-last row hole-fills by moving the tail into the gap.
45
+ Forward iteration while migrating every row pays that shift each step — and forces
46
+ an id snapshot to survive it. Reverse removes from the tail: no shift, no snapshot.
47
+
48
+ ```ts
49
+ // ✅ no shifts, no allocation
50
+ for (let i = arch.rowCount - 1; i >= 0; i--) {
51
+ db.store.update(arch.columns.id.get(i), { _worldMatrix: Mat4x4.identity });
52
+ }
53
+ ```
54
+
55
+ If only *some* rows migrate, snapshot just the ids you'll touch — forward is then fine.
56
+
57
+ **Don't snapshot what the query already filters.** A "every id right now" snapshot
58
+ is only needed when forward iteration would invalidate; reverse iteration or an
59
+ `exclude` clause removes the need.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/**/*.ts'
3
+ - '**/features/*/services/services.ts'
4
+ - '**/features/*/services/*-service/**/*.ts'
4
5
  ---
5
6
 
6
7
  # services/ — the feature's services
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/actions/**/*.ts'
3
+ - '**/features/*/services/main-service/action-database/actions/**/*.ts'
4
4
  ---
5
5
 
6
6
  # services/main-service/actions/ — async orchestration
@@ -5,10 +5,11 @@ paths:
5
5
 
6
6
  # core-database/archetypes.ts — the feature's archetypes
7
7
 
8
- An archetype is a named, ordered set of component keys — a kind of entity. One
9
- file, one export: `archetypes`, built with `Database.archetypes(components, …)`,
10
- which validates every key against the component map and preserves each
11
- archetype's literal tuple (so no per-archetype `as const satisfies`):
8
+ One file, one export: `archetypes`, built with `Database.archetypes(components, …)`,
9
+ which validates every key against the component map and preserves each archetype's
10
+ literal tuple (so no per-archetype `as const satisfies`). The archetype **model** —
11
+ query/superset semantics, base archetypes for logical supertypes, row iteration — is
12
+ the rules-root `archetypes.md`; this file is the declaration convention.
12
13
 
13
14
  ```ts
14
15
  import { Database } from "@adobe/data/ecs";
@@ -19,19 +20,24 @@ export const archetypes = Database.archetypes(components, {
19
20
  });
20
21
  ```
21
22
 
22
- - `UpperCase` archetype names — an archetype names a *shape*, the way a class
23
- name does.
23
+ - `UpperCase` names — an archetype names a *shape*, like a class.
24
24
  - Archetypes may **span scopes**: a document entity can include an ephemeral
25
- `session` column (e.g. a live `dragPosition` slot) — the `components` map
26
- already merges every scope, so keys from any scope validate.
27
- - Archetypes are a packing / iteration convenience, **not** part of the
28
- serialized data model.
25
+ `session` column — `components` merges every scope, so any-scope key validates.
26
+
27
+ ## satisfies CoreDatabase.Archetype
28
+
29
+ Type-check a shared base tuple against `CoreDatabase.Archetype` (= `readonly Component[]`;
30
+ see `core-database.md`) — the same type checks every `count` / `select` query key downstream:
31
+
32
+ ```ts
33
+ const layer = ["placement", "name", "visible", "opacity"] as const satisfies CoreDatabase.Archetype;
34
+ ```
29
35
 
30
36
  ## Drift guard against the data-type
31
37
 
32
- An archetype that mirrors a `data/` row type carries a **non-exported**
33
- compile-time check — infer the row shape from the archetype and assert it equals
34
- that data-type — so the two can't drift apart:
38
+ An archetype that mirrors a `data/` row type carries a **non-exported** compile-time
39
+ check — infer the row shape from the archetype and assert it equals the data-type, so
40
+ the two can't drift:
35
41
 
36
42
  ```ts
37
43
  import { Schema } from "@adobe/data/schema";
@@ -41,6 +47,3 @@ import type { PlacedMark } from "../../data/placed-mark/placed-mark.js";
41
47
  const _schema = Schema.fromArchetype(components, archetypes.PlacedMark);
42
48
  type _Check = Assert<Equal<Schema.ToType<typeof _schema>, PlacedMark>>;
43
49
  ```
44
-
45
- Iterating archetype **rows** at runtime is a separate concern — see the
46
- rules-root `archetypes.md`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/computed/**/*.ts'
3
+ - '**/features/*/services/main-service/computed-database/computed/**/*.ts'
4
4
  ---
5
5
 
6
6
  # database/computed/ — derived observable values
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/conformance/**/*.ts'
3
+ - '**/features/*/services/main-service/conformance/**/*.ts'
4
4
  ---
5
5
 
6
6
  # services/main-service/conformance/ — keeping the ECS honest against the spec
@@ -18,15 +18,12 @@ them against the ECS. This folder holds only the *feature-specific projection*
18
18
  `conformance/` (the zero-config example) and `data-lit-todo` (entity markers +
19
19
  `hydrate`); `data-lit-space-rock-game` for the entity-bag / real-time variant.
20
20
 
21
- **The projection helpers — `fromState`, `toState`, and the
22
- `toData(store, entity)` reader — are strictly for conformance tests and MUST
23
- NEVER run in production code, ever.** `fromState`/`toState` rewrite the whole
24
- store out-of-band; runtime code reads through observables/indexes and writes
25
- through transactions. (This conformance `toData(store, entity)` reader is
26
- unrelated to the library's `db.toData()` store-serialization method, which *is*
27
- a normal runtime API — the collision is only in the name.) Likewise
28
- `Match.matches` / `Match.assert` are comparison helpers for `*.test.ts` only —
29
- never a runtime branch.
21
+ **The projection helpers (`fromState`, `toState`, `toData(store, entity)`) and
22
+ `Match.matches` / `Match.assert` are test-only — never a production/runtime branch.**
23
+ `fromState`/`toState` rewrite the whole store out-of-band; runtime code reads through
24
+ observables/indexes and writes through transactions. (The conformance
25
+ `toData(store, entity)` reader is unrelated to the library's runtime `db.toData()`
26
+ store-serialization method — same name only.)
30
27
 
31
28
  ## What `@adobe/data-testing` gives you
32
29
 
@@ -0,0 +1,48 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/services/main-service/**/core-database/core-database.ts'
4
+ ---
5
+
6
+ # core-database/core-database.ts — the assembled schema + its namespace
7
+
8
+ A thin assembler: `Database.Plugin.create({ components?, resources?, archetypes? })`
9
+ over the facet files, exposed through the `CoreDatabase` namespace. This is the
10
+ single-file namespace form (one value export — `plugin` — everything else a type;
11
+ see `global/namespace.md`).
12
+
13
+ ```ts
14
+ import { Database, type Store } from "@adobe/data/ecs";
15
+ import { components } from "./components.js";
16
+ import { resources } from "./resources.js";
17
+ import { archetypes } from "./archetypes.js";
18
+
19
+ const coreDatabasePlugin = Database.Plugin.create({ components, resources, archetypes });
20
+
21
+ export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
22
+ // Resolved component map, at module scope so the imported `Store` namespace isn't
23
+ // shadowed by `CoreDatabase.Store` below.
24
+ type CoreComponents = Store.Components<Database.Plugin.ToStore<typeof coreDatabasePlugin>>;
25
+
26
+ export namespace CoreDatabase {
27
+ export const plugin = coreDatabasePlugin;
28
+ /** The store transactions operate on — a store *is* the transaction context. */
29
+ export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
30
+ /** Index-declaration type bound to this database's components. */
31
+ export type Index = Database.Index<CoreComponents>;
32
+ /** A component name, and a component-key tuple — an archetype's defining shape, or the key
33
+ * list of a `count` / `select` query. */
34
+ export type Component = Extract<keyof typeof components, string>;
35
+ export type Archetype = readonly Component[];
36
+ }
37
+ ```
38
+
39
+ ## Derive `Component` / `Archetype` from `components`, never the plugin
40
+
41
+ Read the keys straight from the leaf `components` module
42
+ (`Extract<keyof typeof components, string>`). Do **not** derive them from the
43
+ plugin (`Store.Components<ToStore<plugin>>`, i.e. `CoreComponents`): the plugin is
44
+ built *from* `archetypes`, so a plugin-derived `Archetype` is **circular** the
45
+ moment it is used to `satisfies`-check an archetype tuple — and the cycle
46
+ collapses the entire database type to `{}`. The leaf derivation is cycle-free and
47
+ usable both in `archetypes.ts` (to check shared bases) and downstream (to check
48
+ `count` / `select` query keys).
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/*.ts'
3
+ - '**/features/*/services/main-service/main-service.ts'
4
+ - '**/features/*/services/main-service/*-database/*-database.ts'
4
5
  ---
5
6
 
6
7
  # services/main-service/ — the feature service (ECS implementation)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/indexes/**/*.ts'
3
+ - '**/features/*/services/main-service/index-database/indexes/**/*.ts'
4
4
  ---
5
5
 
6
6
  # database/indexes/ — ECS indexes
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/services/**/*.ts'
3
+ - '**/features/*/services/main-service/service-database/services/**/*.ts'
4
4
  ---
5
5
 
6
6
  # database/services/ — database-bound service factories
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/systems/**/*.ts'
4
- - '**/systems/*.ts'
3
+ - '**/features/*/services/main-service/system-database/**/*.ts'
5
4
  ---
6
5
 
7
6
  # database/systems/ — the real-time tick loop
@@ -66,8 +65,12 @@ const plugin = Database.Plugin.create({
66
65
  HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
67
66
  (movement, aging) that run on every entity every frame and shouldn't pay
68
67
  per-entity transaction overhead.
69
- - **Optimize for execution speed in systems** if there are multiple items, then make sure we are using efficient store queries and reading/writing minimal data columns. If this is not hot path, then the simplified data functions can be used, but in hot paths we only consider data functions to be specification. (ie: only use them if it's clearly not a hot path or you cannot write anything more performant by directly using ecs store)
70
- - **The step math may live in `data/`.** but only for non-hot path
68
+ - **Optimize hot paths for speed.** With many entities, query with efficient
69
+ `queryArchetypes` and read/write minimal columns directly; avoid intermediate
70
+ allocations. On a hot path treat `data/` step functions as **spec only** — use
71
+ them when the path isn't hot or you can't beat them by writing the store
72
+ directly; off hot paths, reuse them freely. (Only transactions are observable —
73
+ direct store writes fire no observers.)
71
74
  - **Iterate archetypes per `archetypes.md`.** Express selection with
72
75
  `queryArchetypes(include, { exclude })`; when a system destroys/migrates rows
73
76
  (bullets expiring, entities dying) iterate **tail → head** so hole-fills don't
@@ -95,11 +98,8 @@ const plugin = Database.Plugin.create({
95
98
  and advanced this same tick), you can't advance *all* bodies in one system
96
99
  before the spawn — split that entity's advance out so it runs *after* the spawn,
97
100
  alongside the step that owns it.
98
- - Games or simulations with immediate mode rendering may want to store a session
99
- resource of the canvas and render to it within a system. @adobe/data-gpu provides
100
- example patterns for doing this.
101
- - All simulation systems should be optimized for performance.
102
- Avoid allocating intermediate objects if possible; read/write columns directly if hot-path. You can use transactions if needed, for instance if we are writing non hot path things which we have other things observing. (Only transactions are observable, not direct writes)
101
+ - Immediate-mode rendering: store the canvas in a session resource and render to
102
+ it inside a system (see @adobe/data-gpu for patterns).
103
103
 
104
104
  ## Driving the loop — the scheduler
105
105
 
@@ -136,58 +136,8 @@ an inline body grows too large — the schedule and registration still live inli
136
136
  entities collide, which pair resolves) separately, with seeded edge-case geometries
137
137
  — it's a system concern, not a transaction, and where subtle bugs hide.
138
138
 
139
- # Archetypes — iteration rules
139
+ ## Iterating archetype rows
140
140
 
141
- ## Express selection in the query, not the loop
142
-
143
- `queryArchetypes(include, { exclude })` accepts both required and excluded
144
- component lists. Use them. Don't query a wider set and then skip rows or
145
- archetypes with an `if`.
146
-
147
- ```ts
148
- // ❌ post-filter
149
- for (const arch of db.store.queryArchetypes(["position", "rotation"])) {
150
- if (arch.components.has("_worldMatrix")) continue;
151
- ...
152
- }
153
-
154
- // ✅ declarative
155
- for (const arch of db.store.queryArchetypes(
156
- ["position", "rotation"],
157
- { exclude: ["_worldMatrix"] },
158
- )) {
159
- ...
160
- }
161
- ```
162
-
163
- ## When every row migrates out, iterate tail → head
164
-
165
- Archetypes are densely packed. Removing or migrating a row that isn't the
166
- last one triggers a hole-fill: the tail row is moved into the gap.
167
- Iterating `0 → rowCount-1` while migrating every row pays this cost on
168
- every iteration. Iterating `rowCount-1 → 0` means each removal is from
169
- the tail — no shift, indices ahead of the cursor stay valid.
170
-
171
- ```ts
172
- // ❌ shifts the tail into every hole, and the snapshot allocation is
173
- // only there to survive the shifts.
174
- const ids = [...];
175
- for (let i = 0; i < arch.rowCount; i++) ids[i] = arch.columns.id.get(i);
176
- for (const id of ids) db.store.update(id, { _worldMatrix: Mat4x4.identity });
177
-
178
- // ✅ no shifts, no allocation
179
- for (let i = arch.rowCount - 1; i >= 0; i--) {
180
- db.store.update(arch.columns.id.get(i), { _worldMatrix: Mat4x4.identity });
181
- }
182
- ```
183
-
184
- If only *some* rows migrate (filter inside the loop), snapshot the ids
185
- you'll touch — forward iteration is fine because rows you don't touch
186
- stay put.
187
-
188
- ## Don't snapshot what the query already filters
189
-
190
- A snapshot of "all entity ids in this archetype right now" is only
191
- needed when forward iteration would invalidate. Reverse iteration
192
- removes the need; an `exclude` clause removes the need to look at rows
193
- that don't qualify in the first place.
141
+ A per-frame system reads/writes archetype columns directly. The row-iteration
142
+ rules — declarative `queryArchetypes` selection, tail → head migration, snapshot
143
+ avoidance — are general ECS concerns in the rules-root `archetypes.md`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/services/main-service/**/transactions/**/*.ts'
3
+ - '**/features/*/services/main-service/transaction-database/transactions/**/*.ts'
4
4
  ---
5
5
 
6
6
  # database/transactions/ — atomic mutations
@@ -148,19 +148,18 @@ export const add = ({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point): Point =>
148
148
  This pattern is ONLY allowed if there is only a single value export and every other export is just a type.
149
149
  As soon as there is more than one value type it MUST be promoted to the full namespace pattern.
150
150
 
151
- Example:
151
+ Example — a type merged with one value export and any number of type-only members:
152
152
 
153
- // core-database.ts
153
+ // window/window.ts
154
+ const value = createWindow();
154
155
 
155
- const coreDatabasePlugin = Database.Plugin.create({
156
- components,
157
- resources,
158
- archetypes,
159
- });
156
+ export type Window = typeof value;
160
157
 
161
- export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
162
-
163
- export namespace CoreDatabase {
164
- export const plugin = coreDatabasePlugin;
165
- export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
158
+ export namespace Window {
159
+ export const instance = value; // the sole value export
160
+ export type Size = { width: number; height: number };
166
161
  }
162
+
163
+ The canonical use is a feature's `core-database.ts` / layered `*-database.ts` (a
164
+ `plugin` value plus `Store` / `Index` / `Component` / `Archetype` types) — see
165
+ `features/services/main-service/core-database.md`.
@@ -0,0 +1,143 @@
1
+ ---
2
+ paths:
3
+ - '**/versioning/**/*.ts'
4
+ - '**/versions.ts'
5
+ - '**/versions.test.ts'
6
+ ---
7
+
8
+ # Database schema versioning & upgrade-on-load
9
+
10
+ A versioned `@adobe/data` database keeps an ordered **version history** so an old
11
+ persisted document can be upgraded to the current schema on load. You (human or
12
+ agent) should almost never have to reason about this by hand: **change the schema,
13
+ run the tests, and the failing test prints the exact fix to apply.**
14
+
15
+ ## The pieces (all from `@adobe/data/ecs`)
16
+
17
+ - **`versions.ts`** — `export const versions: readonly VersionEntry[]`. `entries[i]`
18
+ IS version `i`; `entries[0]` is a frozen copy of the initial schema. `currentVersion
19
+ = versions.length - 1`.
20
+ - **`VersionEntry`** — `{ version, changes: { components?, resources? }, handler? }`.
21
+ `version` MUST equal the array index. `changes` is a **JSON Merge Patch** applied to
22
+ the folded-so-far schema — record ONLY what changes, not a restated copy:
23
+ - `name: { …fields… }` adds a new component/resource, or deep-merges those fields
24
+ onto the existing schema;
25
+ - a key set to **`undefined`** DELETES it — `name: undefined` drops the whole
26
+ component/resource; a nested `precision: undefined` drops just that field;
27
+ - `null` is a normal VALUE (so `default: null` sets the default), not a delete;
28
+ arrays/scalars replace wholesale.
29
+ Because it MERGES, a shape change must delete the fields that no longer apply —
30
+ number → object is `{ type: "object", properties: {…}, precision: undefined, default: undefined }`,
31
+ never a bare `{ type: "object", … }` (which would keep the old `precision`/`default`).
32
+ Never write empty `changes: {}` — an entry must record at least one change. Components
33
+ and resources are separate namespaces. `handler` is present ONLY for a change that is
34
+ not automatically convertible.
35
+ - The version is **NOT an ECS resource** — it rides the save-format metadata. The db
36
+ exposes it as `db.version` (`= currentVersion`, or `0` when unversioned), stamps it into
37
+ every `toData` per persisted quadrant (`schemaVersions: { document, settings }`), and
38
+ reads it back on load. Do not declare a `documentVersion`/`version` component or resource.
39
+ - **`createVersionUpgrader(versions)`** passed as `Database.create(plugin, { versioning })`.
40
+ No config — the per-quadrant saved versions come from the blob metadata. Each `handler`
41
+ touches exactly ONE persisted quadrant (`document` or `settings`); the upgrader replays
42
+ each quadrant from its own saved version up to current (see rule 7).
43
+ - ONE co-located guard test in `versions.test.ts` calling `assertVersioning(...)` (see below).
44
+
45
+ ## Rules — do not violate
46
+
47
+ 1. **Existing entries are FROZEN.** Never edit, reorder, or delete an entry — it is
48
+ historical fact. Only ever APPEND a new entry.
49
+ 2. **Version 0 is a deep copy, never a reference.** It must not change when the live
50
+ schema changes; that is the whole point of a history.
51
+ 3. **Additive & minor changes need NO handler** — the load path auto-converts them
52
+ (add-field-with-default, widen/narrow, reorder, clamp, add component/resource).
53
+ Just record the `changes` patch.
54
+ 4. **Breaking changes REQUIRE a handler** — a type change, a rename, splitting/moving
55
+ a field. The handler runs against the store staged to the previous version and
56
+ mutates it in place to the new shape. For an ISOLATED single-component change,
57
+ `Store.remapComponent(store, name, newSchema, old => newValue)` does it in one call —
58
+ but it is NOT required and NOT always applicable. A migration that reads from or
59
+ writes to SEVERAL components (a cross-component move, deriving a new component from
60
+ others, splitting one into many) must be hand-written to your upgrade algorithm:
61
+ query the input components, compute, and write the outputs directly on the store.
62
+ Add a test case either way (rule 6).
63
+ 5. **Removing a component drops its data.** If the data still matters, migrate it in a
64
+ handler BEFORE the removal entry.
65
+ 6. **Versioned schemas must be REAL** — declare a `type` (or `enum`/`const`), and the
66
+ `default` must be fully described by that schema (no extra keys/structure). Only
67
+ SESSION values (`nonPersistent` AND `nonShared`, e.g. a GPU buffer) may be untyped,
68
+ and those are never versioned — leave them out of the history entirely.
69
+ 7. **A handler touches ONE quadrant.** Persisted quadrants (shared+persistent = the
70
+ cloud document; nonShared+persistent = local settings) can be saved separately and
71
+ drift to DIFFERENT versions. On load they are merged into one store and the upgrader
72
+ replays EACH quadrant from its own stamp — so a handler that changes schemas in more
73
+ than one quadrant fails the guard (when one quadrant is behind, staging its handler
74
+ must not touch the other). Split it into one version per quadrant — the upgrader reads
75
+ each quadrant's saved version from the blob metadata automatically.
76
+
77
+ ## When a schema change makes a test fail — the fix is in the error
78
+
79
+ `assertVersionsMatchSchema` fails whenever the live schema no longer folds from the
80
+ history. Its message is a literal recipe. To fix it:
81
+
82
+ 1. **Append one new entry** to the `versions` array with `version:` = the next index
83
+ and the `changes` block the error printed verbatim. `db.version` follows the history
84
+ automatically (`= versions.length - 1`) — there is no resource default to update.
85
+ 2. If the error marks a change **BREAKING — needs a handler**, add a `handler` to that
86
+ entry AND add a matching test case under `testUpgradeHandlers` (rule 6). The error
87
+ may point at `Store.remapComponent(...)` for an isolated single-component change, but
88
+ that is only a suggestion — write whatever upgrade code the change actually needs.
89
+
90
+ Do exactly what the message says; do not touch existing entries.
91
+
92
+ ## The one guard test
93
+
94
+ A single `assertVersioning(...)` runs BOTH checks — the history folds to the current
95
+ schema (schema coverage) AND every entry with a `handler` has a passing test case
96
+ (handler coverage + behavior). Keep this one test.
97
+
98
+ ```ts
99
+ import { Database, assertVersioning, createVersionUpgrader } from "@adobe/data/ecs";
100
+ import { MainService } from "../main-service.js";
101
+ import { versions } from "./versions.js";
102
+
103
+ describe("database schema versions", () => {
104
+ it("the version history is consistent (schema folds + every handler tested)", () =>
105
+ assertVersioning({
106
+ // the version-configured db, so db.version = the history's current version
107
+ database: Database.create(MainService.plugin, { versioning: createVersionUpgrader(versions) }),
108
+ entries: versions,
109
+ handlers: {
110
+ // Append a case per version that adds a `handler` (rule 6):
111
+ // <version>: { setup: (store) => <populate the version-(v-1) store, return entities>,
112
+ // expect: (store, setup) => <assert the version-v result> },
113
+ },
114
+ }));
115
+ });
116
+ ```
117
+
118
+ (`assertVersionsMatchSchema` and `testUpgradeHandlers` remain exported for bespoke use.)
119
+ ```
120
+
121
+ ## Example: a breaking change (number → object) at version 1
122
+
123
+ ```ts
124
+ // versions.ts — append (do NOT edit version 0)
125
+ import { Store } from "@adobe/data/ecs";
126
+ const HealthObject = { type: "object", properties: { current: { type: "number", precision: 1, default: 0 }, max: { type: "number", precision: 1, default: 0 } } };
127
+ export const versions = [
128
+ { version: 0, changes: { components: { /* …frozen… */ hp: { type: "number", precision: 1, default: 0 } } } },
129
+ { version: 1,
130
+ // MERGE patch: add the object shape, DELETE the number-only fields it drops.
131
+ changes: { components: { hp: { type: "object", properties: HealthObject.properties, precision: undefined, default: undefined } } },
132
+ handler: (store) => Store.remapComponent(store, "hp", HealthObject, (old: number) => ({ current: old, max: old })) },
133
+ ];
134
+ // `db.version` becomes 1 automatically (= versions.length - 1) — nothing else to set.
135
+
136
+ // versions.test.ts — add the required case to the guard's `handlers`
137
+ assertVersioning({ database: db, entries: versions, handlers: {
138
+ 1: {
139
+ setup: (store) => store.ensureArchetype(["hp"]).insert({ hp: 42 }),
140
+ expect: (store, e) => expect(store.read(e)?.hp).toEqual({ current: 42, max: 42 }),
141
+ },
142
+ } });
143
+ ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adobe-data-ai",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
5
5
  "author": {
6
6
  "name": "Adobe"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/data-ai",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,12 +29,12 @@
29
29
  "skills"
30
30
  ],
31
31
  "peerDependencies": {
32
- "@adobe/data": "0.10.1"
32
+ "@adobe/data": "0.10.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^5.8.3",
36
36
  "vitest": "^1.6.0",
37
- "@adobe/data": "0.10.1"
37
+ "@adobe/data": "0.10.2"
38
38
  },
39
39
  "license": "MIT",
40
40
  "scripts": {