@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.
- package/.claude/rules/README.md +2 -1
- package/.claude/rules/archetypes.md +59 -0
- package/.claude/rules/features/services/index.md +2 -1
- package/.claude/rules/features/services/main-service/actions.md +1 -1
- package/.claude/rules/features/services/main-service/archetypes.md +19 -16
- package/.claude/rules/features/services/main-service/computed.md +1 -1
- package/.claude/rules/features/services/main-service/conformance.md +7 -10
- package/.claude/rules/features/services/main-service/core-database.md +48 -0
- package/.claude/rules/features/services/main-service/index.md +2 -1
- package/.claude/rules/features/services/main-service/indexes.md +1 -1
- package/.claude/rules/features/services/main-service/services.md +1 -1
- package/.claude/rules/features/services/main-service/systems.md +13 -63
- package/.claude/rules/features/services/main-service/transactions.md +1 -1
- package/.claude/rules/global/namespace.md +11 -12
- package/.claude/rules/versioning.md +143 -0
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
package/.claude/rules/README.md
CHANGED
|
@@ -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`,
|
|
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.
|
|
@@ -5,10 +5,11 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# core-database/archetypes.ts — the feature's archetypes
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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`
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/features/*/services/main-service
|
|
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
|
|
70
|
-
|
|
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
|
-
-
|
|
99
|
-
|
|
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
|
-
|
|
139
|
+
## Iterating archetype rows
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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`.
|
|
@@ -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
|
-
//
|
|
153
|
+
// window/window.ts
|
|
154
|
+
const value = createWindow();
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
components,
|
|
157
|
-
resources,
|
|
158
|
-
archetypes,
|
|
159
|
-
});
|
|
156
|
+
export type Window = typeof value;
|
|
160
157
|
|
|
161
|
-
export
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.10.
|
|
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.
|
|
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.
|
|
37
|
+
"@adobe/data": "0.10.2"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|