@adobe/data-ai 0.10.22 → 0.10.24
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 +8 -3
- package/.claude/rules/ecs.md +1 -1
- package/.claude/rules/element.md +9 -0
- package/.claude/rules/features/data/components.md +6 -3
- package/.claude/rules/features/data/index.md +4 -1
- package/.claude/rules/features/data/state.md +211 -0
- package/.claude/rules/features/ecs/conformance.md +12 -6
- package/.claude/rules/features/index.md +4 -0
- package/.claude/rules/features/services/index.md +2 -0
- package/.claude/rules/features/services/main-service/actions.md +72 -0
- package/.claude/rules/features/services/main-service/archetypes.md +51 -0
- package/.claude/rules/features/services/main-service/components.md +43 -0
- package/.claude/rules/features/services/main-service/computed.md +84 -0
- package/.claude/rules/features/services/main-service/conformance.md +97 -0
- package/.claude/rules/features/services/main-service/core-database.md +50 -0
- package/.claude/rules/features/services/main-service/index.md +189 -0
- package/.claude/rules/features/services/main-service/indexes.md +65 -0
- package/.claude/rules/features/services/main-service/resources.md +30 -0
- package/.claude/rules/features/services/main-service/services.md +27 -0
- package/.claude/rules/features/services/main-service/systems.md +145 -0
- package/.claude/rules/features/services/main-service/transactions.md +61 -0
- package/.claude/rules/features/ui/index.md +2 -0
- package/.claude/rules/service.md +25 -75
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -0
- package/bin/cli.mjs +24 -8
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +2 -0
- package/skills/build-app-entry/SKILL.md +2 -0
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-core-database/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +2 -0
- package/skills/build-feature/SKILL.md +3 -0
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-service-database/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-spec/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +2 -0
- package/skills/build-transactions/SKILL.md +2 -0
- package/skills/build-ui/SKILL.md +2 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/structure/SKILL.md +2 -0
- package/.claude/rules/features/ui/binding-element.md +0 -226
package/.claude/rules/README.md
CHANGED
|
@@ -26,6 +26,11 @@ features/
|
|
|
26
26
|
core.md indexes.md transactions.md computed.md services.md actions.md
|
|
27
27
|
systems.md conformance.md
|
|
28
28
|
ui/ # element / presentation / lazy-wrapper rules
|
|
29
|
+
data/state.md # LEGACY state-based layout (existing features only)
|
|
30
|
+
services/main-service/
|
|
31
|
+
index.md core-database.md components.md resources.md archetypes.md
|
|
32
|
+
indexes.md transactions.md computed.md services.md actions.md
|
|
33
|
+
systems.md conformance.md
|
|
29
34
|
app.md # src/app/: the composition root
|
|
30
35
|
```
|
|
31
36
|
|
|
@@ -34,9 +39,9 @@ etc.) apply everywhere, by design. Alongside `features/`, the remaining rules-ro
|
|
|
34
39
|
`.md` files hold **cross-cutting** patterns the feature rules reference —
|
|
35
40
|
`app.md`, `data-modelling.md`, `archetypes.md` (the query and iteration model),
|
|
36
41
|
`plugin-modelling.md`, `versioning.md`, and the UI rules `element.md` and
|
|
37
|
-
`lazy-element.md` (`features/ui/` holds `presentation.md` and the
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
`lazy-element.md` (`features/ui/` holds `presentation.md` and the lazy-wrapper rule).
|
|
43
|
+
They live here so the bundle is self-contained; this repo symlinks them into its own
|
|
44
|
+
`.claude/rules/`.
|
|
40
45
|
|
|
41
46
|
Each feature rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
|
|
42
47
|
rules apply once an application organizes its source under `features/<name>/`.
|
package/.claude/rules/ecs.md
CHANGED
|
@@ -150,7 +150,7 @@ Synchronous, deterministic atomic mutations. Receive `store` and a payload.
|
|
|
150
150
|
|
|
151
151
|
### actions
|
|
152
152
|
|
|
153
|
-
UI components that call actions must never consume returned values — see `
|
|
153
|
+
UI components that call actions must never consume returned values — see `element.md` (Actions are fire-and-forget).
|
|
154
154
|
|
|
155
155
|
**At most one transaction per action.** Multiple transactions in a single action corrupt the undo/redo stack.
|
|
156
156
|
|
package/.claude/rules/element.md
CHANGED
|
@@ -106,6 +106,15 @@ Actions are fire-and-forget. If a caller needs a result, that's a
|
|
|
106
106
|
transaction the binding invokes directly, *or* a new observable to
|
|
107
107
|
subscribe to — never a return value awaited in render.
|
|
108
108
|
|
|
109
|
+
## Presentation contract
|
|
110
|
+
|
|
111
|
+
- The element imports its sibling `*-presentation.ts`, which exports exactly `render`
|
|
112
|
+
(consumers derive props with `Parameters<typeof render>[0]`).
|
|
113
|
+
- Callback props are named as a verb or verbNoun (`toggleComplete`), never `on*`; the
|
|
114
|
+
element's variable matches the prop name.
|
|
115
|
+
- Cleanup on unmount goes through a hook (`useEffect(() => () => …, [])`), not a
|
|
116
|
+
lifecycle override.
|
|
117
|
+
|
|
109
118
|
## Deletion test
|
|
110
119
|
|
|
111
120
|
Strip the file to its essentials. What remains should be: optional
|
|
@@ -23,7 +23,7 @@ export const todo = True.schema;
|
|
|
23
23
|
export const components = { todo, name, complete, order, dragPosition };
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
- **Scope** is declared per file with `Scope` from `@adobe/data/
|
|
26
|
+
- **Scope** is declared per file with `Scope` from `@adobe/data/schema`:
|
|
27
27
|
|
|
28
28
|
| scope | shared | durable | spread |
|
|
29
29
|
|---|:-:|:-:|---|
|
|
@@ -32,6 +32,8 @@ export const components = { todo, name, complete, order, dragPosition };
|
|
|
32
32
|
| presence | yes | no | `...Scope.presence` |
|
|
33
33
|
| session | no | no | `...Scope.session` |
|
|
34
34
|
|
|
35
|
+
`presence` is not synced yet: `@adobe/data-sync` does not replicate
|
|
36
|
+
`nonPersistent` data, so a presence value behaves like `session` until it does.
|
|
35
37
|
On a component, the flags scope the **column**. An entity's own scope comes from
|
|
36
38
|
its marker components (`entities.md`).
|
|
37
39
|
- **Every column lives here**, including implementation-only ones (a tag, a drag
|
|
@@ -39,8 +41,9 @@ export const components = { todo, name, complete, order, dragPosition };
|
|
|
39
41
|
- **A tag** is a component with `True.schema`. Add one only when an entity's
|
|
40
42
|
component set would otherwise be contained in another's (`entities.md`).
|
|
41
43
|
- **The name describes the role**, the schema the shape (`position` of a `Vec3`).
|
|
42
|
-
- **Reserved names:** `id`, `nonPersistent`, `nonShared`
|
|
43
|
-
`
|
|
44
|
+
- **Reserved names:** `id`, `nonPersistent`, `nonShared` are built into the ECS
|
|
45
|
+
(`Database.Plugin.create` rejects them at compile time); `index` would collide with
|
|
46
|
+
the `index.ts` barrel.
|
|
44
47
|
- **A feature built on another** spreads the lower barrel first and pins that its own
|
|
45
48
|
names don't shadow it (a spread would overwrite silently):
|
|
46
49
|
```ts
|
|
@@ -5,6 +5,8 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# data/ — the feature's pure Data declarations
|
|
7
7
|
|
|
8
|
+
> **Legacy feature** (has `data/state/` or `services/main-service/`)? Follow `data/state.md` instead.
|
|
9
|
+
|
|
8
10
|
The foundation layer. Everything here is a declaration about plain, serializable
|
|
9
11
|
Data: JSON primitives/arrays/objects plus `ReadonlySet`, `ReadonlyMap`, and `Blob`
|
|
10
12
|
(serialize with `Data.stringify` / `Data.parse`). No functions in values, no services,
|
|
@@ -26,4 +28,5 @@ fields, it takes them structurally
|
|
|
26
28
|
(`Iterable<{ readonly mark: PlayerMark; readonly cellIndex: CellIndex }>`).
|
|
27
29
|
|
|
28
30
|
Components and resources share **one namespace** across every feature in an app:
|
|
29
|
-
a name is one column or one resource, never both, and
|
|
31
|
+
a name is one column or one resource, never both (the store warns on a clash, and the resource's schema replaces the component's), and
|
|
32
|
+
has one owning feature.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/data/state/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# data/state/ — the State specification
|
|
7
|
+
|
|
8
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../index.md`.
|
|
9
|
+
|
|
10
|
+
`State` is the whole feature as **one immutable object** — the pure, fully-tested
|
|
11
|
+
source of truth. Each transition is a read→write **patch** over state; each
|
|
12
|
+
derivation a pure selector. The ECS implementation is proven equivalent to it by
|
|
13
|
+
conformance (`../services/main-service/conformance.md`). Reference:
|
|
14
|
+
`data-lit-todo`'s `data/state/`.
|
|
15
|
+
|
|
16
|
+
The presence of this folder makes the feature **state-based**. A feature without
|
|
17
|
+
`data/state/` is **ECS-based** (no spec, no conformance) — see `../index.md`.
|
|
18
|
+
|
|
19
|
+
## The `State` shape
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// data/state/state.ts
|
|
23
|
+
export type State = {
|
|
24
|
+
readonly displayCompleted: boolean; // a SINGLETON → an ECS resource of the same name
|
|
25
|
+
readonly entities: ReadonlyMap<number, Todo>; // ALL entities, keyed by id; the value has no id
|
|
26
|
+
readonly selectedTodo: Entity; // a reference singleton (Entity is a branded number)
|
|
27
|
+
};
|
|
28
|
+
export * as State from "./public.js";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **Singletons** — every non-`entities` field → an ECS **resource** of the same name.
|
|
32
|
+
- **`entities`** — one `ReadonlyMap<number, EntityValue>` holding every entity, keyed
|
|
33
|
+
by a numeric id; the value type is the union of the feature's entity value types and
|
|
34
|
+
carries **no `id`** (identity is the key). Omit `entities` for a feature with no
|
|
35
|
+
entities (all singletons).
|
|
36
|
+
|
|
37
|
+
`State` may reuse plain ECS value types (the `Entity` id type, a `{ parent, order }`
|
|
38
|
+
bundle) but never ECS machinery (a store, a transaction, an index). Entity value types
|
|
39
|
+
are their own `data/<type>/` namespaces with a structural `is` guard (`../index.md`).
|
|
40
|
+
|
|
41
|
+
Two conventional exports drive conformance, re-exported through `public.js`:
|
|
42
|
+
- **`create(): State`** (`create.ts`) — the default; every case's `before`/`input` is a
|
|
43
|
+
delta over it.
|
|
44
|
+
- **`samples: readonly State[]`** (`samples.ts`) — representative full states the ecs
|
|
45
|
+
projection round-trips (`toState ∘ fromState ≡ identity`).
|
|
46
|
+
|
|
47
|
+
## Files: the pure function and its cases are SEPARATE
|
|
48
|
+
|
|
49
|
+
Each conformed function is two files:
|
|
50
|
+
|
|
51
|
+
- **`<name>.ts`** — the pure function, a **single export**, with **zero test-tier
|
|
52
|
+
imports** (no `@adobe/data-testing`, no fakes). It depends only on `@adobe/data`,
|
|
53
|
+
other `data/` types, and — for a transition — the service interfaces it injects.
|
|
54
|
+
- **`<name>.cases.ts`** — the spec-owned cases as **inert data**, imported by the
|
|
55
|
+
manifest. It imports the case type with `import type` and never constructs a live
|
|
56
|
+
service.
|
|
57
|
+
|
|
58
|
+
This split is enforced by the package's project-reference wall (`tsconfig.build.json`
|
|
59
|
+
excludes `*.cases.ts`): a runtime file importing test-tier code is a **compile error**.
|
|
60
|
+
|
|
61
|
+
### The transform — `<name>.ts`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// toggle-complete.ts
|
|
65
|
+
import type { Services } from "../../services/services.js";
|
|
66
|
+
import type { State } from "./state.js";
|
|
67
|
+
|
|
68
|
+
// Reads entities, writes entities — an `{ entities }` patch — and logs `todoToggled`.
|
|
69
|
+
export const toggleComplete = (
|
|
70
|
+
state: Pick<State, "entities">,
|
|
71
|
+
{ id, analytics }: { readonly id: number } & Pick<Services, "analytics">,
|
|
72
|
+
): Pick<State, "entities"> => {
|
|
73
|
+
analytics.todoToggled();
|
|
74
|
+
const target = state.entities.get(id);
|
|
75
|
+
if (target === undefined) return { entities: state.entities };
|
|
76
|
+
return { entities: new Map(state.entities).set(id, { ...target, complete: !target.complete }) };
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Transition signature** `(state: Pick<State, …reads>, args) => Pick<State, …writes>` —
|
|
81
|
+
a read→write patch. The parameter is the smallest `Pick<State,…>` it reads; the return
|
|
82
|
+
is only the fields it writes. No `<T> => T` generic, no `...state` spread — return the
|
|
83
|
+
patch and let the runner merge it. All non-state inputs go in the single `args` object;
|
|
84
|
+
a transition that takes neither args nor services takes only `(state)`.
|
|
85
|
+
- **Inject services with `Pick<Services, …>`** — never an inline `{ analytics: … }`. Data
|
|
86
|
+
args sit alongside via intersection: `{ readonly count: number } & Pick<Services, "a">`.
|
|
87
|
+
- **Guard no-ops by returning an empty/unchanged patch**, never by throwing. Reserve
|
|
88
|
+
`throw` for a genuine precondition violation a case names with `throws`.
|
|
89
|
+
- **Derivation signature** `(state) => value` — a pure selector over **two or more**
|
|
90
|
+
`State` fields. An entity query returns entity **ids** (`readonly number[]` /
|
|
91
|
+
`ReadonlySet<number>`); look values up in `entities`. Value math over a **single**
|
|
92
|
+
`data/<type>` lives on that type's namespace, not here.
|
|
93
|
+
|
|
94
|
+
### The cases — `<name>.cases.ts`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// toggle-complete.cases.ts
|
|
98
|
+
import { Entity } from "@adobe/data/ecs";
|
|
99
|
+
import type { Conformance } from "@adobe/data-testing";
|
|
100
|
+
import type { State } from "./state.js";
|
|
101
|
+
import type { toggleComplete } from "./toggle-complete.js";
|
|
102
|
+
|
|
103
|
+
export const cases: Conformance.SpecCases<State, typeof toggleComplete> = {
|
|
104
|
+
args: { type: "object", properties: { id: Entity.schema } }, // marks entity-reference args
|
|
105
|
+
cases: [
|
|
106
|
+
{ name: "marks an incomplete todo complete",
|
|
107
|
+
before: { entities: new Map([[1, { name: "a", complete: false, order: 0 }]]) },
|
|
108
|
+
args: { id: 1 }, // DATA only — services are never here
|
|
109
|
+
after: { entities: new Map([[1, { name: "a", complete: true, order: 0 }]]) },
|
|
110
|
+
effects: { analytics: [["todoToggled"]] } }, // service calls to assert
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- Annotate `Conformance.SpecCases<State, typeof fn>` for a transition,
|
|
116
|
+
`Conformance.SpecDerivations<typeof fn>` for a derivation. That annotation is the
|
|
117
|
+
transition/derivation discriminator — nothing is inferred from the signature.
|
|
118
|
+
- **`before` is a delta over `State.create()`**; **`after` is the writes patch** (only
|
|
119
|
+
the fields the transition changes). A derivation case is `{ name, input, value }`.
|
|
120
|
+
- **`args` is data only.** Injected services are stripped from the case's `args` type;
|
|
121
|
+
the runner synthesizes each service's recording double from the manifest's `services`
|
|
122
|
+
templates. A transition with only services takes no `args`.
|
|
123
|
+
- **Service returns are scheduled as data in `responses`**, keyed by service then method;
|
|
124
|
+
only value-returning methods take a schedule (a per-call FIFO — exhausting it throws).
|
|
125
|
+
**Service calls to assert go in `effects`** as `[method, ...args]` tuples (`Array` =
|
|
126
|
+
ordered, `Set` = any order); a value-returning read you don't assert is simply omitted.
|
|
127
|
+
The case thus **owns both the input and the expectation** — the double invents nothing.
|
|
128
|
+
```ts
|
|
129
|
+
// create-random-todo.cases.ts (a value-returning service)
|
|
130
|
+
responses: { nameGenerator: { generateName: ["random task"] },
|
|
131
|
+
analytics: { randomTodoRequested: [{ startedAt: 0 }] } },
|
|
132
|
+
effects: { analytics: [["randomTodoRequested"],
|
|
133
|
+
["randomTodoAdded", { timing: { startedAt: 0 }, name: "random task" }]] },
|
|
134
|
+
```
|
|
135
|
+
- **Entity ids are plain spec-id numbers everywhere** — map keys in `before`/`after`/
|
|
136
|
+
`input`, and reference fields hold the id they point at (`{ selectedTodo: 2 }`). The
|
|
137
|
+
ecs mints its own ids, so conformance compares up to an id-bijection. An
|
|
138
|
+
entity-addressed transition adds the `args` schema (above) marking each `Entity.schema`
|
|
139
|
+
field; the case then writes the id plain (`args: { id: 2 }`). For a minted scalar a
|
|
140
|
+
case won't pin (a timestamp), use `Match.anyNumber` / `Match.anyString`.
|
|
141
|
+
- **A `throws` case** declares `throws: true` (any error) or `throws: "substring"`
|
|
142
|
+
instead of `after`/`effects`/`responses` — the two are mutually exclusive.
|
|
143
|
+
|
|
144
|
+
## `transforms.ts` — the conformed-function barrel
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// transforms.ts — exactly the conformed transforms and derivations, nothing else
|
|
148
|
+
export { createTodo } from "./create-todo.js";
|
|
149
|
+
export { toggleComplete } from "./toggle-complete.js";
|
|
150
|
+
export { visibleTodos } from "./visible-todos.js";
|
|
151
|
+
// …
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Its keys are the conformance surface. A genuine **non-transition helper** (`appendTodo`,
|
|
155
|
+
`create`, `samples`) is *not* listed here and carries no cases; it keeps its own
|
|
156
|
+
`*.test.ts`.
|
|
157
|
+
|
|
158
|
+
## `spec.ts` — the conformance manifest
|
|
159
|
+
|
|
160
|
+
The single object both `spec.test.ts` and the ecs `conformance.test.ts` import. It is a
|
|
161
|
+
**test-tier** file (excluded from the runtime build) — the one place the feature touches
|
|
162
|
+
`@adobe/data-testing`.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// spec.ts
|
|
166
|
+
import { Conformance } from "@adobe/data-testing";
|
|
167
|
+
import { State } from "./state.js";
|
|
168
|
+
import * as transforms from "./transforms.js";
|
|
169
|
+
import { MainService } from "../../services/main-service/main-service.js";
|
|
170
|
+
import { ComputedDatabase } from "../../services/main-service/computed-database/computed-database.js";
|
|
171
|
+
import { projection } from "../../services/main-service/conformance/projection.js";
|
|
172
|
+
import { createFake as analytics } from "../../services/analytics-service/analytics.fake.js";
|
|
173
|
+
import { cases as createTodo } from "./create-todo.cases.js";
|
|
174
|
+
import { cases as toggleComplete } from "./toggle-complete.cases.js";
|
|
175
|
+
import { cases as visibleTodos } from "./visible-todos.cases.js";
|
|
176
|
+
// …one import per conformed fn
|
|
177
|
+
|
|
178
|
+
export const spec = Conformance.feature({
|
|
179
|
+
state: State,
|
|
180
|
+
fns: transforms,
|
|
181
|
+
plugin: MainService.plugin,
|
|
182
|
+
computedPlugin: ComputedDatabase.plugin, // required iff the feature has a derivation
|
|
183
|
+
projection,
|
|
184
|
+
hydrate: ["visibleTodos"], // EXACTLY the entity-id-list derivations
|
|
185
|
+
services: { analytics, nameGenerator }, // required iff a transition injects a service
|
|
186
|
+
cases: { createTodo, toggleComplete, visibleTodos, /* …one per fn */ },
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`Conformance.feature` enforces four guards at **compile time**:
|
|
191
|
+
- **`cases` coverage** — exactly one case module per `fns` entry (missing or extra → error).
|
|
192
|
+
- **`hydrate` exactness** — exactly the derivations that return a collection of the
|
|
193
|
+
projection's entity value (its ecs computed emits ids); miss one or add a non-hydrating
|
|
194
|
+
name → error.
|
|
195
|
+
- **`services` completeness** — exactly the injected-service keys the transitions declare,
|
|
196
|
+
each a `() => Service` template; absent for a service-free feature.
|
|
197
|
+
- **`computedPlugin` presence** — required when any case module is a derivation.
|
|
198
|
+
|
|
199
|
+
## `spec.test.ts` — the pure spec
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// spec.test.ts
|
|
203
|
+
import { Conformance } from "@adobe/data-testing";
|
|
204
|
+
import { spec } from "./spec.js";
|
|
205
|
+
Conformance.checkSpec(spec);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
One call verifies every transform and derivation against its cases (seeding `before`
|
|
209
|
+
over `State.create()`, synthesizing service doubles from `services` + `responses`,
|
|
210
|
+
asserting `after`/`value`/`effects`/`throws`). The ecs side is one matching call in
|
|
211
|
+
`conformance.test.ts` (`../services/main-service/conformance.md`).
|
|
@@ -76,10 +76,16 @@ calls actions, so conforming actions covers the path the app runs.
|
|
|
76
76
|
- **Extra ECS tests** (collision selection, a refill after the field clears) live here
|
|
77
77
|
too, since they need the spec or the projection.
|
|
78
78
|
|
|
79
|
-
##
|
|
79
|
+
## Per-case ambient context
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
An action that reads context the spec can't carry (the acting peer's `userId`,
|
|
82
|
+
stamped by the db's concurrency at dispatch, as in p2p presence) still uses
|
|
83
|
+
`checkFeature`. Pass a test `concurrency` and a `seedContext(db, before, args)` that
|
|
84
|
+
primes it from each case:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
Conformance.implementation(spec, { plugin, projection,
|
|
88
|
+
concurrency: peerConcurrency,
|
|
89
|
+
seedContext: (_db, _before, args) => { peerUserId = (args as { mark: string }).mark; },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
@@ -36,6 +36,10 @@ src/
|
|
|
36
36
|
|
|
37
37
|
A feature creates only the layers and folders it uses.
|
|
38
38
|
|
|
39
|
+
A feature that has `data/state/` or `services/main-service/` uses the **legacy
|
|
40
|
+
state-based layout**: follow `data/state.md` and `services/main-service/` for it, and
|
|
41
|
+
don't migrate it unless asked.
|
|
42
|
+
|
|
39
43
|
## Dependencies
|
|
40
44
|
|
|
41
45
|
```
|
|
@@ -5,6 +5,8 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# services/ — the feature's services
|
|
7
7
|
|
|
8
|
+
> **Legacy feature** (has `data/state/` or `services/main-service/`)? Follow `services/main-service/` instead.
|
|
9
|
+
|
|
8
10
|
A service is the boundary between the feature and the outside world (generation,
|
|
9
11
|
analytics, networking). Each is a namespace folder (`global/namespace.md`) holding its
|
|
10
12
|
interface, its implementation and a fake. Services depend only on `data/`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/action-database/actions/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# services/main-service/actions/ — async orchestration
|
|
7
|
+
|
|
8
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
9
|
+
|
|
10
|
+
One action per file: a function taking the **whole database** as its first
|
|
11
|
+
argument and pure `data/` args. Actions orchestrate anything *outside* a
|
|
12
|
+
single transaction — awaiting a `services/` port, sequencing calls, deriving
|
|
13
|
+
timing — and then commit the result through a transaction.
|
|
14
|
+
|
|
15
|
+
**Every *app-facing, transaction-backed* transition has a corresponding
|
|
16
|
+
same-named action** — the async realization the UI drives. It reads the same
|
|
17
|
+
services the transition injects from `db.services`, so it reproduces both the
|
|
18
|
+
transition's state change (through a transaction) and its side effects. It may
|
|
19
|
+
reuse another transition's transaction (`createRandomTodo` reuses `createTodo`) —
|
|
20
|
+
there need not be a same-named transaction; transactions are the looser layer.
|
|
21
|
+
|
|
22
|
+
**Per-frame / system transitions are exempt.** In a real-time feature the `step*`
|
|
23
|
+
/ physics / collision transitions are realized by the **systems** tick loop, not
|
|
24
|
+
by an action, and are conformed by the tick-loop test (`systems.md`), not the
|
|
25
|
+
action surface of `checkFeature`. Give an action only to transitions a user/UI invokes directly
|
|
26
|
+
(and skip it too when the realization needs more than one transaction — e.g. a
|
|
27
|
+
`newGame` that both sets bounds and resets is conformed via its transaction).
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import type { ServiceDatabase } from "../../service-database/service-database.js";
|
|
31
|
+
|
|
32
|
+
export const addRandomTodo = async (service: ServiceDatabase) => {
|
|
33
|
+
const name = await service.services.nameGenerator.generateName(); // await a services/ port
|
|
34
|
+
service.transactions.createTodo({ name }); // then exactly one commit
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Type the `service` parameter on the lowest database layer exposing what the
|
|
39
|
+
action touches — usually `ServiceDatabase` (services **and**
|
|
40
|
+
transactions). Never the action layer itself (that would be a cycle).
|
|
41
|
+
- **Call at most one transaction** per action, so undo/redo stays one step
|
|
42
|
+
per operation.
|
|
43
|
+
- **Fire-and-forget.** An action's return value is not consumed; results flow
|
|
44
|
+
back through observables. `service.services.*` calls are `void` or
|
|
45
|
+
awaited-internally, never surfaced to the caller.
|
|
46
|
+
- Do the outside-world work here: await/sequence `services/` calls, and if a
|
|
47
|
+
slow call needs timing, compute it here around the call.
|
|
48
|
+
- **Read current state synchronously from the store** (`db.resources` / `db.read`
|
|
49
|
+
/ `db.select`, then a pure `data/` helper) — never from a cached `computed`.
|
|
50
|
+
Reactive computeds refresh only on a committed transaction, so an imperative
|
|
51
|
+
read of one can hand back a stale shared cache (and it's the UI's layer, not
|
|
52
|
+
the action's). This also keeps the action correct under the conformance seed.
|
|
53
|
+
- **ECS-based feature (no `data/state/`):** no conformance cases exist, so **every
|
|
54
|
+
action carries its own unit test** — build a db, run the action with fake
|
|
55
|
+
`services`, and assert the committed state and the recorded service calls. The
|
|
56
|
+
bullet below applies only to **state-based** features (see `../../index.md`, Two modes).
|
|
57
|
+
- **Conformance** is the action surface of the feature's single
|
|
58
|
+
`conformance/conformance.test.ts` `Conformance.checkFeature(spec)` call. It pulls
|
|
59
|
+
actions off **`plugin.actions`** and pairs each to the **same-named** `data/state`
|
|
60
|
+
transition. **The action is the primary seam** — it reads injected services from
|
|
61
|
+
`db.services`, so the runner synthesizes each recording double from the manifest's
|
|
62
|
+
`services` templates + the case's `responses` and builds
|
|
63
|
+
`Database.toSystemDatabase(Database.create(plugin, { services }))`; it runs the action
|
|
64
|
+
with the case's data `args`, then `Match.assert`s `toState ≡ after` **and** checks the
|
|
65
|
+
recorded calls against the case's `effects`. No per-op wiring, no coverage guard. A thin
|
|
66
|
+
**same-named** action gives a transaction-only or renamed transition something to
|
|
67
|
+
pair with (todo's `reorderTodo`). A streaming/capability action with no transition
|
|
68
|
+
is skipped. **A per-transition action kept out of the facet** (to bound the
|
|
69
|
+
plugin's type) is supplied via the manifest's `ops.actions` glob —
|
|
70
|
+
`ops: { actions: import.meta.glob([".../actions/*.ts", "!.../actions/index.ts"], { eager: true }) }`
|
|
71
|
+
(p2p negotiation). That is the *only* reason to set `ops` (see `conformance.md`).
|
|
72
|
+
- An `index.ts` barrel feeds the `actions` plugin facet.
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
9
|
+
|
|
10
|
+
One file, one export: `archetypes`, built with `Database.archetypes(components, …)`,
|
|
11
|
+
which validates every key against the component map and preserves each archetype's
|
|
12
|
+
literal tuple (so no per-archetype `as const satisfies`). The archetype **model** —
|
|
13
|
+
query/superset semantics, base archetypes for logical supertypes, row iteration — is
|
|
14
|
+
the rules-root `archetypes.md`; this file is the declaration convention.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Database } from "@adobe/data/ecs";
|
|
18
|
+
import { components } from "./components.js";
|
|
19
|
+
|
|
20
|
+
export const archetypes = Database.archetypes(components, {
|
|
21
|
+
Todo: ["todo", "name", "complete", "order", "dragPosition", "assignees"],
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `UpperCase` names — an archetype names a *shape*, like a class.
|
|
26
|
+
- Archetypes may **span scopes**: a document entity can include an ephemeral
|
|
27
|
+
`session` column — `components` merges every scope, so any-scope key validates.
|
|
28
|
+
|
|
29
|
+
## satisfies CoreDatabase.Archetype
|
|
30
|
+
|
|
31
|
+
Type-check a shared base tuple against `CoreDatabase.Archetype` (= `readonly Component[]`;
|
|
32
|
+
see `core-database.md`) — the same type checks every `count` / `select` query key downstream:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const layer = ["placement", "name", "visible", "opacity"] as const satisfies CoreDatabase.Archetype;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Drift guard against the data-type
|
|
39
|
+
|
|
40
|
+
An archetype that mirrors a `data/` row type carries a **non-exported** compile-time
|
|
41
|
+
check — infer the row shape from the archetype and assert it equals the data-type, so
|
|
42
|
+
the two can't drift:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Schema } from "@adobe/data/schema";
|
|
46
|
+
import type { Assert, Equal } from "@adobe/data/types";
|
|
47
|
+
import type { PlacedMark } from "../../data/placed-mark/placed-mark.js";
|
|
48
|
+
|
|
49
|
+
const _schema = Schema.fromArchetype(components, archetypes.PlacedMark);
|
|
50
|
+
type _Check = Assert<Equal<Schema.ToType<typeof _schema>, PlacedMark>>;
|
|
51
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
9
|
+
|
|
10
|
+
One file, one export: `components`, built with `Database.components(...)`. A
|
|
11
|
+
component binds a `data/` type into ECS column storage, grouped by schema scope.
|
|
12
|
+
The scope groups are optional — declare only the ones you use.
|
|
13
|
+
|
|
14
|
+
Bind each schema straight from its `data/` type (or a primitive schema); don't
|
|
15
|
+
author shapes here. Component entries are any `Schema` — no `default` required
|
|
16
|
+
(that's resources):
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Database } from "@adobe/data/ecs";
|
|
20
|
+
import { Boolean, True } from "@adobe/data/schema";
|
|
21
|
+
import { PlayerMark } from "../../data/player-mark/player-mark.js";
|
|
22
|
+
import { DragPosition } from "../../data/drag-position/drag-position.js";
|
|
23
|
+
|
|
24
|
+
export const components = Database.components({
|
|
25
|
+
document: { mark: PlayerMark.schema, complete: Boolean.schema },
|
|
26
|
+
session: { dragPosition: DragPosition.schema },
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- A component name may differ from the type it stores (`position` of a `Vec3`,
|
|
31
|
+
`mark` of a `PlayerMark`) — the field name describes the role, the schema the
|
|
32
|
+
shape.
|
|
33
|
+
- **Scope = which group it sits in** (see `services/main-service/index.md` for the four scopes and
|
|
34
|
+
their flags). `document` is the common case; hover a scope key for its
|
|
35
|
+
meaning. Never write `nonShared` / `nonPersistent` by hand — the group applies
|
|
36
|
+
them.
|
|
37
|
+
- **Shared cross-feature columns live in `data/`** and go in `document` (which
|
|
38
|
+
preserves schema identity) so `combinePlugins` dedupes them.
|
|
39
|
+
- Struct vs. object storage and schema selection are properties of the `data/`
|
|
40
|
+
type, decided there.
|
|
41
|
+
- Don't infer entity type based upon component membership.
|
|
42
|
+
Consider using a const: true "tag" component or a type/kind enumerated value if needed.
|
|
43
|
+
Note that *many* good ecs designs don't care the type of the entity and only act on components.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/computed-database/computed/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# database/computed/ — derived observable values
|
|
7
|
+
|
|
8
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
9
|
+
|
|
10
|
+
One derived value per file: a `cached` function of a database layer that
|
|
11
|
+
returns an `Observe` of state projected through pure `data/` helpers.
|
|
12
|
+
Derivation logic itself lives in `data/`; a computed only wires a service
|
|
13
|
+
observable to it.
|
|
14
|
+
|
|
15
|
+
**Reuse tested pure logic; let performance pick the wiring.** Prefer wiring the
|
|
16
|
+
pure **`data/<type>` helper** (or composing indexes) directly — the tictactoe
|
|
17
|
+
computeds delegate their observable straight to
|
|
18
|
+
`BoardState.deriveStatus`/`getWinner`/`currentPlayer` — because a helper reads
|
|
19
|
+
exactly the resource/entities it needs, and both the computed and the matching
|
|
20
|
+
`state/` derivation call it, so they agree by construction.
|
|
21
|
+
|
|
22
|
+
You **may** also import a pure `state/` derivation `(state) => value` into a
|
|
23
|
+
computed where performance is adequate: a small-N or resource/scalar composition,
|
|
24
|
+
not a hot per-entity or large-N path. Reusing a tested derivation is good — the
|
|
25
|
+
only cost is that it takes the **whole `State`**, so the computed must observe the
|
|
26
|
+
full-state projection and re-runs on *any* field change (fine for a small feature,
|
|
27
|
+
wasteful on a large or hot one, where you hand-wire the minimal resource/index
|
|
28
|
+
reads instead). A derivation takes **no services**, so its sibling `*.cases.ts` is
|
|
29
|
+
inert `{ input, value }` data (no test-doubles); like every `*.cases.ts` it is
|
|
30
|
+
test-tier and excluded from the runtime build by the project-reference wall, so it
|
|
31
|
+
never reaches the app bundle. **Performance is the first-class constraint**: reuse
|
|
32
|
+
freely where it doesn't matter, hand-wire minimal reads where it does.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { cached } from "@adobe/data/cache";
|
|
36
|
+
import { Observe } from "@adobe/data/observe";
|
|
37
|
+
import { BoardState } from "../../../data/board-state/board-state.js";
|
|
38
|
+
import type { IndexDatabase } from "../../index-database/index-database.js";
|
|
39
|
+
|
|
40
|
+
export const status = cached((service: IndexDatabase) =>
|
|
41
|
+
Observe.withFilter(service.observe.resources.board, BoardState.deriveStatus),
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Type the parameter on the lowest database layer that exposes what it
|
|
46
|
+
reads. An `index.ts` barrel re-exports every computed; `computed-database.ts`
|
|
47
|
+
registers it under the `computed` facet.
|
|
48
|
+
|
|
49
|
+
For a computed that only needs *how many* entities match an archetype (an
|
|
50
|
+
empty-state flag, a badge count), read `db.observe.count(archetype.components,
|
|
51
|
+
{ where })` — an `Observe<number>` that sums archetype row counts and re-emits
|
|
52
|
+
only when the count changes, allocating no entity array. Prefer it over
|
|
53
|
+
`db.observe.select(...)` mapped through `.length`.
|
|
54
|
+
|
|
55
|
+
**Conform a computed to its `data/state` derivation** whenever one exists. The
|
|
56
|
+
derivation's sibling `*.cases.ts` holds `{ input, value }` cases
|
|
57
|
+
(`Conformance.SpecDerivations<typeof fn>`), and the feature's single
|
|
58
|
+
`conformance/conformance.test.ts` `Conformance.checkFeature(spec)` call conforms
|
|
59
|
+
computeds: it pulls them off **`computedPlugin.computed`**, pairs each to its
|
|
60
|
+
**same-named** derivation, seeds the store from `input`, reads the computed's
|
|
61
|
+
synchronous emission, and `Match.assert`s it against `value` (see `conformance.md`).
|
|
62
|
+
There is no `define`/`conforms` wiring. **`computedPlugin` is the `ComputedDatabase`
|
|
63
|
+
layer** — the runner builds computed conformance from that layer, not the assembled
|
|
64
|
+
`MainService`, so a `withCache` above can't hand back a stale pre-seed value (a
|
|
65
|
+
direct `fromState` seed emits no transaction to invalidate it). Comparison is
|
|
66
|
+
identity by default; a computed that emits an **entity-id list** names itself in
|
|
67
|
+
`hydrate: [...]` (todo's `visibleTodos`) so the runner projects each id through
|
|
68
|
+
`toData`. A computed with **no `state/` derivation is skipped** —
|
|
69
|
+
single-`data/<type>` math is covered by that helper's own test. A feature with no
|
|
70
|
+
`state/` derivation omits `computedPlugin` entirely.
|
|
71
|
+
|
|
72
|
+
**What needs conformance is proportional to wiring logic.** A computed that
|
|
73
|
+
composes/branches over the aggregate *is* a `state/` derivation (composes ≥2
|
|
74
|
+
fields) and is conformed here. A computed that **trivially applies one
|
|
75
|
+
`data/<type>` helper to one field** (`winner` ← `BoardState.getWinner(board)`) is
|
|
76
|
+
already covered by its parts — the field by `toState` conformance, the math by
|
|
77
|
+
that helper's unit test — so an added conformance would just re-invoke the same
|
|
78
|
+
helper on both sides (tautological); the helper's unit test is sufficient. Two
|
|
79
|
+
exceptions: (1) to pin a particular non-obvious wiring, promote it to a thin
|
|
80
|
+
`state/` derivation `(state) => Type.helper(state.field)` and the runner conforms
|
|
81
|
+
it — no new mechanism; (2) if a computed folds **entities into a value with no
|
|
82
|
+
intermediate `State` field**, that projection *is* unconformed — promote the
|
|
83
|
+
intermediate to a `State` field so `toState` covers it, making it a normal
|
|
84
|
+
`state/` derivation.
|