@adobe/data-ai 0.9.99 → 0.9.100
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.
|
@@ -41,21 +41,32 @@ function record(m: unknown) {
|
|
|
41
41
|
A collection's type states whether its order is meaningful — the model is the
|
|
42
42
|
single source of truth, not a downstream comparison flag:
|
|
43
43
|
|
|
44
|
-
- **`ReadonlyArray<T>`** — order is meaningful. A display list
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
- **`ReadonlyArray<T>`** — order is meaningful. A display list of scalar values, a
|
|
45
|
+
positional tuple (`Vec2 = readonly [number, number]`), or an **ordered query
|
|
46
|
+
result of entity ids** (`ReadonlyArray<number>`, sorted by an `order` component —
|
|
47
|
+
see below). Not for the entity *store* itself.
|
|
48
|
+
- **`ReadonlySet<T>`** — an unordered bag / membership set. A set of entity
|
|
49
|
+
references is `ReadonlySet<number>` (a set of ids), and an unordered entity
|
|
50
|
+
*query result* is `ReadonlySet<number>`.
|
|
51
|
+
- **`ReadonlyMap<K, V>`** — a keyed lookup. This is how **identity-keyed entities**
|
|
52
|
+
are modelled: `entities: ReadonlyMap<number, EntityValue>` — the `number` id is the
|
|
53
|
+
key, and the value carries **no `id` of its own** (identity is the key, never a
|
|
54
|
+
field). Also used for deterministic-key lookups (an enum, a name, a stable string).
|
|
55
|
+
|
|
56
|
+
**Entities are keyed, never id-bearing values.** An entity's identity is its map
|
|
57
|
+
key, so entity value types (`Todo`, `Bullet`) have no `id` field, and there is no
|
|
58
|
+
`ReadonlySet<T>`/`ReadonlyArray<T>` *of entity values* — the single
|
|
59
|
+
`ReadonlyMap<number, …>` store is the only home for entities, and queries return
|
|
60
|
+
their **ids** (`ReadonlySet<number>` unordered, `ReadonlyArray<number>` ordered).
|
|
61
|
+
See `features/data/state.md` for the full `State` shape.
|
|
52
62
|
|
|
53
63
|
These are first-class `Data` (see `features/data/index.md`) — serialize a
|
|
54
64
|
Set/Map-bearing value with `Data.stringify` / `Data.parse` (plain `JSON.stringify`
|
|
55
65
|
cannot represent them), and `equals` compares them faithfully. Conformance mirrors
|
|
56
66
|
the semantics: `ReadonlyArray` compares positionally, `ReadonlySet` / `ReadonlyMap`
|
|
57
|
-
order-independently
|
|
58
|
-
is
|
|
67
|
+
order-independently. Entity identity is the map key (resolved to the allocated ECS
|
|
68
|
+
entity during conformance); the id is never a value field, so there is nothing to
|
|
69
|
+
"ignore" when comparing entity content.
|
|
59
70
|
|
|
60
71
|
## Shape of keyed collections
|
|
61
72
|
|
|
@@ -15,18 +15,89 @@ State Specification is authoritative and the ECS conforms to it. A feature witho
|
|
|
15
15
|
this rule does not apply — see `../index.md`, Two modes.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
//
|
|
19
|
-
export type
|
|
18
|
+
// data/todo/todo.ts — an entity value type: plain readonly data, NO id.
|
|
19
|
+
export type Todo = { readonly name: string; readonly complete: boolean; readonly order: number };
|
|
20
|
+
export * as Todo from "./public.js";
|
|
21
|
+
|
|
22
|
+
// data/todo/is.ts — a structural type guard, re-exported so it reads as `Todo.is`.
|
|
23
|
+
// `"k" in v` narrows `v` so each field reads without a cast.
|
|
24
|
+
export const is = (v: unknown): v is Todo =>
|
|
25
|
+
typeof v === "object" && v !== null &&
|
|
26
|
+
"name" in v && typeof v.name === "string" &&
|
|
27
|
+
"complete" in v && typeof v.complete === "boolean" &&
|
|
28
|
+
"order" in v && typeof v.order === "number";
|
|
29
|
+
|
|
30
|
+
// data/state/state.ts — singletons + one identity-keyed entity map.
|
|
31
|
+
export type State = {
|
|
32
|
+
readonly displayCompleted: boolean; // a SINGLETON → an ECS resource of the same name
|
|
33
|
+
readonly entities: ReadonlyMap<number, Todo>; // ALL entities, keyed by id; the value has no id
|
|
34
|
+
};
|
|
20
35
|
export * as State from "./public.js";
|
|
21
36
|
```
|
|
22
37
|
|
|
23
|
-
Every **state-based** feature owns a `State
|
|
24
|
-
|
|
38
|
+
Every **state-based** feature owns a `State`. An ECS-based feature has none.
|
|
39
|
+
|
|
40
|
+
## The standard `State` shape
|
|
41
|
+
|
|
42
|
+
A `State` has two kinds of field:
|
|
43
|
+
|
|
44
|
+
- **Singletons** — every non-`entities` field (`displayCompleted`, a `score`, a
|
|
45
|
+
`board`). Each becomes an ECS **resource** of the same name (see
|
|
46
|
+
`../services/main-service/resources.md`).
|
|
47
|
+
- **`entities`** — a single `ReadonlyMap<number, EntityValue>` holding *every* entity,
|
|
48
|
+
keyed by a numeric id, where `EntityValue` is the union of the feature's entity value
|
|
49
|
+
types (`Todo`, or `Bullet | Asteroid`). Omit `entities` entirely for a feature with
|
|
50
|
+
no entities (`tictactoe`, `dashboard`) — it is then all singletons.
|
|
51
|
+
|
|
52
|
+
This maps 1:1 to the ECS with **zero dependency on it** — the key is a plain `number`,
|
|
53
|
+
never the ECS `Entity`, so `data/` provably cannot reach ECS machinery. Singletons ↔
|
|
54
|
+
resources; each `entities` value ↔ one entity whose **component set is the value's own
|
|
55
|
+
keys**, so `fromState` inserts a value into the archetype named by `Object.keys(value)`
|
|
56
|
+
and `toState` reads every entity back into the map. Identity lives in the key, never a
|
|
57
|
+
field — see `../../data-modelling.md` (Entities are keyed, never id-bearing values).
|
|
58
|
+
|
|
59
|
+
### Entity value types — structural, id-less, one `is` guard each
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
Each entity type is its own `data/<type>/` namespace (`data/index.md`): a plain
|
|
62
|
+
readonly type with **no `id`** plus a structural `is` guard (`is.ts`, re-exported so it
|
|
63
|
+
reads `Todo.is`). A **sub-archetype** is an intersection — `Bar = Foo & { baz }` — and
|
|
64
|
+
its guard **composes the base guard**: `Bar.is` calls `Foo.is(v)` first, then checks the
|
|
65
|
+
added fields (`"baz" in v && typeof v.baz === "boolean"`). So `Bar.is ⟹ Foo.is` by
|
|
66
|
+
construction.
|
|
67
|
+
|
|
68
|
+
**Model structurally, like the ECS — no tags by default.** ECS systems match purely on
|
|
69
|
+
structure (a mover runs on anything with `position` + `velocity`, regardless of
|
|
70
|
+
"kind"), and `State` mirrors that:
|
|
71
|
+
|
|
72
|
+
- **A property name means one thing feature-wide.** No two entity types may declare the
|
|
73
|
+
same property name with a different type/semantics — that is what lets every property
|
|
74
|
+
collapse to exactly one ECS component and keeps structural matching sound.
|
|
75
|
+
- **No marker / `kind` / tag field unless a genuine modelling need demands it.** Prefer
|
|
76
|
+
discriminating on the presence of real components; add a tag only when structure alone
|
|
77
|
+
cannot express the distinction.
|
|
78
|
+
|
|
79
|
+
### Querying entities — `State.getXEntities`, returning ids
|
|
80
|
+
|
|
81
|
+
Entity queries are `state/` derivations that return entity **ids** (look the value up
|
|
82
|
+
with `state.entities.get(id)`), mirroring the ECS `select`. Perf is irrelevant here
|
|
83
|
+
(full-map scan + guard); the ECS realises them as archetype queries.
|
|
84
|
+
|
|
85
|
+
- **Unordered** → `ReadonlySet<number>`: keys whose value passes the guard. It is a
|
|
86
|
+
**superset** match — `getFooEntities` (via `Foo.is`) also includes `Bar` entities,
|
|
87
|
+
just as `queryArchetypes(["…Foo's cols"])` returns the `Foo` *and* `Bar` archetypes.
|
|
88
|
+
- **Ordered** → `ReadonlyArray<number>`: the matching ids sorted by an explicit `order`
|
|
89
|
+
component, exactly like an ECS `select(cols, { order })`. Order is **never** carried
|
|
90
|
+
by the `entities` map (it is identity-keyed / unordered); a meaningful order is a
|
|
91
|
+
component on the value and the derivation returns the sorted ids.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// data/state/get-todos.ts — an ordered membership derivation returning ids
|
|
95
|
+
export const getTodos = (state: State): readonly number[] =>
|
|
96
|
+
[...state.entities]
|
|
97
|
+
.filter(([, v]) => Todo.is(v))
|
|
98
|
+
.sort(([, a], [, b]) => a.order - b.order)
|
|
99
|
+
.map(([id]) => id);
|
|
100
|
+
```
|
|
30
101
|
|
|
31
102
|
**`State` has a standard shape.** Two exports are conventional and drive
|
|
32
103
|
conformance:
|
|
@@ -106,21 +177,28 @@ export const entity = ConformanceApi.entity;
|
|
|
106
177
|
```ts
|
|
107
178
|
// create-todo.ts
|
|
108
179
|
import { Match } from "@adobe/data-testing";
|
|
109
|
-
import type { Conformance } from "./conformance-case.js";
|
|
110
|
-
import type { Services } from "../../services/services.js";
|
|
180
|
+
import type { Conformance } from "./conformance-case.js"; // the thin per-feature alias above
|
|
181
|
+
import type { Services } from "../../services/services.js"; // the feature's service map
|
|
111
182
|
export const createTodo = (
|
|
112
|
-
state: Pick<State, "
|
|
113
|
-
{ name,
|
|
114
|
-
): Pick<State, "
|
|
183
|
+
state: Pick<State, "entities">,
|
|
184
|
+
{ name, analytics }: { name: string } & Pick<Services, "analytics">,
|
|
185
|
+
): Pick<State, "entities"> => {
|
|
115
186
|
analytics.todoCreated({ name });
|
|
116
|
-
|
|
187
|
+
// The spec mints the id (the map key) and the order; the value carries neither an id
|
|
188
|
+
// nor anything the ECS allocates — identity is the key.
|
|
189
|
+
const id = Math.max(0, ...state.entities.keys()) + 1;
|
|
190
|
+
const order = state.entities.size;
|
|
191
|
+
return { entities: new Map(state.entities).set(id, { name, complete: false, order }) };
|
|
117
192
|
};
|
|
118
193
|
|
|
119
194
|
export const cases: Conformance<typeof createTodo> = [
|
|
120
|
-
{ name: "
|
|
121
|
-
before: {}, // empty delta — the default State.create()
|
|
195
|
+
{ name: "adds the first todo",
|
|
196
|
+
before: {}, // empty delta — the default State.create() (empty entities)
|
|
122
197
|
args: { name: "a", analytics: AnalyticsService.createFake() },
|
|
123
|
-
|
|
198
|
+
// The map key is an id the ECS mints — use `Match.ref("label")` (a DISTINCT
|
|
199
|
+
// label per entity). The value is id-less so content compares directly. (Maps
|
|
200
|
+
// compare entry-wise / order-independently — see conformance.md.)
|
|
201
|
+
after: { entities: new Map([[Match.ref("a"), { name: "a", complete: false, order: 0 }]]) },
|
|
124
202
|
effects: { analytics: [["todoCreated", { name: "a" }]] } },
|
|
125
203
|
];
|
|
126
204
|
```
|
|
@@ -155,12 +233,17 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
155
233
|
overrides the default wholesale.)
|
|
156
234
|
- **`after` leaves minted values open** with the shared matchers `Match.anyNumber`
|
|
157
235
|
/ `Match.anyString`, imported from `@adobe/data-testing` — there is **no**
|
|
158
|
-
per-feature `matchers.ts` anymore. An entity's
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
per-feature `matchers.ts` anymore. An entity's identity is the `entities` map **key**,
|
|
237
|
+
not a value field, so entity content compares directly with nothing to ignore. The
|
|
238
|
+
key is an ECS-minted id, so use **`Match.ref("label")` with a DISTINCT label per map
|
|
239
|
+
entry** (`[Match.ref("a"), value]`) — `ref` returns a fresh object so distinct labels
|
|
240
|
+
are distinct keys, and its injective binding both keeps entities distinct and lets an
|
|
241
|
+
entity **correlate** with a reference elsewhere in the case (reuse the label, e.g. a
|
|
242
|
+
`selectedId: Match.ref("a")` singleton). Do **not** use `Match.anyNumber` as a map key
|
|
243
|
+
— it is a shared singleton, so two entries keyed by it collapse to one. `entity(specId)`
|
|
244
|
+
is for **`args`** (it resolves via the seed map), **not** `after` keys — a case's `after`
|
|
245
|
+
entities may be freshly created, with no seed mapping. Match by content, not by a value
|
|
246
|
+
you don't control. `Match` is framework-agnostic and
|
|
164
247
|
honors any asymmetric matcher, so vitest's `expect.stringContaining(...)` interops
|
|
165
248
|
on the expected side too. When an id must **line up in two places** within one
|
|
166
249
|
comparison — a `selectedId` that points at a specific todo, say — use
|
|
@@ -168,12 +251,32 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
168
251
|
not a pinned value, so the two occurrences of the label must resolve to the same
|
|
169
252
|
actual id and two labels can't collide. `anyNumber`/`anyString` are for an id a
|
|
170
253
|
case does not pin at all; `ref` for one that must be consistent across the case.
|
|
171
|
-
- **Entity-addressed cases use `entity(specId)`.** A transition that
|
|
172
|
-
entity by id writes it as `args: { id: entity(2) }` — `entity` imported
|
|
173
|
-
feature's `conformance-case.ts` (re-exported from `@adobe/data-testing`). It
|
|
174
|
-
as the id it stands for
|
|
175
|
-
|
|
176
|
-
|
|
254
|
+
- **Entity-addressed cases use `entity(specId)` in `args`.** A transition that
|
|
255
|
+
addresses an entity by id writes it as `args: { id: entity(2) }` — `entity` imported
|
|
256
|
+
from the feature's `conformance-case.ts` (re-exported from `@adobe/data-testing`). It
|
|
257
|
+
types as the id it stands for, so it slots into the transform's own arg type. `runSpec`
|
|
258
|
+
unwraps it to the plain data-id for the pure side; the ECS runners resolve it (via the
|
|
259
|
+
`fromState` seed map) to the seeded entity (see `conformance.md`).
|
|
260
|
+
- **The `entities` map key convention across a case, in one place:**
|
|
261
|
+
- **`before`** (the seed): **plain spec-id numbers** (`new Map([[1, …], [2, …]])`).
|
|
262
|
+
`fromState` seeds from these and returns the `spec-id → entity` map, so `args:
|
|
263
|
+
{ id: entity(1) }` resolves to the entity seeded for `1`.
|
|
264
|
+
- **`after`** (the expectation, compared against the ECS by content): **`Match.ref`
|
|
265
|
+
with a distinct label per entry** (`[[Match.ref("a"), …], [Match.ref("b"), …]]`) —
|
|
266
|
+
the ECS mints its own ids, so keys must be open; `ref` is a fresh object (distinct
|
|
267
|
+
keys don't collapse) and injective (entities stay distinct, and a label reused in a
|
|
268
|
+
singleton reference correlates). Never `entity(specId)` here — a case's `after` may
|
|
269
|
+
hold freshly created entities with no seed mapping.
|
|
270
|
+
- **`samples`** (round-tripped `toState ∘ fromState`): same as `after` — open,
|
|
271
|
+
distinct keys against ECS-minted ids.
|
|
272
|
+
- **Building the open-keyed map — `Match.refMap(values)`.** When the entries come
|
|
273
|
+
from a list rather than hand-authored labels (a `samples` entry, a case `after`
|
|
274
|
+
whose entities you don't cross-reference), don't hand-roll the `Match.ref` keys:
|
|
275
|
+
`Match.refMap(iterableOfValues)` returns a `ReadonlyMap<number, V>` keyed by
|
|
276
|
+
process-unique open `ref`s — the standard way to build an identity-keyed collection
|
|
277
|
+
for comparison. Reach for a **hand-written `Match.ref(label)`** only when a key must
|
|
278
|
+
**correspond** to a reference elsewhere in the same case (a `selectedId`), where the
|
|
279
|
+
shared label is the whole point.
|
|
177
280
|
- No per-transform test. The single **`spec.test.ts`** is one call —
|
|
178
281
|
`Conformance.runSpec({ state: State, transitions })` importing `transitions`
|
|
179
282
|
from the test-only `./transitions.js` (above) — that auto-discovers every module
|
|
@@ -220,8 +323,10 @@ state change and the service calls.
|
|
|
220
323
|
## Derivations — `(state) => value`, cases `{ input, value }`
|
|
221
324
|
|
|
222
325
|
Pure selectors that **compose the aggregate** — a value drawn from **two or more
|
|
223
|
-
`State` fields** (`visibleTodos` from `
|
|
224
|
-
`currentPlayer` from `board` + `firstPlayer`).
|
|
326
|
+
`State` fields** (`visibleTodos` from `entities` + `displayCompleted`;
|
|
327
|
+
`currentPlayer` from `board` + `firstPlayer`). An entity query like `visibleTodos`
|
|
328
|
+
returns entity **ids** (`ReadonlyArray<number>` / `ReadonlySet<number>`), never the
|
|
329
|
+
values (look those up in `entities`). A value computed from a **single**
|
|
225
330
|
`State` field is that field's own type math and lives on its `data/<type>`
|
|
226
331
|
namespace (`winner`/`status` from `board` → `data/board-state`), tested there —
|
|
227
332
|
**not** in `state/`. A feature may therefore have zero `state/` derivations.
|
|
@@ -37,14 +37,18 @@ installing `@adobe/data` never pulls in a `vitest` peer dependency):
|
|
|
37
37
|
|
|
38
38
|
- **`Match`** — the tolerant, matcher-aware value comparison: `matches(actual,
|
|
39
39
|
expected, options?)` and its throwing wrapper `assert(...)`, plus the matchers
|
|
40
|
-
`Match.anyNumber` / `Match.anyString` / `Match.ref(label)
|
|
40
|
+
`Match.anyNumber` / `Match.anyString` / `Match.ref(label)`, and the collection
|
|
41
|
+
builder `Match.refMap(values)` (an identity-keyed `ReadonlyMap<number, V>` of
|
|
42
|
+
id-less values with distinct open keys — the standard way to author a `samples`
|
|
43
|
+
entry or a created-entity `after`; see `../../../data/state.md`). Options are
|
|
41
44
|
`{ tolerance?: number }` — numbers snap to `tolerance` (default `0.01`) to absorb
|
|
42
45
|
F32↔f64 / trig noise. **Ordering is carried by the value's type**: a
|
|
43
46
|
`ReadonlyArray` compares **in order**, a `ReadonlySet` / `ReadonlyMap`
|
|
44
|
-
**order-independently** — there is no `unordered` option.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
**order-independently** — there is no `unordered` option. Entity identity is the
|
|
48
|
+
key of `State.entities` (a `ReadonlyMap<number, …>`), resolved to the allocated ECS
|
|
49
|
+
entity via `resolver` (below); entity *values* carry no `id`, so there is no id to
|
|
50
|
+
ignore when comparing content. Framework-agnostic: it honors any asymmetric matcher,
|
|
51
|
+
so vitest's `expect.any(...)` interops.
|
|
48
52
|
- **`Conformance`** — the case types (`Case`, `Cases`, `DerivationCase`,
|
|
49
53
|
`DerivationCases`, `Effects`, `ServiceCall`), the `entity(specId)` identity
|
|
50
54
|
marker, the id `resolver(map)`, the whole-feature driver **`runFeature`**, the
|
|
@@ -144,10 +148,10 @@ Conformance.runFeature({
|
|
|
144
148
|
`data-lit-tictactoe` is the zero-config call (no `computedPlugin`, no `hydrate`,
|
|
145
149
|
no `match`, no `ops` — moves are board-index addressed, so no `entity()`
|
|
146
150
|
markers). `data-lit-todo` adds `hydrate: ["visibleTodos"]` and `entity()` markers.
|
|
147
|
-
`data-lit-space-rock-game`
|
|
148
|
-
`
|
|
149
|
-
option (its per-frame transitions are
|
|
150
|
-
`systems.md`).
|
|
151
|
+
`data-lit-space-rock-game` holds its `bullet` / `asteroid` entities in the single
|
|
152
|
+
`entities: ReadonlyMap<number, Bullet | Asteroid>`, keyed by id, so they compare
|
|
153
|
+
order-independently by the map — no `match` option (its per-frame transitions are
|
|
154
|
+
conformed by the systems tick loop, not here — see `systems.md`).
|
|
151
155
|
|
|
152
156
|
## The pure spec — `data/state/spec.test.ts`
|
|
153
157
|
|
|
@@ -205,9 +209,10 @@ positional, `ReadonlySet` / `ReadonlyMap` order-independent (the rule and its
|
|
|
205
209
|
rationale live in `../../../data-modelling.md`). What's specific to writing
|
|
206
210
|
conformance cases:
|
|
207
211
|
|
|
208
|
-
- **
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
- **Entity identity is the `State.entities` key, not a value field.** Entity values
|
|
213
|
+
carry no `id`, so there is nothing to omit or ignore — content compares directly.
|
|
214
|
+
The map key is a spec-domain id the runner resolves to the allocated ECS entity via
|
|
215
|
+
`resolver`; assert a *reference* between entities with `Match.ref` (below).
|
|
211
216
|
- **Float noise** is absorbed by the default `tolerance` (`0.01`), threaded through
|
|
212
217
|
`match?: { tolerance }`; raise it only when a case needs a looser grid.
|
|
213
218
|
- **`Match.ref(label)`** on the expected side asserts id *correspondence* for a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.100",
|
|
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.9.
|
|
32
|
+
"@adobe/data": "0.9.100"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
36
|
"vitest": "^1.6.0",
|
|
37
|
-
"@adobe/data": "0.9.
|
|
37
|
+
"@adobe/data": "0.9.100"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|