@adobe/data-ai 0.10.21 → 0.10.22
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 +26 -31
- package/.claude/rules/app.md +46 -0
- package/.claude/rules/archetypes.md +5 -3
- package/.claude/rules/data-modelling.md +8 -7
- package/.claude/rules/ecs.md +11 -7
- package/.claude/rules/element.md +6 -5
- package/.claude/rules/features/data/components.md +51 -0
- package/.claude/rules/features/data/entities.md +37 -0
- package/.claude/rules/features/data/index.md +18 -70
- package/.claude/rules/features/data/resources.md +34 -0
- package/.claude/rules/features/data/values.md +36 -0
- package/.claude/rules/features/ecs/actions.md +31 -0
- package/.claude/rules/features/ecs/computed.md +31 -0
- package/.claude/rules/features/ecs/conformance.md +85 -0
- package/.claude/rules/features/ecs/core.md +43 -0
- package/.claude/rules/features/ecs/index.md +72 -0
- package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
- package/.claude/rules/features/ecs/services.md +36 -0
- package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
- package/.claude/rules/features/ecs/transactions.md +32 -0
- package/.claude/rules/features/index.md +104 -150
- package/.claude/rules/features/services/index.md +52 -84
- package/.claude/rules/features/spec/index.md +161 -0
- package/.claude/rules/features/ui/binding-element.md +5 -5
- package/.claude/rules/features/ui/index.md +8 -3
- package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
- package/.claude/rules/features/ui/presentation-test.md +1 -0
- package/.claude/rules/features/ui/presentation.md +1 -0
- package/.claude/rules/global/function-references.md +2 -2
- package/.claude/rules/global/namespace.md +3 -3
- package/.claude/rules/global/react.md +4 -4
- package/.claude/rules/lit.md +2 -2
- package/.claude/rules/plugin-modelling.md +2 -0
- package/.claude/rules/service.md +2 -0
- package/.claude/rules/versioning.md +8 -6
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +7 -8
- package/skills/build-app-entry/SKILL.md +12 -8
- package/skills/build-computed/SKILL.md +8 -7
- package/skills/build-core-database/SKILL.md +18 -15
- package/skills/build-data/SKILL.md +14 -13
- package/skills/build-feature/SKILL.md +2 -1
- package/skills/build-game/SKILL.md +6 -4
- package/skills/build-indexes/SKILL.md +6 -7
- package/skills/build-service-database/SKILL.md +8 -8
- package/skills/build-services/SKILL.md +9 -7
- package/skills/build-spec/SKILL.md +23 -0
- package/skills/build-systems/SKILL.md +12 -18
- package/skills/build-transactions/SKILL.md +8 -8
- package/skills/build-ui/SKILL.md +5 -4
- package/skills/review/SKILL.md +3 -3
- package/.claude/rules/features/data/state.md +0 -209
- package/.claude/rules/features/services/main-service/actions.md +0 -70
- package/.claude/rules/features/services/main-service/archetypes.md +0 -49
- package/.claude/rules/features/services/main-service/components.md +0 -41
- package/.claude/rules/features/services/main-service/computed.md +0 -82
- package/.claude/rules/features/services/main-service/conformance.md +0 -95
- package/.claude/rules/features/services/main-service/core-database.md +0 -48
- package/.claude/rules/features/services/main-service/index.md +0 -187
- package/.claude/rules/features/services/main-service/resources.md +0 -28
- package/.claude/rules/features/services/main-service/services.md +0 -25
- package/.claude/rules/features/services/main-service/transactions.md +0 -59
package/skills/build-ui/SKILL.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-ui
|
|
3
|
-
description: Build a feature's ui/ layer
|
|
3
|
+
description: Build a feature's ui/ layer.
|
|
4
4
|
input: feature
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Create `ui/`: one folder per UI unit —
|
|
9
9
|
`<name>/{<name>.ts (lazy wrapper), <name>-element.ts (container), <name>-presentation.ts (pure render), <name>.css.ts}`.
|
|
10
|
-
The element subscribes to state and
|
|
10
|
+
The element subscribes to `MainService` state and calls `service.actions.*`; the
|
|
11
|
+
presentation imports only `data/` and sibling `ui/` lazy wrappers. No business logic here.
|
|
11
12
|
|
|
12
|
-
Comes last. The how is in the auto-loading
|
|
13
|
-
`lazy-element.md`, `presentation.md`).
|
|
13
|
+
Comes last. Gate: presentation tests. The how is in the auto-loading
|
|
14
|
+
`features/ui/index.md` rule (and `element.md`, `lazy-element.md`, `presentation.md`).
|
package/skills/review/SKILL.md
CHANGED
|
@@ -11,9 +11,9 @@ rules (each layer's rule and the cross-cutting rules — `namespace`, `data-mode
|
|
|
11
11
|
`type-casts`, `cohesion`, …):
|
|
12
12
|
|
|
13
13
|
- **Correctness** — does it typecheck and follow the layer's required shape?
|
|
14
|
-
- **Adherence** — single public export per file, naming,
|
|
15
|
-
(`
|
|
16
|
-
store-layer typing, etc.
|
|
14
|
+
- **Adherence** — single public export per file, naming, per-file scope flags
|
|
15
|
+
(`Scope`), `satisfies` targets, barrels, the layer dependency order (no runtime
|
|
16
|
+
import of `spec/`), store-layer typing, etc.
|
|
17
17
|
- **Generality** — no leaked type identities; follows the cross-cutting patterns.
|
|
18
18
|
|
|
19
19
|
Report either **OPTIMAL**, or a short list of issues. Tag each issue with where the
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/data/state/**/*.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# data/state/ — the State specification
|
|
7
|
-
|
|
8
|
-
`State` is the whole feature as **one immutable object** — the pure, fully-tested
|
|
9
|
-
source of truth. Each transition is a read→write **patch** over state; each
|
|
10
|
-
derivation a pure selector. The ECS implementation is proven equivalent to it by
|
|
11
|
-
conformance (`../services/main-service/conformance.md`). Reference:
|
|
12
|
-
`data-lit-todo`'s `data/state/`.
|
|
13
|
-
|
|
14
|
-
The presence of this folder makes the feature **state-based**. A feature without
|
|
15
|
-
`data/state/` is **ECS-based** (no spec, no conformance) — see `../index.md`.
|
|
16
|
-
|
|
17
|
-
## The `State` shape
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
// data/state/state.ts
|
|
21
|
-
export type State = {
|
|
22
|
-
readonly displayCompleted: boolean; // a SINGLETON → an ECS resource of the same name
|
|
23
|
-
readonly entities: ReadonlyMap<number, Todo>; // ALL entities, keyed by id; the value has no id
|
|
24
|
-
readonly selectedTodo: Entity; // a reference singleton (Entity is a branded number)
|
|
25
|
-
};
|
|
26
|
-
export * as State from "./public.js";
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
- **Singletons** — every non-`entities` field → an ECS **resource** of the same name.
|
|
30
|
-
- **`entities`** — one `ReadonlyMap<number, EntityValue>` holding every entity, keyed
|
|
31
|
-
by a numeric id; the value type is the union of the feature's entity value types and
|
|
32
|
-
carries **no `id`** (identity is the key). Omit `entities` for a feature with no
|
|
33
|
-
entities (all singletons).
|
|
34
|
-
|
|
35
|
-
`State` may reuse plain ECS value types (the `Entity` id type, a `{ parent, order }`
|
|
36
|
-
bundle) but never ECS machinery (a store, a transaction, an index). Entity value types
|
|
37
|
-
are their own `data/<type>/` namespaces with a structural `is` guard (`../index.md`).
|
|
38
|
-
|
|
39
|
-
Two conventional exports drive conformance, re-exported through `public.js`:
|
|
40
|
-
- **`create(): State`** (`create.ts`) — the default; every case's `before`/`input` is a
|
|
41
|
-
delta over it.
|
|
42
|
-
- **`samples: readonly State[]`** (`samples.ts`) — representative full states the ecs
|
|
43
|
-
projection round-trips (`toState ∘ fromState ≡ identity`).
|
|
44
|
-
|
|
45
|
-
## Files: the pure function and its cases are SEPARATE
|
|
46
|
-
|
|
47
|
-
Each conformed function is two files:
|
|
48
|
-
|
|
49
|
-
- **`<name>.ts`** — the pure function, a **single export**, with **zero test-tier
|
|
50
|
-
imports** (no `@adobe/data-testing`, no fakes). It depends only on `@adobe/data`,
|
|
51
|
-
other `data/` types, and — for a transition — the service interfaces it injects.
|
|
52
|
-
- **`<name>.cases.ts`** — the spec-owned cases as **inert data**, imported by the
|
|
53
|
-
manifest. It imports the case type with `import type` and never constructs a live
|
|
54
|
-
service.
|
|
55
|
-
|
|
56
|
-
This split is enforced by the package's project-reference wall (`tsconfig.build.json`
|
|
57
|
-
excludes `*.cases.ts`): a runtime file importing test-tier code is a **compile error**.
|
|
58
|
-
|
|
59
|
-
### The transform — `<name>.ts`
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
// toggle-complete.ts
|
|
63
|
-
import type { Services } from "../../services/services.js";
|
|
64
|
-
import type { State } from "./state.js";
|
|
65
|
-
|
|
66
|
-
// Reads entities, writes entities — an `{ entities }` patch — and logs `todoToggled`.
|
|
67
|
-
export const toggleComplete = (
|
|
68
|
-
state: Pick<State, "entities">,
|
|
69
|
-
{ id, analytics }: { readonly id: number } & Pick<Services, "analytics">,
|
|
70
|
-
): Pick<State, "entities"> => {
|
|
71
|
-
analytics.todoToggled();
|
|
72
|
-
const target = state.entities.get(id);
|
|
73
|
-
if (target === undefined) return { entities: state.entities };
|
|
74
|
-
return { entities: new Map(state.entities).set(id, { ...target, complete: !target.complete }) };
|
|
75
|
-
};
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
- **Transition signature** `(state: Pick<State, …reads>, args) => Pick<State, …writes>` —
|
|
79
|
-
a read→write patch. The parameter is the smallest `Pick<State,…>` it reads; the return
|
|
80
|
-
is only the fields it writes. No `<T> => T` generic, no `...state` spread — return the
|
|
81
|
-
patch and let the runner merge it. All non-state inputs go in the single `args` object;
|
|
82
|
-
a transition that takes neither args nor services takes only `(state)`.
|
|
83
|
-
- **Inject services with `Pick<Services, …>`** — never an inline `{ analytics: … }`. Data
|
|
84
|
-
args sit alongside via intersection: `{ readonly count: number } & Pick<Services, "a">`.
|
|
85
|
-
- **Guard no-ops by returning an empty/unchanged patch**, never by throwing. Reserve
|
|
86
|
-
`throw` for a genuine precondition violation a case names with `throws`.
|
|
87
|
-
- **Derivation signature** `(state) => value` — a pure selector over **two or more**
|
|
88
|
-
`State` fields. An entity query returns entity **ids** (`readonly number[]` /
|
|
89
|
-
`ReadonlySet<number>`); look values up in `entities`. Value math over a **single**
|
|
90
|
-
`data/<type>` lives on that type's namespace, not here.
|
|
91
|
-
|
|
92
|
-
### The cases — `<name>.cases.ts`
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
// toggle-complete.cases.ts
|
|
96
|
-
import { Entity } from "@adobe/data/ecs";
|
|
97
|
-
import type { Conformance } from "@adobe/data-testing";
|
|
98
|
-
import type { State } from "./state.js";
|
|
99
|
-
import type { toggleComplete } from "./toggle-complete.js";
|
|
100
|
-
|
|
101
|
-
export const cases: Conformance.SpecCases<State, typeof toggleComplete> = {
|
|
102
|
-
args: { type: "object", properties: { id: Entity.schema } }, // marks entity-reference args
|
|
103
|
-
cases: [
|
|
104
|
-
{ name: "marks an incomplete todo complete",
|
|
105
|
-
before: { entities: new Map([[1, { name: "a", complete: false, order: 0 }]]) },
|
|
106
|
-
args: { id: 1 }, // DATA only — services are never here
|
|
107
|
-
after: { entities: new Map([[1, { name: "a", complete: true, order: 0 }]]) },
|
|
108
|
-
effects: { analytics: [["todoToggled"]] } }, // service calls to assert
|
|
109
|
-
],
|
|
110
|
-
};
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
- Annotate `Conformance.SpecCases<State, typeof fn>` for a transition,
|
|
114
|
-
`Conformance.SpecDerivations<typeof fn>` for a derivation. That annotation is the
|
|
115
|
-
transition/derivation discriminator — nothing is inferred from the signature.
|
|
116
|
-
- **`before` is a delta over `State.create()`**; **`after` is the writes patch** (only
|
|
117
|
-
the fields the transition changes). A derivation case is `{ name, input, value }`.
|
|
118
|
-
- **`args` is data only.** Injected services are stripped from the case's `args` type;
|
|
119
|
-
the runner synthesizes each service's recording double from the manifest's `services`
|
|
120
|
-
templates. A transition with only services takes no `args`.
|
|
121
|
-
- **Service returns are scheduled as data in `responses`**, keyed by service then method;
|
|
122
|
-
only value-returning methods take a schedule (a per-call FIFO — exhausting it throws).
|
|
123
|
-
**Service calls to assert go in `effects`** as `[method, ...args]` tuples (`Array` =
|
|
124
|
-
ordered, `Set` = any order); a value-returning read you don't assert is simply omitted.
|
|
125
|
-
The case thus **owns both the input and the expectation** — the double invents nothing.
|
|
126
|
-
```ts
|
|
127
|
-
// create-random-todo.cases.ts (a value-returning service)
|
|
128
|
-
responses: { nameGenerator: { generateName: ["random task"] },
|
|
129
|
-
analytics: { randomTodoRequested: [{ startedAt: 0 }] } },
|
|
130
|
-
effects: { analytics: [["randomTodoRequested"],
|
|
131
|
-
["randomTodoAdded", { timing: { startedAt: 0 }, name: "random task" }]] },
|
|
132
|
-
```
|
|
133
|
-
- **Entity ids are plain spec-id numbers everywhere** — map keys in `before`/`after`/
|
|
134
|
-
`input`, and reference fields hold the id they point at (`{ selectedTodo: 2 }`). The
|
|
135
|
-
ecs mints its own ids, so conformance compares up to an id-bijection. An
|
|
136
|
-
entity-addressed transition adds the `args` schema (above) marking each `Entity.schema`
|
|
137
|
-
field; the case then writes the id plain (`args: { id: 2 }`). For a minted scalar a
|
|
138
|
-
case won't pin (a timestamp), use `Match.anyNumber` / `Match.anyString`.
|
|
139
|
-
- **A `throws` case** declares `throws: true` (any error) or `throws: "substring"`
|
|
140
|
-
instead of `after`/`effects`/`responses` — the two are mutually exclusive.
|
|
141
|
-
|
|
142
|
-
## `transforms.ts` — the conformed-function barrel
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
// transforms.ts — exactly the conformed transforms and derivations, nothing else
|
|
146
|
-
export { createTodo } from "./create-todo.js";
|
|
147
|
-
export { toggleComplete } from "./toggle-complete.js";
|
|
148
|
-
export { visibleTodos } from "./visible-todos.js";
|
|
149
|
-
// …
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Its keys are the conformance surface. A genuine **non-transition helper** (`appendTodo`,
|
|
153
|
-
`create`, `samples`) is *not* listed here and carries no cases; it keeps its own
|
|
154
|
-
`*.test.ts`.
|
|
155
|
-
|
|
156
|
-
## `spec.ts` — the conformance manifest
|
|
157
|
-
|
|
158
|
-
The single object both `spec.test.ts` and the ecs `conformance.test.ts` import. It is a
|
|
159
|
-
**test-tier** file (excluded from the runtime build) — the one place the feature touches
|
|
160
|
-
`@adobe/data-testing`.
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
// spec.ts
|
|
164
|
-
import { Conformance } from "@adobe/data-testing";
|
|
165
|
-
import { State } from "./state.js";
|
|
166
|
-
import * as transforms from "./transforms.js";
|
|
167
|
-
import { MainService } from "../../services/main-service/main-service.js";
|
|
168
|
-
import { ComputedDatabase } from "../../services/main-service/computed-database/computed-database.js";
|
|
169
|
-
import { projection } from "../../services/main-service/conformance/projection.js";
|
|
170
|
-
import { createFake as analytics } from "../../services/analytics-service/analytics.fake.js";
|
|
171
|
-
import { cases as createTodo } from "./create-todo.cases.js";
|
|
172
|
-
import { cases as toggleComplete } from "./toggle-complete.cases.js";
|
|
173
|
-
import { cases as visibleTodos } from "./visible-todos.cases.js";
|
|
174
|
-
// …one import per conformed fn
|
|
175
|
-
|
|
176
|
-
export const spec = Conformance.feature({
|
|
177
|
-
state: State,
|
|
178
|
-
fns: transforms,
|
|
179
|
-
plugin: MainService.plugin,
|
|
180
|
-
computedPlugin: ComputedDatabase.plugin, // required iff the feature has a derivation
|
|
181
|
-
projection,
|
|
182
|
-
hydrate: ["visibleTodos"], // EXACTLY the entity-id-list derivations
|
|
183
|
-
services: { analytics, nameGenerator }, // required iff a transition injects a service
|
|
184
|
-
cases: { createTodo, toggleComplete, visibleTodos, /* …one per fn */ },
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
`Conformance.feature` enforces four guards at **compile time**:
|
|
189
|
-
- **`cases` coverage** — exactly one case module per `fns` entry (missing or extra → error).
|
|
190
|
-
- **`hydrate` exactness** — exactly the derivations that return a collection of the
|
|
191
|
-
projection's entity value (its ecs computed emits ids); miss one or add a non-hydrating
|
|
192
|
-
name → error.
|
|
193
|
-
- **`services` completeness** — exactly the injected-service keys the transitions declare,
|
|
194
|
-
each a `() => Service` template; absent for a service-free feature.
|
|
195
|
-
- **`computedPlugin` presence** — required when any case module is a derivation.
|
|
196
|
-
|
|
197
|
-
## `spec.test.ts` — the pure spec
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
// spec.test.ts
|
|
201
|
-
import { Conformance } from "@adobe/data-testing";
|
|
202
|
-
import { spec } from "./spec.js";
|
|
203
|
-
Conformance.checkSpec(spec);
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
One call verifies every transform and derivation against its cases (seeding `before`
|
|
207
|
-
over `State.create()`, synthesizing service doubles from `services` + `responses`,
|
|
208
|
-
asserting `after`/`value`/`effects`/`throws`). The ecs side is one matching call in
|
|
209
|
-
`conformance.test.ts` (`../services/main-service/conformance.md`).
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/services/main-service/action-database/actions/**/*.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# services/main-service/actions/ — async orchestration
|
|
7
|
-
|
|
8
|
-
One action per file: a function taking the **whole database** as its first
|
|
9
|
-
argument and pure `data/` args. Actions orchestrate anything *outside* a
|
|
10
|
-
single transaction — awaiting a `services/` port, sequencing calls, deriving
|
|
11
|
-
timing — and then commit the result through a transaction.
|
|
12
|
-
|
|
13
|
-
**Every *app-facing, transaction-backed* transition has a corresponding
|
|
14
|
-
same-named action** — the async realization the UI drives. It reads the same
|
|
15
|
-
services the transition injects from `db.services`, so it reproduces both the
|
|
16
|
-
transition's state change (through a transaction) and its side effects. It may
|
|
17
|
-
reuse another transition's transaction (`createRandomTodo` reuses `createTodo`) —
|
|
18
|
-
there need not be a same-named transaction; transactions are the looser layer.
|
|
19
|
-
|
|
20
|
-
**Per-frame / system transitions are exempt.** In a real-time feature the `step*`
|
|
21
|
-
/ physics / collision transitions are realized by the **systems** tick loop, not
|
|
22
|
-
by an action, and are conformed by the tick-loop test (`systems.md`), not the
|
|
23
|
-
action surface of `checkFeature`. Give an action only to transitions a user/UI invokes directly
|
|
24
|
-
(and skip it too when the realization needs more than one transaction — e.g. a
|
|
25
|
-
`newGame` that both sets bounds and resets is conformed via its transaction).
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import type { ServiceDatabase } from "../../service-database/service-database.js";
|
|
29
|
-
|
|
30
|
-
export const addRandomTodo = async (service: ServiceDatabase) => {
|
|
31
|
-
const name = await service.services.nameGenerator.generateName(); // await a services/ port
|
|
32
|
-
service.transactions.createTodo({ name }); // then exactly one commit
|
|
33
|
-
};
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
- Type the `service` parameter on the lowest database layer exposing what the
|
|
37
|
-
action touches — usually `ServiceDatabase` (services **and**
|
|
38
|
-
transactions). Never the action layer itself (that would be a cycle).
|
|
39
|
-
- **Call at most one transaction** per action, so undo/redo stays one step
|
|
40
|
-
per operation.
|
|
41
|
-
- **Fire-and-forget.** An action's return value is not consumed; results flow
|
|
42
|
-
back through observables. `service.services.*` calls are `void` or
|
|
43
|
-
awaited-internally, never surfaced to the caller.
|
|
44
|
-
- Do the outside-world work here: await/sequence `services/` calls, and if a
|
|
45
|
-
slow call needs timing, compute it here around the call.
|
|
46
|
-
- **Read current state synchronously from the store** (`db.resources` / `db.read`
|
|
47
|
-
/ `db.select`, then a pure `data/` helper) — never from a cached `computed`.
|
|
48
|
-
Reactive computeds refresh only on a committed transaction, so an imperative
|
|
49
|
-
read of one can hand back a stale shared cache (and it's the UI's layer, not
|
|
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).
|
|
55
|
-
- **Conformance** is the action surface of the feature's single
|
|
56
|
-
`conformance/conformance.test.ts` `Conformance.checkFeature(spec)` call. It pulls
|
|
57
|
-
actions off **`plugin.actions`** and pairs each to the **same-named** `data/state`
|
|
58
|
-
transition. **The action is the primary seam** — it reads injected services from
|
|
59
|
-
`db.services`, so the runner synthesizes each recording double from the manifest's
|
|
60
|
-
`services` templates + the case's `responses` and builds
|
|
61
|
-
`Database.toSystemDatabase(Database.create(plugin, { services }))`; it runs the action
|
|
62
|
-
with the case's data `args`, then `Match.assert`s `toState ≡ after` **and** checks the
|
|
63
|
-
recorded calls against the case's `effects`. No per-op wiring, no coverage guard. A thin
|
|
64
|
-
**same-named** action gives a transaction-only or renamed transition something to
|
|
65
|
-
pair with (todo's `reorderTodo`). A streaming/capability action with no transition
|
|
66
|
-
is skipped. **A per-transition action kept out of the facet** (to bound the
|
|
67
|
-
plugin's type) is supplied via the manifest's `ops.actions` glob —
|
|
68
|
-
`ops: { actions: import.meta.glob([".../actions/*.ts", "!.../actions/index.ts"], { eager: true }) }`
|
|
69
|
-
(p2p negotiation). That is the *only* reason to set `ops` (see `conformance.md`).
|
|
70
|
-
- An `index.ts` barrel feeds the `actions` plugin facet.
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/services/main-service/**/core-database/archetypes.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# core-database/archetypes.ts — the feature's archetypes
|
|
7
|
-
|
|
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.
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { Database } from "@adobe/data/ecs";
|
|
16
|
-
import { components } from "./components.js";
|
|
17
|
-
|
|
18
|
-
export const archetypes = Database.archetypes(components, {
|
|
19
|
-
Todo: ["todo", "name", "complete", "order", "dragPosition", "assignees"],
|
|
20
|
-
});
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
- `UpperCase` names — an archetype names a *shape*, like a class.
|
|
24
|
-
- Archetypes may **span scopes**: a document entity can include an ephemeral
|
|
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
|
-
```
|
|
35
|
-
|
|
36
|
-
## Drift guard against the data-type
|
|
37
|
-
|
|
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:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
import { Schema } from "@adobe/data/schema";
|
|
44
|
-
import type { Assert, Equal } from "@adobe/data/types";
|
|
45
|
-
import type { PlacedMark } from "../../data/placed-mark/placed-mark.js";
|
|
46
|
-
|
|
47
|
-
const _schema = Schema.fromArchetype(components, archetypes.PlacedMark);
|
|
48
|
-
type _Check = Assert<Equal<Schema.ToType<typeof _schema>, PlacedMark>>;
|
|
49
|
-
```
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/services/main-service/**/core-database/components.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# core-database/components.ts — the feature's components
|
|
7
|
-
|
|
8
|
-
One file, one export: `components`, built with `Database.components(...)`. A
|
|
9
|
-
component binds a `data/` type into ECS column storage, grouped by schema scope.
|
|
10
|
-
The scope groups are optional — declare only the ones you use.
|
|
11
|
-
|
|
12
|
-
Bind each schema straight from its `data/` type (or a primitive schema); don't
|
|
13
|
-
author shapes here. Component entries are any `Schema` — no `default` required
|
|
14
|
-
(that's resources):
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { Database } from "@adobe/data/ecs";
|
|
18
|
-
import { Boolean, True } from "@adobe/data/schema";
|
|
19
|
-
import { PlayerMark } from "../../data/player-mark/player-mark.js";
|
|
20
|
-
import { DragPosition } from "../../data/drag-position/drag-position.js";
|
|
21
|
-
|
|
22
|
-
export const components = Database.components({
|
|
23
|
-
document: { mark: PlayerMark.schema, complete: Boolean.schema },
|
|
24
|
-
session: { dragPosition: DragPosition.schema },
|
|
25
|
-
});
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
- A component name may differ from the type it stores (`position` of a `Vec3`,
|
|
29
|
-
`mark` of a `PlayerMark`) — the field name describes the role, the schema the
|
|
30
|
-
shape.
|
|
31
|
-
- **Scope = which group it sits in** (see `services/main-service/index.md` for the four scopes and
|
|
32
|
-
their flags). `document` is the common case; hover a scope key for its
|
|
33
|
-
meaning. Never write `nonShared` / `nonPersistent` by hand — the group applies
|
|
34
|
-
them.
|
|
35
|
-
- **Shared cross-feature columns live in `data/`** and go in `document` (which
|
|
36
|
-
preserves schema identity) so `combinePlugins` dedupes them.
|
|
37
|
-
- Struct vs. object storage and schema selection are properties of the `data/`
|
|
38
|
-
type, decided there.
|
|
39
|
-
- Don't infer entity type based upon component membership.
|
|
40
|
-
Consider using a const: true "tag" component or a type/kind enumerated value if needed.
|
|
41
|
-
Note that *many* good ecs designs don't care the type of the entity and only act on components.
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/services/main-service/computed-database/computed/**/*.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# database/computed/ — derived observable values
|
|
7
|
-
|
|
8
|
-
One derived value per file: a `cached` function of a database layer that
|
|
9
|
-
returns an `Observe` of state projected through pure `data/` helpers.
|
|
10
|
-
Derivation logic itself lives in `data/`; a computed only wires a service
|
|
11
|
-
observable to it.
|
|
12
|
-
|
|
13
|
-
**Reuse tested pure logic; let performance pick the wiring.** Prefer wiring the
|
|
14
|
-
pure **`data/<type>` helper** (or composing indexes) directly — the tictactoe
|
|
15
|
-
computeds delegate their observable straight to
|
|
16
|
-
`BoardState.deriveStatus`/`getWinner`/`currentPlayer` — because a helper reads
|
|
17
|
-
exactly the resource/entities it needs, and both the computed and the matching
|
|
18
|
-
`state/` derivation call it, so they agree by construction.
|
|
19
|
-
|
|
20
|
-
You **may** also import a pure `state/` derivation `(state) => value` into a
|
|
21
|
-
computed where performance is adequate: a small-N or resource/scalar composition,
|
|
22
|
-
not a hot per-entity or large-N path. Reusing a tested derivation is good — the
|
|
23
|
-
only cost is that it takes the **whole `State`**, so the computed must observe the
|
|
24
|
-
full-state projection and re-runs on *any* field change (fine for a small feature,
|
|
25
|
-
wasteful on a large or hot one, where you hand-wire the minimal resource/index
|
|
26
|
-
reads instead). A derivation takes **no services**, so its sibling `*.cases.ts` is
|
|
27
|
-
inert `{ input, value }` data (no test-doubles); like every `*.cases.ts` it is
|
|
28
|
-
test-tier and excluded from the runtime build by the project-reference wall, so it
|
|
29
|
-
never reaches the app bundle. **Performance is the first-class constraint**: reuse
|
|
30
|
-
freely where it doesn't matter, hand-wire minimal reads where it does.
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
import { cached } from "@adobe/data/cache";
|
|
34
|
-
import { Observe } from "@adobe/data/observe";
|
|
35
|
-
import { BoardState } from "../../../data/board-state/board-state.js";
|
|
36
|
-
import type { IndexDatabase } from "../../index-database/index-database.js";
|
|
37
|
-
|
|
38
|
-
export const status = cached((service: IndexDatabase) =>
|
|
39
|
-
Observe.withFilter(service.observe.resources.board, BoardState.deriveStatus),
|
|
40
|
-
);
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Type the parameter on the lowest database layer that exposes what it
|
|
44
|
-
reads. An `index.ts` barrel re-exports every computed; `computed-database.ts`
|
|
45
|
-
registers it under the `computed` facet.
|
|
46
|
-
|
|
47
|
-
For a computed that only needs *how many* entities match an archetype (an
|
|
48
|
-
empty-state flag, a badge count), read `db.observe.count(archetype.components,
|
|
49
|
-
{ where })` — an `Observe<number>` that sums archetype row counts and re-emits
|
|
50
|
-
only when the count changes, allocating no entity array. Prefer it over
|
|
51
|
-
`db.observe.select(...)` mapped through `.length`.
|
|
52
|
-
|
|
53
|
-
**Conform a computed to its `data/state` derivation** whenever one exists. The
|
|
54
|
-
derivation's sibling `*.cases.ts` holds `{ input, value }` cases
|
|
55
|
-
(`Conformance.SpecDerivations<typeof fn>`), and the feature's single
|
|
56
|
-
`conformance/conformance.test.ts` `Conformance.checkFeature(spec)` call conforms
|
|
57
|
-
computeds: it pulls them off **`computedPlugin.computed`**, pairs each to its
|
|
58
|
-
**same-named** derivation, seeds the store from `input`, reads the computed's
|
|
59
|
-
synchronous emission, and `Match.assert`s it against `value` (see `conformance.md`).
|
|
60
|
-
There is no `define`/`conforms` wiring. **`computedPlugin` is the `ComputedDatabase`
|
|
61
|
-
layer** — the runner builds computed conformance from that layer, not the assembled
|
|
62
|
-
`MainService`, so a `withCache` above can't hand back a stale pre-seed value (a
|
|
63
|
-
direct `fromState` seed emits no transaction to invalidate it). Comparison is
|
|
64
|
-
identity by default; a computed that emits an **entity-id list** names itself in
|
|
65
|
-
`hydrate: [...]` (todo's `visibleTodos`) so the runner projects each id through
|
|
66
|
-
`toData`. A computed with **no `state/` derivation is skipped** —
|
|
67
|
-
single-`data/<type>` math is covered by that helper's own test. A feature with no
|
|
68
|
-
`state/` derivation omits `computedPlugin` entirely.
|
|
69
|
-
|
|
70
|
-
**What needs conformance is proportional to wiring logic.** A computed that
|
|
71
|
-
composes/branches over the aggregate *is* a `state/` derivation (composes ≥2
|
|
72
|
-
fields) and is conformed here. A computed that **trivially applies one
|
|
73
|
-
`data/<type>` helper to one field** (`winner` ← `BoardState.getWinner(board)`) is
|
|
74
|
-
already covered by its parts — the field by `toState` conformance, the math by
|
|
75
|
-
that helper's unit test — so an added conformance would just re-invoke the same
|
|
76
|
-
helper on both sides (tautological); the helper's unit test is sufficient. Two
|
|
77
|
-
exceptions: (1) to pin a particular non-obvious wiring, promote it to a thin
|
|
78
|
-
`state/` derivation `(state) => Type.helper(state.field)` and the runner conforms
|
|
79
|
-
it — no new mechanism; (2) if a computed folds **entities into a value with no
|
|
80
|
-
intermediate `State` field**, that projection *is* unconformed — promote the
|
|
81
|
-
intermediate to a `State` field so `toState` covers it, making it a normal
|
|
82
|
-
`state/` derivation.
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/features/*/services/main-service/conformance/**/*.ts'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# services/main-service/conformance/ — keeping the ECS honest against the spec
|
|
7
|
-
|
|
8
|
-
**State-based features only** (a feature with a `data/state/` spec). Test-only: imported
|
|
9
|
-
only by `*.test.ts`, in no facet barrel. The `data/state` cases (authored per
|
|
10
|
-
`../../../data/state.md`, gathered by the `spec.ts` manifest) are the shared truth; the
|
|
11
|
-
`@adobe/data-testing` runner replays them against the ECS. This folder holds only the
|
|
12
|
-
**feature-specific projection** (`projection.ts`) plus one `conformance.test.ts`.
|
|
13
|
-
Reference: `data-lit-tictactoe` (minimal), `data-lit-todo` (entities + `hydrate`).
|
|
14
|
-
|
|
15
|
-
**The projection helpers and `Match` are test-only — never a runtime branch.**
|
|
16
|
-
`fromState`/`toState` rewrite the whole store out-of-band; runtime code reads through
|
|
17
|
-
observables/indexes and writes through transactions.
|
|
18
|
-
|
|
19
|
-
## `projection.ts` — the store ⇄ State mapping (the one per-feature piece)
|
|
20
|
-
|
|
21
|
-
A single aggregated export — three test-only helpers, one `export`:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
export const projection = { fromState, toState, toData };
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- `fromState(store, state)` seeds a store to a `State` (clear, insert entities, set
|
|
28
|
-
resources) and **returns the `id → entity` map** it built (`ReadonlyMap<Id, Entity>`).
|
|
29
|
-
The ecs mints its own ids, so the runner turns this map into the resolver that maps a
|
|
30
|
-
case's spec-id to the seeded entity — no feature writes id resolution by hand. Return
|
|
31
|
-
`void` for an index-addressed / singleton feature (ids then resolve to `Entity.none`).
|
|
32
|
-
- `toState(store)` reads the whole store back, built on `toData`.
|
|
33
|
-
- `toData(store, entity)` reads one entity as its `data/` value — the single place the
|
|
34
|
-
ecs↔data mapping lives. Its **return type** is what tells conformance which derivations
|
|
35
|
-
are entity-list computeds. Present only when the feature has entities.
|
|
36
|
-
|
|
37
|
-
## `conformance.test.ts` — one call
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { Conformance } from "@adobe/data-testing";
|
|
41
|
-
import { spec } from "../../../data/state/spec.js";
|
|
42
|
-
Conformance.checkFeature(spec);
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`checkFeature` replays the manifest's shared cases against the ECS: it pulls the
|
|
46
|
-
transactions/actions off `spec.plugin`, the computeds off `spec.computedPlugin`, seeds
|
|
47
|
-
each case's `before` over `State.create()`, synthesizes each service's recording double
|
|
48
|
-
from `spec.services` + the case's `responses`, round-trips `State.samples` through
|
|
49
|
-
`spec.projection`, and hydrates the id-emitting computeds named in `spec.hydrate` through
|
|
50
|
-
`toData`. It shares `spec` with `spec.test.ts` — same cases, substituted implementation.
|
|
51
|
-
|
|
52
|
-
Everything `checkFeature` needs lives on the manifest (`../../../data/state.md`):
|
|
53
|
-
`plugin`, `computedPlugin`, `projection`, `hydrate`, `services`, `state`, `fns`, `cases`.
|
|
54
|
-
The manifest's compile-time guards mean the two calls (`checkSpec` + `checkFeature`) are
|
|
55
|
-
the whole conformance surface — no per-op wiring, no coverage guard to hand-maintain.
|
|
56
|
-
|
|
57
|
-
- **Name-pairing.** Each ecs op conforms to its **same-named** transition/derivation. An
|
|
58
|
-
op with no same-named transition (infrastructure like `setInput`, a streaming action,
|
|
59
|
-
a richer UI op) is skipped — so when the real op is renamed (`dragTodo`), add a thin
|
|
60
|
-
**same-named** op (`reorderTodo`) for the transition to pair with; never a per-item
|
|
61
|
-
adapter.
|
|
62
|
-
- **Projection round-trip.** When `State.samples` is non-empty the runner adds a
|
|
63
|
-
`toState ∘ fromState ≡ identity` test per sample.
|
|
64
|
-
- **Comparison.** Ordering is carried by the value's type — `ReadonlyArray` positional,
|
|
65
|
-
`ReadonlySet`/`ReadonlyMap` order-independent. Entity ids compare up to an id-bijection
|
|
66
|
-
from the schema-marked spec-ids (no matcher). Float noise is absorbed by
|
|
67
|
-
`match?: { tolerance }` (default `0.01`). A minted scalar uses `Match.anyNumber` /
|
|
68
|
-
`Match.anyString`.
|
|
69
|
-
|
|
70
|
-
## The escape hatch — per-surface `seedContext` / custom concurrency
|
|
71
|
-
|
|
72
|
-
A feature that needs **ambient per-case context differing per surface** — a user-scoped
|
|
73
|
-
`userId` seeded before the raw transaction *and* independently before the action dispatch
|
|
74
|
-
(**p2p presence**) — cannot use `checkFeature` (it has no place to thread two different
|
|
75
|
-
`seedContext`s or a custom concurrency). It drives the lower-level runners directly,
|
|
76
|
-
feeding them the manifest's cases via `Conformance.adaptCases`:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { Conformance } from "@adobe/data-testing";
|
|
80
|
-
import { spec } from "../../../data/state/spec.js";
|
|
81
|
-
|
|
82
|
-
Conformance.runTransactions({
|
|
83
|
-
createStore, fromState, toState,
|
|
84
|
-
transitions: Conformance.adaptCases(spec.fns, spec.cases, spec.services, false),
|
|
85
|
-
transactions,
|
|
86
|
-
seedContext: (store, _before, args) => seedUserId(store, (args as { mark: string }).mark),
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
`adaptCases(fns, cases, services, injectServices)` turns the manifest into the discovery
|
|
91
|
-
map the runners consume — `injectServices: false` for the transaction store (a
|
|
92
|
-
transaction never receives services), `true` for the action db. `runActions` /
|
|
93
|
-
`runComputeds` take the same shape and their own `makeDb` / `seedContext` / concurrency.
|
|
94
|
-
This is the **only** sanctioned use of the lower runners; ordinary features use
|
|
95
|
-
`checkFeature`.
|
|
@@ -1,48 +0,0 @@
|
|
|
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).
|