@adobe/data-ai 0.9.96 → 0.9.98
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 +33 -15
- 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/conformance.md +34 -25
- package/.claude/rules/features/services/main-service/index.md +9 -8
- package/.claude/rules/features/services/main-service/services.md +1 -1
- 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:
|
|
@@ -97,9 +107,10 @@ export const entity = ConformanceApi.entity;
|
|
|
97
107
|
// create-todo.ts
|
|
98
108
|
import { Match } from "@adobe/data-testing";
|
|
99
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
|
|
100
111
|
export const createTodo = (
|
|
101
112
|
state: Pick<State, "todos">,
|
|
102
|
-
{ name, complete, analytics }: { name: string; complete?: boolean
|
|
113
|
+
{ name, complete, analytics }: { name: string; complete?: boolean } & Pick<Services, "analytics">,
|
|
103
114
|
): Pick<State, "todos"> => {
|
|
104
115
|
analytics.todoCreated({ name });
|
|
105
116
|
return { todos: [...state.todos, { name, complete: complete ?? false }] }; // writes patch only
|
|
@@ -144,9 +155,12 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
144
155
|
overrides the default wholesale.)
|
|
145
156
|
- **`after` leaves minted values open** with the shared matchers `Match.anyNumber`
|
|
146
157
|
/ `Match.anyString`, imported from `@adobe/data-testing` — there is **no**
|
|
147
|
-
per-feature `matchers.ts` anymore. An
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
150
164
|
honors any asymmetric matcher, so vitest's `expect.stringContaining(...)` interops
|
|
151
165
|
on the expected side too. When an id must **line up in two places** within one
|
|
152
166
|
comparison — a `selectedId` that points at a specific todo, say — use
|
|
@@ -167,8 +181,9 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
167
181
|
`value` case → derivation; otherwise a transition whose declared `effects` are
|
|
168
182
|
also asserted). Passing `{ state: State }` (the same `state` shape `runFeature`
|
|
169
183
|
takes) is what makes each case's `before`/`input` a delta over `State.create()`.
|
|
170
|
-
Add `match` alongside it only when
|
|
171
|
-
|
|
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.
|
|
172
187
|
There is no per-feature `expect-state-matches.ts`, `record-effects.ts`,
|
|
173
188
|
`expect-conforms.ts`, or `conformance-case.type-test.ts` — those are gone; the
|
|
174
189
|
shared driver owns comparison, effect recording, and name-based auto-pairing, and
|
|
@@ -179,13 +194,16 @@ export const cases: Conformance<typeof createTodo> = [
|
|
|
179
194
|
|
|
180
195
|
## Injected services and side effects
|
|
181
196
|
|
|
182
|
-
A transform that needs an outside capability
|
|
183
|
-
|
|
184
|
-
(`
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
189
207
|
|
|
190
208
|
- A transition **is deterministic given its dependencies** — inject a fixed
|
|
191
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`
|
|
@@ -5,6 +5,11 @@ 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
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*
|
|
@@ -33,10 +38,13 @@ installing `@adobe/data` never pulls in a `vitest` peer dependency):
|
|
|
33
38
|
- **`Match`** — the tolerant, matcher-aware value comparison: `matches(actual,
|
|
34
39
|
expected, options?)` and its throwing wrapper `assert(...)`, plus the matchers
|
|
35
40
|
`Match.anyNumber` / `Match.anyString` / `Match.ref(label)`. Options are
|
|
36
|
-
`{
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
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.
|
|
40
48
|
- **`Conformance`** — the case types (`Case`, `Cases`, `DerivationCase`,
|
|
41
49
|
`DerivationCases`, `Effects`, `ServiceCall`), the `entity(specId)` identity
|
|
42
50
|
marker, the id `resolver(map)`, the whole-feature driver **`runFeature`**, the
|
|
@@ -92,7 +100,7 @@ Conformance.runFeature({
|
|
|
92
100
|
computedPlugin: ComputedDatabase.plugin, // omit if no state/ derivations
|
|
93
101
|
projection,
|
|
94
102
|
hydrate: ["visibleTodos"], // entity-id-list computeds; omit if none
|
|
95
|
-
match: {
|
|
103
|
+
match: { tolerance: 0.1 }, // float grid only; omit if 0.01 is fine
|
|
96
104
|
ops: { actions: import.meta.glob([...]) }, // ONLY when ops aren't in the plugin facet
|
|
97
105
|
});
|
|
98
106
|
```
|
|
@@ -136,8 +144,9 @@ Conformance.runFeature({
|
|
|
136
144
|
`data-lit-tictactoe` is the zero-config call (no `computedPlugin`, no `hydrate`,
|
|
137
145
|
no `match`, no `ops` — moves are board-index addressed, so no `entity()`
|
|
138
146
|
markers). `data-lit-todo` adds `hydrate: ["visibleTodos"]` and `entity()` markers.
|
|
139
|
-
`data-lit-space-rock-game`
|
|
140
|
-
|
|
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
|
|
141
150
|
`systems.md`).
|
|
142
151
|
|
|
143
152
|
## The pure spec — `data/state/spec.test.ts`
|
|
@@ -189,24 +198,24 @@ op is infra, and a thin **same-named** op (todo's `reorderTodo` action, space-ro
|
|
|
189
198
|
`createInitial` transaction) gives the transition something to pair with. Do **not**
|
|
190
199
|
reintroduce a per-item adapter to bridge a name mismatch — add the same-named op.
|
|
191
200
|
|
|
192
|
-
## Ordering, tolerance, `ref`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
210
219
|
|
|
211
220
|
## Recording side effects — built in, no Proxy
|
|
212
221
|
|
|
@@ -103,9 +103,9 @@ main-service/
|
|
|
103
103
|
core-database/ # the schema (above)
|
|
104
104
|
index-database/ # extends core; indexes/
|
|
105
105
|
transaction-database/ # extends index; transactions/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
action-database/ # extends
|
|
106
|
+
service-database/ # extends transaction; services/
|
|
107
|
+
computed-database/ # extends service; computed/
|
|
108
|
+
action-database/ # extends computed; actions/
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
Each exports a namespace mirroring the plugin — the composed object is a
|
|
@@ -121,8 +121,8 @@ export namespace IndexDatabase {
|
|
|
121
121
|
|
|
122
122
|
### The assembled database — `MainService`
|
|
123
123
|
|
|
124
|
-
*Which* layer is topmost varies with the facets a feature uses (
|
|
125
|
-
|
|
124
|
+
*Which* layer is topmost varies with the facets a feature uses (service /
|
|
125
|
+
computed / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/`
|
|
126
126
|
— need the *whole* feature database but must not name that layer, or adding or
|
|
127
127
|
dropping a layer rewrites them all. Alias it once from the **folder-eponymous
|
|
128
128
|
`main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in
|
|
@@ -155,10 +155,11 @@ two features' `CoreDatabase`s never collide.
|
|
|
155
155
|
transaction-context type. Use **`CoreDatabase.Store`** for a transaction that
|
|
156
156
|
reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
|
|
157
157
|
moment it reads an index.
|
|
158
|
-
- **the whole `Database`** — **
|
|
158
|
+
- **the whole `Database`** — **services**, **computed**, and **actions** each
|
|
159
159
|
take `service: <Layer>` (the lowest layer whose database exposes what they
|
|
160
|
-
read/call):
|
|
161
|
-
|
|
160
|
+
read/call): services read observables and call transactions; computed reads
|
|
161
|
+
`service.observe.*` and, once it needs a service's own reactive surface,
|
|
162
|
+
`service.services.*`; actions call `service.services.*` then
|
|
162
163
|
`service.transactions.*`.
|
|
163
164
|
|
|
164
165
|
Each subfolder has its own rule. Modelling a plugin's authored vs. derived
|
|
@@ -11,7 +11,7 @@ its transactions. This is where the async `services/` contracts (or
|
|
|
11
11
|
framework services like `AgenticService`) are wired to ECS state.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
export const createAgentService = (service:
|
|
14
|
+
export const createAgentService = (service: TransactionDatabase, mark: PlayerMark): AgenticService =>
|
|
15
15
|
/* … reads service.observe.*, calls service.transactions.* … */;
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -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.98",
|
|
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.98"
|
|
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.98"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|