@adobe/data-ai 0.9.95 → 0.9.97
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/data-modelling.md +21 -0
- package/.claude/rules/features/data/index.md +4 -3
- package/.claude/rules/features/data/state.md +46 -26
- package/.claude/rules/features/index.md +28 -0
- package/.claude/rules/features/services/index.md +27 -0
- package/.claude/rules/features/services/main-service/actions.md +4 -0
- package/.claude/rules/features/services/main-service/computed.md +1 -1
- package/.claude/rules/features/services/main-service/conformance.md +39 -29
- package/.claude/rules/features/services/main-service/transactions.md +5 -0
- package/.claude/rules/service.md +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
|
@@ -36,6 +36,27 @@ function record(m: unknown) {
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Collection ordering is carried by the type
|
|
40
|
+
|
|
41
|
+
A collection's type states whether its order is meaningful — the model is the
|
|
42
|
+
single source of truth, not a downstream comparison flag:
|
|
43
|
+
|
|
44
|
+
- **`ReadonlyArray<T>`** — order is meaningful. A display list rendered in sequence,
|
|
45
|
+
a drag-reorderable list, a positional tuple (`Vec2 = readonly [number, number]`).
|
|
46
|
+
- **`ReadonlySet<T>`** — an unordered bag. Entities materialised in nondeterministic
|
|
47
|
+
order, a membership set. Use this for **identity-keyed** collections: the element
|
|
48
|
+
carries its own `id`, so a `ReadonlySet<Entity>` replaces any
|
|
49
|
+
`ReadonlyMap<id, Entity>`.
|
|
50
|
+
- **`ReadonlyMap<K, V>`** — a keyed lookup whose **keys are meaningful/deterministic**
|
|
51
|
+
(an enum, a name, a stable string). Not for identity keys (those are Sets).
|
|
52
|
+
|
|
53
|
+
These are first-class `Data` (see `features/data/index.md`) — serialize a
|
|
54
|
+
Set/Map-bearing value with `Data.stringify` / `Data.parse` (plain `JSON.stringify`
|
|
55
|
+
cannot represent them), and `equals` compares them faithfully. Conformance mirrors
|
|
56
|
+
the semantics: `ReadonlyArray` compares positionally, `ReadonlySet` / `ReadonlyMap`
|
|
57
|
+
order-independently, and a numeric `id` is ignored (the ECS allocates it) — so there
|
|
58
|
+
is no separate "unordered" declaration when writing conformance cases.
|
|
59
|
+
|
|
39
60
|
## Shape of keyed collections
|
|
40
61
|
|
|
41
62
|
- `Record<EnumKey, T>` — every key required at all times. Default lists
|
|
@@ -6,9 +6,10 @@ paths:
|
|
|
6
6
|
# data/ — the data model
|
|
7
7
|
|
|
8
8
|
The foundation layer: the feature's **data types** and the pure declarations
|
|
9
|
-
over them. A data type is a readonly,
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
over them. A data type is a readonly, serializable value suitable for persistence
|
|
10
|
+
and for communication over the wire — no functions, no handles, just plain data:
|
|
11
|
+
JSON primitives/arrays/objects plus `ReadonlySet`, `ReadonlyMap`, and `Blob`
|
|
12
|
+
(serialize a Set/Map-bearing value with `Data.stringify` / `Data.parse`). A `data/` **type** depends on nothing but `@adobe/data` and other
|
|
12
13
|
`data/` types, and needs no knowledge of anything built on top of it. The
|
|
13
14
|
**transitions** over those types are less restricted: when one injects a service
|
|
14
15
|
it may import freely from `services/` — the service type and any utilities its
|
|
@@ -9,14 +9,24 @@ paths:
|
|
|
9
9
|
source of truth. Each transition is a read→write **patch** over state; each
|
|
10
10
|
derivation a pure selector. Reference: `data-lit-todo`'s `data/state/`.
|
|
11
11
|
|
|
12
|
+
The presence of this folder makes the feature **state-based** — its Functional
|
|
13
|
+
State Specification is authoritative and the ECS conforms to it. A feature without
|
|
14
|
+
`data/state/` is **ECS-based** (the ECS is the source of truth, no conformance) and
|
|
15
|
+
this rule does not apply — see `../index.md`, Two modes.
|
|
16
|
+
|
|
12
17
|
```ts
|
|
13
18
|
// state.ts — the aggregate + the transition/derivation namespace.
|
|
14
19
|
export type State = { readonly todos: readonly Todo[]; readonly displayCompleted: boolean };
|
|
15
20
|
export * as State from "./public.js";
|
|
16
21
|
```
|
|
17
22
|
|
|
18
|
-
Every feature
|
|
19
|
-
`{
|
|
23
|
+
Every **state-based** feature owns a `State` (a scalar `{ playing: boolean }`, or
|
|
24
|
+
`{}` when there is none). An ECS-based feature has no `State` at all.
|
|
25
|
+
|
|
26
|
+
**A `State` field's collection type carries its ordering** — see
|
|
27
|
+
`../../data-modelling.md` (Collection ordering is carried by the type). `todos`
|
|
28
|
+
above is a `ReadonlyArray` because todo order is a user-visible, reorderable fact;
|
|
29
|
+
an unordered entity bag (bullets, sprites) is a `ReadonlySet`.
|
|
20
30
|
|
|
21
31
|
**`State` has a standard shape.** Two exports are conventional and drive
|
|
22
32
|
conformance:
|
|
@@ -48,7 +58,7 @@ export const transitions = import.meta.glob<Record<string, unknown>>(
|
|
|
48
58
|
It stays **out of `public.js`** deliberately: unlike `create`/`samples` (plain
|
|
49
59
|
prod-safe values), `transitions` is the test-case graph — hanging it on `State`
|
|
50
60
|
would drag `import.meta.glob` (a Vite-only construct) and every `cases` fixture
|
|
51
|
-
(fake services, matchers, the whole `@adobe/data
|
|
61
|
+
(fake services, matchers, the whole `@adobe/data-testing` module) into every
|
|
52
62
|
production import of `State`, none of which tree-shakes off a live namespace
|
|
53
63
|
re-export. So `create`/`samples` earn a namespace slot; `transitions` is a test
|
|
54
64
|
concern the tests import directly.
|
|
@@ -73,15 +83,17 @@ centrally by the shared driver's barrel-driven guard rather than by eyeballing
|
|
|
73
83
|
one test file per transform — and genuine non-transition helpers still keep
|
|
74
84
|
their own `*.test.ts` (see below).
|
|
75
85
|
|
|
76
|
-
The case types, matchers, and runners all live in the
|
|
77
|
-
**`@adobe/data
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
The case types, matchers, and runners all live in the separate
|
|
87
|
+
**`@adobe/data-testing`** package (two namespaces, `Match` and `Conformance`) —
|
|
88
|
+
kept out of `@adobe/data` itself so installing `@adobe/data` never pulls in a
|
|
89
|
+
`vitest` peer dependency; add `@adobe/data-testing` as a devDependency alongside
|
|
90
|
+
`vitest`, its optional peer dependency. Only one tiny per-feature file remains —
|
|
91
|
+
a ~10-line alias, `conformance-case.ts`, that binds `State` once so
|
|
92
|
+
transform/derivation files can write a one-parameter type:
|
|
81
93
|
|
|
82
94
|
```ts
|
|
83
95
|
// data/state/conformance-case.ts — the only per-feature conformance declaration
|
|
84
|
-
import { Conformance as ConformanceApi } from "@adobe/data
|
|
96
|
+
import { Conformance as ConformanceApi } from "@adobe/data-testing";
|
|
85
97
|
import type { State } from "./state.js";
|
|
86
98
|
export type Conformance<F extends (...args: never[]) => unknown> = ConformanceApi.Cases<State, F>;
|
|
87
99
|
export type Derivation<F extends (...args: never[]) => unknown> = ConformanceApi.DerivationCases<F>;
|
|
@@ -93,11 +105,12 @@ export const entity = ConformanceApi.entity;
|
|
|
93
105
|
|
|
94
106
|
```ts
|
|
95
107
|
// create-todo.ts
|
|
96
|
-
import { Match } from "@adobe/data
|
|
108
|
+
import { Match } from "@adobe/data-testing";
|
|
97
109
|
import type { Conformance } from "./conformance-case.js"; // the thin per-feature alias above
|
|
110
|
+
import type { Services } from "../../services/services.js"; // the feature's service map
|
|
98
111
|
export const createTodo = (
|
|
99
112
|
state: Pick<State, "todos">,
|
|
100
|
-
{ name, complete, analytics }: { name: string; complete?: boolean
|
|
113
|
+
{ name, complete, analytics }: { name: string; complete?: boolean } & Pick<Services, "analytics">,
|
|
101
114
|
): Pick<State, "todos"> => {
|
|
102
115
|
analytics.todoCreated({ name });
|
|
103
116
|
return { todos: [...state.todos, { name, complete: complete ?? false }] }; // writes patch only
|
|
@@ -141,10 +154,13 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
141
154
|
is the default and stays unchanged. (A full `before`/`after` still works — it just
|
|
142
155
|
overrides the default wholesale.)
|
|
143
156
|
- **`after` leaves minted values open** with the shared matchers `Match.anyNumber`
|
|
144
|
-
/ `Match.anyString`, imported from `@adobe/data
|
|
145
|
-
per-feature `matchers.ts` anymore. An
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
/ `Match.anyString`, imported from `@adobe/data-testing` — there is **no**
|
|
158
|
+
per-feature `matchers.ts` anymore. An entity's own numeric `id` is **ignored by
|
|
159
|
+
default** (the ECS allocates it from its own id-space), so a case simply **omits
|
|
160
|
+
`id`** and the entity's content still compares — no `id: Match.anyNumber` needed.
|
|
161
|
+
(Reach for `id: Match.anyNumber` only when the type makes `id` required and a
|
|
162
|
+
literal would otherwise pin it.) Match by content, not by the value you don't
|
|
163
|
+
control. `Match` is framework-agnostic and
|
|
148
164
|
honors any asymmetric matcher, so vitest's `expect.stringContaining(...)` interops
|
|
149
165
|
on the expected side too. When an id must **line up in two places** within one
|
|
150
166
|
comparison — a `selectedId` that points at a specific todo, say — use
|
|
@@ -154,7 +170,7 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
154
170
|
case does not pin at all; `ref` for one that must be consistent across the case.
|
|
155
171
|
- **Entity-addressed cases use `entity(specId)`.** A transition that addresses an
|
|
156
172
|
entity by id writes it as `args: { id: entity(2) }` — `entity` imported from the
|
|
157
|
-
feature's `conformance-case.ts` (re-exported from `@adobe/data
|
|
173
|
+
feature's `conformance-case.ts` (re-exported from `@adobe/data-testing`). It types
|
|
158
174
|
as the id it stands for (like `Match.anyNumber`), so it slots into the transform's
|
|
159
175
|
own arg type. `runSpec` unwraps it to the plain data-id for the pure side; the ECS
|
|
160
176
|
runners resolve it to the seeded entity (see `conformance.md`).
|
|
@@ -165,25 +181,29 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
165
181
|
`value` case → derivation; otherwise a transition whose declared `effects` are
|
|
166
182
|
also asserted). Passing `{ state: State }` (the same `state` shape `runFeature`
|
|
167
183
|
takes) is what makes each case's `before`/`input` a delta over `State.create()`.
|
|
168
|
-
Add `match` alongside it only when
|
|
169
|
-
|
|
184
|
+
Add `match` alongside it only when the feature needs float `tolerance` (see
|
|
185
|
+
`conformance.md`); unordered collections are modeled as `ReadonlySet` /
|
|
186
|
+
`ReadonlyMap` on `State` (below), not declared as a match option.
|
|
170
187
|
There is no per-feature `expect-state-matches.ts`, `record-effects.ts`,
|
|
171
188
|
`expect-conforms.ts`, or `conformance-case.type-test.ts` — those are gone; the
|
|
172
189
|
shared driver owns comparison, effect recording, and name-based auto-pairing, and
|
|
173
|
-
the `Effects` type-test now lives once in `@adobe/data
|
|
190
|
+
the `Effects` type-test now lives once in `@adobe/data-testing`. A genuine **non-transition helper** in
|
|
174
191
|
`state/` — a `create()` constructor, a single-field predicate — has no `cases`
|
|
175
192
|
and isn't a `(state,args)=>state` transform, so `runSpec` skips it: **keep its own
|
|
176
193
|
sibling `*.test.ts`** rather than deleting it and losing coverage.
|
|
177
194
|
|
|
178
195
|
## Injected services and side effects
|
|
179
196
|
|
|
180
|
-
A transform that needs an outside capability
|
|
181
|
-
|
|
182
|
-
(`
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
197
|
+
A transform that needs an outside capability injects it with **`Pick<Services,
|
|
198
|
+
…>`** — never an ad-hoc inline type. `Services` is the feature's service map
|
|
199
|
+
(`services/services.ts`, see `../services/index.md`), keyed by the service name
|
|
200
|
+
minus its `-service` suffix (`analytics`, `nameGenerator`), so the key and its type
|
|
201
|
+
come from **one** place and can't drift per-transition. Plain data args sit
|
|
202
|
+
alongside via intersection: `{ readonly count: number } & Pick<Services, "analytics">`
|
|
203
|
+
(or `Pick<Services, "a" | "b">` when a transition takes only services). The **same
|
|
204
|
+
services appear on `db.services` for the matching action** — pinned to `Services` in
|
|
205
|
+
the ecs `service-database` — so the transition is the complete spec of *both* the
|
|
206
|
+
state change and the service calls.
|
|
187
207
|
|
|
188
208
|
- A transition **is deterministic given its dependencies** — inject a fixed
|
|
189
209
|
double and the output (and its calls) are fixed. An **async** dependency makes
|
|
@@ -24,6 +24,34 @@ spec, it can be generated and kept honest by AI rules, with the conformance
|
|
|
24
24
|
tests as the safety net. Net result: write a slow-but-verifiable app, then
|
|
25
25
|
derive a fast one that provably behaves the same.
|
|
26
26
|
|
|
27
|
+
## Two modes: state-based vs. ECS-based
|
|
28
|
+
|
|
29
|
+
The "built twice" structure above is the **state-based** mode. Every feature is in
|
|
30
|
+
one of two modes, discriminated by **the presence of `data/state/`**:
|
|
31
|
+
|
|
32
|
+
- **State-based** — a **Functional State Specification (FSS)** is the source of
|
|
33
|
+
truth: the pure `data/State` aggregate with its transitions and derivations, and
|
|
34
|
+
the ECS is a conformance-verified implementation of it. Adds `data/state/` (State,
|
|
35
|
+
transitions, co-located `cases`, `spec.test.ts`), `services/main-service/conformance/`,
|
|
36
|
+
and the `state` projection computed. **This is how every new feature is authored.**
|
|
37
|
+
- **ECS-based** — the **ECS is the source of truth**, authored directly:
|
|
38
|
+
**no `data/state/`**, no FSS, no conformance. This is a **legacy** shape — features
|
|
39
|
+
written before the state-based approach existed. New features are **never** authored
|
|
40
|
+
this way; ECS-based exists only to describe and maintain those older features.
|
|
41
|
+
|
|
42
|
+
**Discriminator:** `data/state/` present → state-based; absent → ECS-based. The
|
|
43
|
+
`data/<type>` value folders (the serializable types backing components/resources,
|
|
44
|
+
the wire, and persistence) exist in **both** modes — only the aggregate `State`
|
|
45
|
+
spec and the conformance layer are mode-specific. `data/` never disappears; the
|
|
46
|
+
*aggregate spec* does.
|
|
47
|
+
|
|
48
|
+
**Testing follows the mode.** A state-based feature is verified by conformance: the
|
|
49
|
+
shared `cases` drive both the pure spec and the ECS, so a transaction or action with
|
|
50
|
+
a same-named transition needs no test of its own. An **ECS-based feature has no
|
|
51
|
+
conformance cases, so every transaction and every action carries its own unit
|
|
52
|
+
test** — the direct substitute for the conformance oracle (see `services/main-service/transactions.md`
|
|
53
|
+
and `actions.md`).
|
|
54
|
+
|
|
27
55
|
## The layers
|
|
28
56
|
|
|
29
57
|
**The layers organize by the *kind of type* each folder holds, not by a strict
|
|
@@ -20,6 +20,33 @@ Everything a feature exposes as a service lives here. Two kinds:
|
|
|
20
20
|
The rest of this rule governs the capability contracts; `main-service` follows
|
|
21
21
|
its own subtree.
|
|
22
22
|
|
|
23
|
+
## `services/services.ts` — the injectable service map
|
|
24
|
+
|
|
25
|
+
The folder root exports one `Services` type: the feature's capability services
|
|
26
|
+
keyed by short name (the `-service` suffix dropped), the single source of truth for
|
|
27
|
+
service injection.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// services/services.ts
|
|
31
|
+
import type { AnalyticsService } from "./analytics-service/analytics-service.js";
|
|
32
|
+
import type { NameGeneratorService } from "./name-generator-service/name-generator-service.js";
|
|
33
|
+
export type Services = {
|
|
34
|
+
readonly analytics: AnalyticsService;
|
|
35
|
+
readonly nameGenerator: NameGeneratorService;
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Transitions inject with `Pick<Services, …>`** (`data/state.md`), never a
|
|
40
|
+
re-declared inline `{ analytics: AnalyticsService }` — so the key/type live in one
|
|
41
|
+
place.
|
|
42
|
+
- **The ecs `service-database` is pinned to it.** After its `ServiceDatabase` type,
|
|
43
|
+
a drift-guard asserts the resolved services match the map, so `db.services` (what
|
|
44
|
+
actions call) and `Services` (what transitions inject) can't diverge:
|
|
45
|
+
`type _Pin = Assert<Equal<ServiceDatabase["services"], Services>>`.
|
|
46
|
+
- **Inherited services**, when a peer feature builds on another, intersect the
|
|
47
|
+
parent map: `export type Services = MainServices & { readonly baz: BazService }`.
|
|
48
|
+
- A feature with no capability services needs no `services.ts`.
|
|
49
|
+
|
|
23
50
|
## Capability contracts
|
|
24
51
|
|
|
25
52
|
Each is a namespace folder (`global/namespace.md`); the export and folder both carry the
|
|
@@ -48,6 +48,10 @@ export const addRandomTodo = async (service: ServiceDatabase) => {
|
|
|
48
48
|
Reactive computeds refresh only on a committed transaction, so an imperative
|
|
49
49
|
read of one can hand back a stale shared cache (and it's the UI's layer, not
|
|
50
50
|
the action's). This also keeps the action correct under the conformance seed.
|
|
51
|
+
- **ECS-based feature (no `data/state/`):** no conformance cases exist, so **every
|
|
52
|
+
action carries its own unit test** — build a db, run the action with fake
|
|
53
|
+
`services`, and assert the committed state and the recorded service calls. The
|
|
54
|
+
bullet below applies only to **state-based** features (see `../../index.md`, Two modes).
|
|
51
55
|
- **Conformance** is the action surface of the feature's single
|
|
52
56
|
`conformance/conformance.test.ts` `Conformance.runFeature({...})` call. It pulls
|
|
53
57
|
actions off **`plugin.actions`** and pairs each to the **same-named** `data/state`
|
|
@@ -25,7 +25,7 @@ full-state projection and re-runs on *any* field change (fine for a small featur
|
|
|
25
25
|
wasteful on a large or hot one, where you hand-wire the minimal resource/index
|
|
26
26
|
reads instead). A derivation takes **no services**, so its co-located `cases` are
|
|
27
27
|
inert `{ input, value }` data (no test-doubles) that tree-shake out of the app
|
|
28
|
-
build (they reference the `@adobe/data
|
|
28
|
+
build (they reference the `@adobe/data-testing` matchers only, and that module is
|
|
29
29
|
`sideEffects: false`) — the one hazard is a `cases` literal touching the
|
|
30
30
|
`public.js` barrel at load, which `state.md` already forbids. **Performance is the
|
|
31
31
|
first-class constraint**: reuse freely where it doesn't matter, hand-wire minimal
|
|
@@ -5,8 +5,13 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# services/main-service/conformance/ — keeping the ECS honest against the spec
|
|
7
7
|
|
|
8
|
+
**State-based features only.** This folder exists only when the feature has a
|
|
9
|
+
`data/state/` spec to conform to. An **ECS-based** feature (no `data/state/`) omits
|
|
10
|
+
this folder entirely and unit-tests its transactions/actions directly instead (see
|
|
11
|
+
`../../index.md`, Two modes, and `transactions.md` / `actions.md`).
|
|
12
|
+
|
|
8
13
|
Test-only (imported only by `*.test.ts`, in no facet barrel). The `data/state`
|
|
9
|
-
cases are the shared truth; the shared **`@adobe/data
|
|
14
|
+
cases are the shared truth; the shared **`@adobe/data-testing`** runner replays
|
|
10
15
|
them against the ECS. This folder holds only the *feature-specific projection*
|
|
11
16
|
(`projection.ts`) plus one `conformance.test.ts` that makes a **single**
|
|
12
17
|
`Conformance.runFeature({...})` call. Reference: `data-lit-tictactoe`'s
|
|
@@ -23,19 +28,23 @@ a normal runtime API — the collision is only in the name.) Likewise
|
|
|
23
28
|
`Match.matches` / `Match.assert` are comparison helpers for `*.test.ts` only —
|
|
24
29
|
never a runtime branch.
|
|
25
30
|
|
|
26
|
-
## What `@adobe/data
|
|
31
|
+
## What `@adobe/data-testing` gives you
|
|
27
32
|
|
|
28
33
|
Two namespaces, imported only from `*.test.ts` (the module is
|
|
29
|
-
`sideEffects: false
|
|
30
|
-
|
|
34
|
+
`sideEffects: false`; add it as a devDependency alongside `vitest`, its
|
|
35
|
+
optional peer dependency — kept as a separate package from `@adobe/data` so
|
|
36
|
+
installing `@adobe/data` never pulls in a `vitest` peer dependency):
|
|
31
37
|
|
|
32
38
|
- **`Match`** — the tolerant, matcher-aware value comparison: `matches(actual,
|
|
33
39
|
expected, options?)` and its throwing wrapper `assert(...)`, plus the matchers
|
|
34
40
|
`Match.anyNumber` / `Match.anyString` / `Match.ref(label)`. Options are
|
|
35
|
-
`{
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
|
|
41
|
+
`{ tolerance?: number }` — numbers snap to `tolerance` (default `0.01`) to absorb
|
|
42
|
+
F32↔f64 / trig noise. **Ordering is carried by the value's type**: a
|
|
43
|
+
`ReadonlyArray` compares **in order**, a `ReadonlySet` / `ReadonlyMap`
|
|
44
|
+
**order-independently** — there is no `unordered` option. A numeric `id` a case
|
|
45
|
+
does not mention is **ignored** (the ECS allocates it), so entity content compares
|
|
46
|
+
without pinning ids. Framework-agnostic: it honors any asymmetric matcher, so
|
|
47
|
+
vitest's `expect.any(...)` interops.
|
|
39
48
|
- **`Conformance`** — the case types (`Case`, `Cases`, `DerivationCase`,
|
|
40
49
|
`DerivationCases`, `Effects`, `ServiceCall`), the `entity(specId)` identity
|
|
41
50
|
marker, the id `resolver(map)`, the whole-feature driver **`runFeature`**, the
|
|
@@ -91,7 +100,7 @@ Conformance.runFeature({
|
|
|
91
100
|
computedPlugin: ComputedDatabase.plugin, // omit if no state/ derivations
|
|
92
101
|
projection,
|
|
93
102
|
hydrate: ["visibleTodos"], // entity-id-list computeds; omit if none
|
|
94
|
-
match: {
|
|
103
|
+
match: { tolerance: 0.1 }, // float grid only; omit if 0.01 is fine
|
|
95
104
|
ops: { actions: import.meta.glob([...]) }, // ONLY when ops aren't in the plugin facet
|
|
96
105
|
});
|
|
97
106
|
```
|
|
@@ -135,8 +144,9 @@ Conformance.runFeature({
|
|
|
135
144
|
`data-lit-tictactoe` is the zero-config call (no `computedPlugin`, no `hydrate`,
|
|
136
145
|
no `match`, no `ops` — moves are board-index addressed, so no `entity()`
|
|
137
146
|
markers). `data-lit-todo` adds `hydrate: ["visibleTodos"]` and `entity()` markers.
|
|
138
|
-
`data-lit-space-rock-game`
|
|
139
|
-
|
|
147
|
+
`data-lit-space-rock-game` models its entity bags (`bullets`, `asteroids`) as
|
|
148
|
+
`ReadonlySet` on `State`, so they compare order-independently by type — no `match`
|
|
149
|
+
option (its per-frame transitions are conformed by the systems tick loop, not here — see
|
|
140
150
|
`systems.md`).
|
|
141
151
|
|
|
142
152
|
## The pure spec — `data/state/spec.test.ts`
|
|
@@ -188,24 +198,24 @@ op is infra, and a thin **same-named** op (todo's `reorderTodo` action, space-ro
|
|
|
188
198
|
`createInitial` transaction) gives the transition something to pair with. Do **not**
|
|
189
199
|
reintroduce a per-item adapter to bridge a name mismatch — add the same-named op.
|
|
190
200
|
|
|
191
|
-
## Ordering, tolerance, `ref`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
## Ordering, tolerance, `ref`
|
|
202
|
+
|
|
203
|
+
Ordering is carried by the value's **type**, not a match option — `ReadonlyArray`
|
|
204
|
+
positional, `ReadonlySet` / `ReadonlyMap` order-independent (the rule and its
|
|
205
|
+
rationale live in `../../../data-modelling.md`). What's specific to writing
|
|
206
|
+
conformance cases:
|
|
207
|
+
|
|
208
|
+
- **A numeric `id` is ignored unless a case pins it.** The ECS allocates entity ids
|
|
209
|
+
from its own space, so a case omits `id` and the entity's content still compares.
|
|
210
|
+
Pin it only to assert a reference (below).
|
|
211
|
+
- **Float noise** is absorbed by the default `tolerance` (`0.01`), threaded through
|
|
212
|
+
`match?: { tolerance }`; raise it only when a case needs a looser grid.
|
|
213
|
+
- **`Match.ref(label)`** on the expected side asserts id *correspondence* for a
|
|
214
|
+
referential feature — a reference that must line up with the entity it points at
|
|
215
|
+
(a `selectedId` → a specific todo). Put `ref("t")` on both the reference **and**
|
|
216
|
+
that entity's `id`; the labels form a bijection over the actual ids.
|
|
217
|
+
`Match.anyNumber` / `anyString` leave an id a case doesn't pin fully open. `ref`
|
|
218
|
+
correspondence holds even across a `ReadonlySet` boundary.
|
|
209
219
|
|
|
210
220
|
## Recording side effects — built in, no Proxy
|
|
211
221
|
|
|
@@ -32,6 +32,11 @@ export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
|
|
|
32
32
|
result — read the touched slice, call the `data/` transform, write the diff.
|
|
33
33
|
- Keep transaction files **single-export** (the `transactions/` barrel is
|
|
34
34
|
`export *`-ed into the plugin facet, so a second export would pollute it).
|
|
35
|
+
- **ECS-based feature (no `data/state/`):** there are no conformance cases, so
|
|
36
|
+
**every transaction carries its own unit test** — `Store.create(plugin)`, run the
|
|
37
|
+
transaction, assert the resulting resources/entities/archetypes. That test is the
|
|
38
|
+
direct substitute for the conformance oracle (see `../../index.md`, Two modes). The
|
|
39
|
+
bullet below applies only to **state-based** features.
|
|
35
40
|
- **Conformance is wired once, centrally, and auto-paired** — not per-file. The
|
|
36
41
|
feature's single `conformance/conformance.test.ts` `Conformance.runFeature({...})`
|
|
37
42
|
call conforms transactions: it pulls them off **`plugin.transactions`** (the
|
package/.claude/rules/service.md
CHANGED
|
@@ -8,7 +8,7 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
Asynchronous data services. Live in the `services/` layer. Adhere to the namespace rule for type and function organization.
|
|
10
10
|
|
|
11
|
-
**Data** = readonly JSON values or Blobs.
|
|
11
|
+
**Data** = readonly JSON values, `ReadonlySet`, `ReadonlyMap`, or Blobs.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.97",
|
|
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.97"
|
|
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.97"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|