@adobe/data-ai 0.10.20 → 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/.claude/rules/README.md
CHANGED
|
@@ -7,43 +7,38 @@ frontmatter glob, injects them only when a matching file is in context.
|
|
|
7
7
|
|
|
8
8
|
## Layout mirrors the feature-folder structure
|
|
9
9
|
|
|
10
|
-
The `features/` subtree mirrors
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
child gets its own file (recursing wherever the source tree does):
|
|
10
|
+
The `features/` subtree mirrors a feature's layers one-to-one, so the guidance for a
|
|
11
|
+
folder lives at the same path shape as the code it governs. A folder's `index.md`
|
|
12
|
+
holds just enough to understand that folder; each child gets its own file:
|
|
14
13
|
|
|
15
14
|
```
|
|
16
15
|
global/ # always-on conventions — deliberately repo-wide globs
|
|
17
16
|
namespace.md cohesion.md type-casts.md function-references.md react.md
|
|
18
17
|
features/
|
|
19
|
-
index.md # the
|
|
18
|
+
index.md # the layering as a whole: data → services → spec | ecs → ui → app
|
|
20
19
|
data/
|
|
21
|
-
index.md #
|
|
22
|
-
|
|
23
|
-
services/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
index.md # pure Data declarations
|
|
21
|
+
values.md components.md resources.md entities.md
|
|
22
|
+
services/index.md # service namespaces: interface, create, createFake
|
|
23
|
+
spec/index.md # the State spec: actions, derivations, cases (test tier)
|
|
24
|
+
ecs/
|
|
25
|
+
index.md # the layered database plugins + MainService
|
|
26
|
+
core.md indexes.md transactions.md computed.md services.md actions.md
|
|
27
|
+
systems.md conformance.md
|
|
28
|
+
ui/ # element / presentation / lazy-wrapper rules
|
|
29
|
+
app.md # src/app/: the composition root
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
The `global/` rules
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
here so the bundle is self-contained; this repo symlinks them into its own
|
|
42
|
-
`.claude/rules/` (as it does `features/`).
|
|
32
|
+
The `global/` rules are **intentionally repo-wide** — their `paths:` globs (`**/*.ts`
|
|
33
|
+
etc.) apply everywhere, by design. Alongside `features/`, the remaining rules-root
|
|
34
|
+
`.md` files hold **cross-cutting** patterns the feature rules reference —
|
|
35
|
+
`app.md`, `data-modelling.md`, `archetypes.md` (the query and iteration model),
|
|
36
|
+
`plugin-modelling.md`, `versioning.md`, and the UI rules `element.md` and
|
|
37
|
+
`lazy-element.md` (`features/ui/` holds `presentation.md` and the binding-element and
|
|
38
|
+
lazy-wrapper rules). They live here so the bundle is
|
|
39
|
+
self-contained; this repo symlinks them into its own `.claude/rules/`.
|
|
43
40
|
|
|
44
|
-
Each rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
|
|
45
|
-
rules apply
|
|
46
|
-
|
|
47
|
-
`
|
|
48
|
-
`main-service/index.md` (the layer overview) and `main-service/components.md`
|
|
49
|
-
(the specifics) — at creation and at every later edit.
|
|
41
|
+
Each feature rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
|
|
42
|
+
rules apply once an application organizes its source under `features/<name>/`.
|
|
43
|
+
Editing `features/<name>/data/components/name.ts` pulls in both `data/index.md` and
|
|
44
|
+
`data/components.md` — at creation and at every later edit.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/src/app/**/*.ts'
|
|
4
|
+
- '**/src/app/**/*.tsx'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# src/app/ — the composition root
|
|
8
|
+
|
|
9
|
+
The app root composes the features. It is the only place that knows every feature,
|
|
10
|
+
and no feature imports it.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
src/app/
|
|
14
|
+
schema.ts the app's composed plugin (when there are several features)
|
|
15
|
+
versioning/ versions.ts + versions.test.ts (versioning.md)
|
|
16
|
+
persistence.test.ts a save/load round-trip, when the app persists
|
|
17
|
+
*.test.ts cross-feature tests (e.g. a todo op ignores a user id)
|
|
18
|
+
main.ts the entry: create the database, inject services, render
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// schema.ts — main's full behavior, plus the schema of features that load lazily
|
|
23
|
+
const appSchemaPlugin = Database.Plugin.create({
|
|
24
|
+
imports: AssignCoreDatabase.plugin,
|
|
25
|
+
extends: MainService.plugin,
|
|
26
|
+
});
|
|
27
|
+
export namespace AppSchema {
|
|
28
|
+
export const plugin = appSchemaPlugin;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// main.ts
|
|
32
|
+
const service = Database.create(AppSchema.plugin, {
|
|
33
|
+
versioning: createVersionUpgrader(versions),
|
|
34
|
+
services: { game }, // a service whose `create` throws until the app injects it
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **`imports`, not `extends`, for a lazily loaded feature**: the store knows its
|
|
39
|
+
columns up front (its data persists and loads), while its behavior, types and code
|
|
40
|
+
stay out until one of its elements connects and extends the database.
|
|
41
|
+
- **Inject every service whose `create` throws** at `Database.create`. Services can't
|
|
42
|
+
be injected by a later `extend`.
|
|
43
|
+
- **Versioning** folds the whole persisted schema, so it lives here, built from
|
|
44
|
+
`AppSchema.plugin`.
|
|
45
|
+
- A single-feature app needs no `schema.ts`; `main.ts` uses the feature's
|
|
46
|
+
`MainService.plugin`.
|
|
@@ -4,13 +4,15 @@ paths:
|
|
|
4
4
|
- '**/*-plugin/**/*.ts'
|
|
5
5
|
- '**/*-database.ts'
|
|
6
6
|
- '**/*-database/**/*.ts'
|
|
7
|
+
- '**/features/*/data/entities/**/*.ts'
|
|
8
|
+
- '**/features/*/ecs/core/**/*.ts'
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
# Archetypes — the ECS query & iteration model
|
|
10
12
|
|
|
11
|
-
General to all `@adobe/data` ECS use, feature-organized or not. The
|
|
12
|
-
`
|
|
13
|
-
`features/
|
|
13
|
+
General to all `@adobe/data` ECS use, feature-organized or not. The feature file
|
|
14
|
+
conventions live in `features/data/entities.md` (entity tuples) and
|
|
15
|
+
`features/ecs/core.md` (archetype packing).
|
|
14
16
|
|
|
15
17
|
An archetype is a named set of component keys — a kind of entity — and also a
|
|
16
18
|
**query**: `queryArchetypes(include, { exclude })` (and the `select` / `count`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/data
|
|
3
|
+
- '**/features/*/data/**/*.ts'
|
|
4
|
+
- '**/features/*/spec/**/*.ts'
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Data modelling — locality of knowledge
|
|
@@ -54,15 +55,15 @@ single source of truth, not a downstream comparison flag:
|
|
|
54
55
|
may be a plain `number` or the ECS `Entity` type (a branded `number`) — reusing
|
|
55
56
|
`Entity`, and reference bundles like `CoeditingRelations.OrderedChild`, is allowed:
|
|
56
57
|
they are plain value types, not ECS machinery, and keep the spec aligned with the ECS
|
|
57
|
-
(see `features/
|
|
58
|
-
name
|
|
58
|
+
(see `features/spec/index.md`). In a `State`, every `ReadonlyMap` field is an entity
|
|
59
|
+
collection; a lookup keyed by an enum or name belongs inside a value type.
|
|
59
60
|
|
|
60
61
|
**Entities are keyed, never id-bearing values.** An entity's identity is its map
|
|
61
62
|
key, so entity value types (`Todo`, `Bullet`) have no `id` field, and there is no
|
|
62
|
-
`ReadonlySet<T>`/`ReadonlyArray<T>` *of entity values* —
|
|
63
|
-
`ReadonlyMap<number, …>`
|
|
64
|
-
|
|
65
|
-
See `features/
|
|
63
|
+
`ReadonlySet<T>`/`ReadonlyArray<T>` *of entity values* in `State` — a
|
|
64
|
+
`ReadonlyMap<number, …>` per entity kind is the only home for entities. A derivation
|
|
65
|
+
may return entity values for display (the ECS computed then emits ids, hydrated by
|
|
66
|
+
conformance). See `features/spec/index.md` for the full `State` shape.
|
|
66
67
|
|
|
67
68
|
These are first-class `Data` (see `features/data/index.md`) — serialize a
|
|
68
69
|
Set/Map-bearing value with `Data.stringify` / `Data.parse` (plain `JSON.stringify`
|
package/.claude/rules/ecs.md
CHANGED
|
@@ -7,6 +7,8 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
# Database.Plugin authoring
|
|
9
9
|
|
|
10
|
+
> Inside a feature (`features/<name>/ecs/`), the `features/ecs/` rules govern file layout and naming: layer files are `<layer>-database.ts` exporting an `XDatabase` namespace, and systems are camelCase. This file covers general plugin mechanics.
|
|
11
|
+
|
|
10
12
|
Plugins are created with `Database.Plugin.create()` from `@adobe/data/ecs`.
|
|
11
13
|
|
|
12
14
|
## Property order (enforced at runtime)
|
|
@@ -19,7 +21,7 @@ Properties **must** appear in this exact order. All are optional.
|
|
|
19
21
|
| 2 | `extends` | `Plugin` | Base plugin to extend (types re-exported) |
|
|
20
22
|
| 3 | `services` | `(db) => ServiceInstance` | Singleton service factories |
|
|
21
23
|
| 4 | `components` | schema object | ECS component schemas |
|
|
22
|
-
| 5 | `resources` |
|
|
24
|
+
| 5 | `resources` | schema with a `default` | Global resource schemas |
|
|
23
25
|
| 6 | `archetypes` | `['comp1', 'comp2']` | Standard ECS archetypes |
|
|
24
26
|
| 7 | `indexes` | `{ key, order?, unique?, archetype? }` | Sorted/filtered entity indexes |
|
|
25
27
|
| 8 | `computed` | `(db) => Observe<T>` | Computed observables |
|
|
@@ -99,7 +101,7 @@ const particleDataPlugin = Database.Plugin.create({
|
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
- **Components**: per-entity data. Use schema imports (`Vec3`, `Vec4`, `F32` from `@adobe/data/math`) or type namespaces for custom shapes.
|
|
102
|
-
- **Resources**: global state.
|
|
104
|
+
- **Resources**: global state. Every resource carries a `default` (`{ ...Boolean.schema }`, or `{ default: 'dark' as ThemeColor }` to widen a literal).
|
|
103
105
|
- **Archetypes**: one per entity kind. List all components that kind requires.
|
|
104
106
|
|
|
105
107
|
---
|
|
@@ -108,23 +110,25 @@ const particleDataPlugin = Database.Plugin.create({
|
|
|
108
110
|
|
|
109
111
|
### components
|
|
110
112
|
|
|
111
|
-
Non-persistable values (e.g. HTML elements, DOM refs)
|
|
113
|
+
Non-persistable values (e.g. HTML elements, DOM refs) are session scope — spread
|
|
114
|
+
`Scope.session` (`nonPersistent` + `nonShared`) into the schema.
|
|
112
115
|
|
|
113
116
|
```ts
|
|
114
117
|
components: {
|
|
115
118
|
layout: Layout.schema,
|
|
116
|
-
layoutElement: { default: null as
|
|
119
|
+
layoutElement: { default: null as HTMLElement | null, ...Scope.session },
|
|
117
120
|
},
|
|
118
121
|
```
|
|
119
122
|
|
|
120
123
|
### resources
|
|
121
124
|
|
|
122
|
-
Use `as Type` to
|
|
125
|
+
Use `as Type` to widen a literal default to its type. A resource initialized later
|
|
126
|
+
(in a system initializer) defaults to `null` widened to `Type | null`.
|
|
123
127
|
|
|
124
128
|
```ts
|
|
125
129
|
resources: {
|
|
126
130
|
themeColor: { default: 'dark' as ThemeColor },
|
|
127
|
-
connection: { default: null as
|
|
131
|
+
connection: { default: null as WebSocket | null, ...Scope.session },
|
|
128
132
|
},
|
|
129
133
|
```
|
|
130
134
|
|
|
@@ -156,7 +160,7 @@ UI components that call actions must never consume returned values — see `feat
|
|
|
156
160
|
|
|
157
161
|
---
|
|
158
162
|
|
|
159
|
-
## Naming conventions
|
|
163
|
+
## Naming conventions (standalone plugins)
|
|
160
164
|
|
|
161
165
|
| Item | Convention | Example |
|
|
162
166
|
| ----------- | ----------------------------------------------------- | ---------------------------- |
|
package/.claude/rules/element.md
CHANGED
|
@@ -3,6 +3,7 @@ paths:
|
|
|
3
3
|
- '**/*-element.ts'
|
|
4
4
|
- '**/elements/**/*.ts'
|
|
5
5
|
- '**/ui/**/*.ts'
|
|
6
|
+
- '**/ui/**/*.tsx'
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Container element authoring
|
|
@@ -84,9 +85,9 @@ function **by reference**, don't wrap it in an identity arrow:
|
|
|
84
85
|
|
|
85
86
|
```ts
|
|
86
87
|
// ❌ identity wrapper — adds nothing
|
|
87
|
-
setOperation: (op) => this.service.
|
|
88
|
+
setOperation: (op) => this.service.actions.setOperation(op),
|
|
88
89
|
// ✅ same signature, pass it through
|
|
89
|
-
setOperation: this.service.
|
|
90
|
+
setOperation: this.service.actions.setOperation,
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
Only write an arrow when you genuinely adapt the call (drop an event
|
|
@@ -117,14 +118,14 @@ transaction, or hook.
|
|
|
117
118
|
| Framework | Container is | Subscription hook | Locator inputs | Render delegation |
|
|
118
119
|
| --------- | ------------------------ | --------------------- | -------------------- | ------------------------------------------------ |
|
|
119
120
|
| Lit | `DatabaseElement<P>` | `useObservableValues` | `@property` only | `presentation.render({ ...values, ...callbacks })` |
|
|
120
|
-
| React | function component | `useObservableValues` | function arguments |
|
|
121
|
-
| Solid | component function | framework-specific | function arguments |
|
|
121
|
+
| React | function component | `useObservableValues` | function arguments | `presentation.render({ ...values, ...callbacks })` |
|
|
122
|
+
| Solid | component function | framework-specific | function arguments | `presentation.render({ ...values, ...callbacks })` |
|
|
122
123
|
|
|
123
124
|
Lit-only: `static styles = styles` from a sibling `*.css.ts`. `@property`
|
|
124
125
|
exists *only* for locator inputs — never for state, never for derived
|
|
125
126
|
values, never for parent-forwarded flags. The `P` in `DatabaseElement<P>` (and
|
|
126
127
|
the `get plugin()` it returns) is the surface the element *consumes* — never the
|
|
127
|
-
varying topmost-layer name (see `features/
|
|
128
|
+
varying topmost-layer name (see `features/ecs/index.md`): `typeof
|
|
128
129
|
MainService.plugin` for a self-contained feature. **Exception:** an element
|
|
129
130
|
meant to be *extended* — injected with various databases that build on this
|
|
130
131
|
feature (a peer app that adds an agent, a p2p/presence build) — types on the
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/data/components/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# data/components/ — one component schema per file
|
|
7
|
+
|
|
8
|
+
A component is a named column of entity state. Each lives in its own file, with a
|
|
9
|
+
single export: its schema. The file owns that schema object, and every other feature
|
|
10
|
+
reuses it by importing the object, since the ECS compares schemas by identity.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// components/name.ts — document scope: a value type's schema, re-exported by identity
|
|
14
|
+
export const name = Name.schema;
|
|
15
|
+
|
|
16
|
+
// components/drag-position.ts — session scope: flags spread in the file
|
|
17
|
+
export const dragPosition = { ...DragPosition.schema, ...Scope.session } satisfies Schema;
|
|
18
|
+
|
|
19
|
+
// components/todo.ts — a tag
|
|
20
|
+
export const todo = True.schema;
|
|
21
|
+
|
|
22
|
+
// components/index.ts — the feature's vocabulary
|
|
23
|
+
export const components = { todo, name, complete, order, dragPosition };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Scope** is declared per file with `Scope` from `@adobe/data/ecs`:
|
|
27
|
+
|
|
28
|
+
| scope | shared | durable | spread |
|
|
29
|
+
|---|:-:|:-:|---|
|
|
30
|
+
| document | yes | yes | nothing (the default) |
|
|
31
|
+
| settings | no | yes | `...Scope.settings` |
|
|
32
|
+
| presence | yes | no | `...Scope.presence` |
|
|
33
|
+
| session | no | no | `...Scope.session` |
|
|
34
|
+
|
|
35
|
+
On a component, the flags scope the **column**. An entity's own scope comes from
|
|
36
|
+
its marker components (`entities.md`).
|
|
37
|
+
- **Every column lives here**, including implementation-only ones (a tag, a drag
|
|
38
|
+
offset). `ecs/core/` only chooses how they are packed.
|
|
39
|
+
- **A tag** is a component with `True.schema`. Add one only when an entity's
|
|
40
|
+
component set would otherwise be contained in another's (`entities.md`).
|
|
41
|
+
- **The name describes the role**, the schema the shape (`position` of a `Vec3`).
|
|
42
|
+
- **Reserved names:** `id`, `nonPersistent`, `nonShared` (built into the ECS), and
|
|
43
|
+
`index` (it would collide with the `index.ts` barrel).
|
|
44
|
+
- **A feature built on another** spreads the lower barrel first and pins that its own
|
|
45
|
+
names don't shadow it (a spread would overwrite silently):
|
|
46
|
+
```ts
|
|
47
|
+
export const components = { ...mainComponents, user, assignees };
|
|
48
|
+
type _NoShadow = Assert<Equal<Extract<"user" | "assignees", keyof typeof mainComponents>, never>>;
|
|
49
|
+
```
|
|
50
|
+
- The barrel never includes the built-in `nonPersistent` / `nonShared` markers; the
|
|
51
|
+
ECS reserves those names and throws.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/data/entities/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# data/entities/ — entities as component tuples
|
|
7
|
+
|
|
8
|
+
An entity is declared by the components it has. Its value type is the aggregate of
|
|
9
|
+
those components' types, derived rather than written:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// entities/todo.ts
|
|
13
|
+
export const Todo = ["name", "complete", "order"] as const satisfies Database.EntityComponents<typeof components>;
|
|
14
|
+
export type Todo = Database.EntityType<typeof components, typeof Todo>;
|
|
15
|
+
// → { readonly name: string; readonly complete: boolean; readonly order: number }
|
|
16
|
+
|
|
17
|
+
// entities/index.ts
|
|
18
|
+
export const entities = { Todo };
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The same-named `const` and `type` pair counts as one export.
|
|
22
|
+
|
|
23
|
+
- **Declare the spec-visible entity.** Implementation-only columns (a tag, a drag
|
|
24
|
+
offset) are added when `ecs/core/` packs the archetype.
|
|
25
|
+
- **An entity's kind is its component set.** Queries match every entity that has *at
|
|
26
|
+
least* the listed components. So when one entity's set is contained in another's
|
|
27
|
+
(`User = [name]` inside `Todo = [name, complete, order]`), give one of them a tag
|
|
28
|
+
(`User = ["user", "name"]`) or query with `exclude`.
|
|
29
|
+
- **Entity scope** comes from the built-in marker components in the tuple:
|
|
30
|
+
`["cursor", "position", "nonPersistent", "nonShared"]` is a session entity. Markers
|
|
31
|
+
are omitted from the value type; tags are kept.
|
|
32
|
+
- **A feature built on another** may declare an entity over the lower feature's
|
|
33
|
+
components, e.g. `AssignedTodo = ["name", "complete", "order", "assignees"]`.
|
|
34
|
+
- **The entity takes the name.** When a value type has the same shape, don't declare
|
|
35
|
+
both: helpers on its fields go on the field types' `values/` namespaces.
|
|
36
|
+
- The archetype model (supersets, base archetypes, row iteration) is in the
|
|
37
|
+
rules-root `archetypes.md`.
|
|
@@ -3,79 +3,27 @@ paths:
|
|
|
3
3
|
- '**/features/*/data/**/*.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# data/ — the
|
|
6
|
+
# data/ — the feature's pure Data declarations
|
|
7
7
|
|
|
8
|
-
The foundation layer
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
(
|
|
13
|
-
`
|
|
14
|
-
**transitions** over those types are less restricted: when one injects a service
|
|
15
|
-
it may import freely from `services/` — the service type and any utilities its
|
|
16
|
-
namespace exposes (see `state.md`). What it must **never** touch is `ui/`:
|
|
17
|
-
`data/` never depends on presentation — that isolation is strict and inviolable.
|
|
18
|
-
Only *services themselves* are never authored here; that is what makes this the
|
|
19
|
-
value-type layer.
|
|
8
|
+
The foundation layer. Everything here is a declaration about plain, serializable
|
|
9
|
+
Data: JSON primitives/arrays/objects plus `ReadonlySet`, `ReadonlyMap`, and `Blob`
|
|
10
|
+
(serialize with `Data.stringify` / `Data.parse`). No functions in values, no services,
|
|
11
|
+
no ECS machinery. The one exception is a session-scoped resource holding a runtime
|
|
12
|
+
handle (a canvas, a live database): `{ default: null as Handle | null, ...Scope.session }`.
|
|
13
|
+
It depends only on `@adobe/data` and the `data/` of features this one builds on.
|
|
20
14
|
|
|
21
|
-
Each data type is its own namespace folder (see `global/namespace.md`), holding
|
|
22
|
-
its hand-authored type, its (optional, matching) schema, and its pure
|
|
23
|
-
synchronous helpers together:
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
data/player-mark/
|
|
27
|
-
player-mark.ts # HAND-AUTHORED type + `export * as PlayerMark from "./public.js"`
|
|
28
|
-
schema.ts # (optional / later) schema matching the type, pinned via Assert<Equal<…>>
|
|
29
|
-
public.ts # re-exports schema + helpers
|
|
30
|
-
is.ts values.ts opponent.ts … # one pure helper per file
|
|
31
15
|
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- The **schema is derived from the type, not the reverse.** It is written to
|
|
38
|
-
match the type and **pinned** to it with a compile-time assertion so the two
|
|
39
|
-
cannot drift (see below). A field typed as a branded primitive reuses that
|
|
40
|
-
type's schema — `F32` → `F32.schema`, `Vec3` → `Vec3.schema`.
|
|
41
|
-
- **Schemas are optional until a runtime boundary needs one.** A pure `data/`
|
|
42
|
-
type is complete with `<type>.ts` alone. The schema is added only when
|
|
43
|
-
persistence, the wire, or an ECS component/resource requires it — and is
|
|
44
|
-
typically written by the agent in a later phase (e.g. when implementing the
|
|
45
|
-
ECS), not by the human up front.
|
|
46
|
-
- Helpers are synchronous and pure; each has a sibling `*.test.ts`.
|
|
47
|
-
- Objects of only 32-bit numeric values / sub-structs: the schema uses
|
|
48
|
-
`Schema.fromStructProperties` (guarantees a valid struct schema, stored in
|
|
49
|
-
linear memory).
|
|
50
|
-
|
|
51
|
-
## Pinning the schema to the type
|
|
52
|
-
|
|
53
|
-
The type is written first and by hand; the schema is written to match it and
|
|
54
|
-
is locked to it at compile time, so schema authoring (whenever it happens)
|
|
55
|
-
cannot silently drift from the type readers rely on.
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
// player-mark/player-mark.ts — HAND-AUTHORED, the source of truth
|
|
59
|
-
export type PlayerMark = 'X' | 'O';
|
|
60
|
-
export * as PlayerMark from './public.js';
|
|
61
|
-
|
|
62
|
-
// player-mark/schema.ts — written to MATCH the type (often later, by the agent)
|
|
63
|
-
import { Schema } from '@adobe/data/schema';
|
|
64
|
-
import type { Assert, Equal } from '@adobe/data/types';
|
|
65
|
-
import type { PlayerMark } from './player-mark.js';
|
|
66
|
-
|
|
67
|
-
export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema;
|
|
68
|
-
|
|
69
|
-
// Compile-time pin: fails to build if schema and type diverge.
|
|
70
|
-
type _Pin = Assert<Equal<Schema.ToType<typeof schema>, PlayerMark>>;
|
|
16
|
+
data/
|
|
17
|
+
values/<type>/ Data types + pure helpers (values.md)
|
|
18
|
+
components/<name>.ts one component schema each (components.md)
|
|
19
|
+
resources/<name>.ts one resource schema each (resources.md)
|
|
20
|
+
entities/<entity>.ts component-name tuples (entities.md)
|
|
71
21
|
```
|
|
72
22
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
hand-authored `PlayerMark`.
|
|
23
|
+
Order within `data/`: `values → components / resources → entities`. A value never
|
|
24
|
+
imports a component, resource or entity. Where a value helper needs an entity's
|
|
25
|
+
fields, it takes them structurally
|
|
26
|
+
(`Iterable<{ readonly mark: PlayerMark; readonly cellIndex: CellIndex }>`).
|
|
78
27
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
the individual type folders here are its building blocks.
|
|
28
|
+
Components and resources share **one namespace** across every feature in an app:
|
|
29
|
+
a name is one column or one resource, never both, and has one owning feature.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/data/resources/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# data/resources/ — one resource schema per file
|
|
7
|
+
|
|
8
|
+
A resource is a singleton value: one per database. Each lives in its own file, with a
|
|
9
|
+
single export: its schema, which must carry a `default`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// resources/display-completed.ts
|
|
13
|
+
export const displayCompleted = { ...Boolean.schema, ...Scope.settings } satisfies ResourceSchema;
|
|
14
|
+
|
|
15
|
+
// resources/selected-todo.ts — an entity reference
|
|
16
|
+
export const selectedTodo = { ...Entity.schema, default: Entity.none, ...Scope.session } satisfies ResourceSchema;
|
|
17
|
+
|
|
18
|
+
// resources/count.ts — a primitive needs no values/ type
|
|
19
|
+
export const count = { type: "number", default: 0 } as const satisfies ResourceSchema;
|
|
20
|
+
|
|
21
|
+
// resources/index.ts
|
|
22
|
+
export const resources = { displayCompleted, selectedTodo };
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **`satisfies ResourceSchema`** checks the `default` at the declaration.
|
|
26
|
+
- **Scope** is spread from `Scope`, as for components (`components.md`). On a resource
|
|
27
|
+
the flags also place its singleton entity in that quadrant, so a `session` resource
|
|
28
|
+
resets on reload and a `settings` one stays on the device.
|
|
29
|
+
- **An entity reference** spreads `Entity.schema`. The `entity` mark is how
|
|
30
|
+
conformance knows the value is an id.
|
|
31
|
+
- Every singleton in the spec's `State` is a resource of the same name
|
|
32
|
+
(`spec/index.md`), including a single game object like a ship or player.
|
|
33
|
+
- A resource and a component never share a name (`index.md`).
|
|
34
|
+
- There is only one instance, so never choose a struct schema for efficiency here.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/data/values/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# data/values/ — Data types and pure helpers
|
|
7
|
+
|
|
8
|
+
Each Data type is its own namespace folder (`global/namespace.md`): its hand-written
|
|
9
|
+
type, its schema pinned to it, and its pure synchronous helpers.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
data/values/player-mark/
|
|
13
|
+
player-mark.ts # HAND-WRITTEN type + `export * as PlayerMark from "./public.js"`
|
|
14
|
+
schema.ts # schema matching the type, pinned with Assert<Equal<…>>
|
|
15
|
+
public.ts # re-exports schema + helpers
|
|
16
|
+
opponent.ts opponent.test.ts … # one pure helper per file, each tested
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Write the type by hand** in `<type>.ts`. It is the readable source of truth; use
|
|
20
|
+
the richest types available (`F32`, `Vec3`, unions, branded aliases).
|
|
21
|
+
- **Write the schema to match, and pin it**, so the two can't drift:
|
|
22
|
+
```ts
|
|
23
|
+
// schema.ts
|
|
24
|
+
export const schema = { type: "string", enum: ["X", "O"] } as const satisfies Schema;
|
|
25
|
+
type _Pin = Assert<Equal<Schema.ToType<typeof schema>, PlayerMark>>;
|
|
26
|
+
```
|
|
27
|
+
`Schema.ToType` yields `readonly` properties and arrays, so write the type
|
|
28
|
+
`readonly` too. `Schema.ToType` appears only in the pin, never as the exported type.
|
|
29
|
+
- **Add a schema when something needs one**: a component, a resource, persistence, or
|
|
30
|
+
the wire. A type used only by the spec may have none.
|
|
31
|
+
- **Helpers are synchronous and pure**, each with a sibling `*.test.ts`. They are the
|
|
32
|
+
home for logic the spec and the ECS share.
|
|
33
|
+
- **A helper-only namespace** (`motion`, `collision`) with no type of its own also
|
|
34
|
+
lives here.
|
|
35
|
+
- Objects of only 32-bit numeric values or sub-structs use
|
|
36
|
+
`Schema.fromStructProperties` (linear-memory storage).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/actions/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/actions/ — the operations the app performs
|
|
7
|
+
|
|
8
|
+
One action per file: a function taking the whole database and Data args. Actions are
|
|
9
|
+
the app-facing operations: the UI calls `service.actions.*`, and each spec action
|
|
10
|
+
conforms against the action of the same name.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
export const createRandomTodo = async (db: ServiceDatabase) => {
|
|
14
|
+
const timing = await db.services.analytics.randomTodoRequested();
|
|
15
|
+
const name = await db.services.nameGenerator.generateName();
|
|
16
|
+
db.transactions.createTodo({ name });
|
|
17
|
+
db.services.analytics.randomTodoAdded({ timing, name });
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **Every spec action has a same-named action**, even when it only forwards to one
|
|
22
|
+
transaction. When the UI drives a richer op (a streamed drag), add a thin action for
|
|
23
|
+
the spec op as well (`reorderTodo` beside `dragTodo`).
|
|
24
|
+
- **Type `db` on the lowest layer** exposing what it calls, usually `ServiceDatabase`.
|
|
25
|
+
Never the action layer itself.
|
|
26
|
+
- **Call services here**: await ports, sequence calls, time a slow call. Then commit
|
|
27
|
+
through **at most one transaction**, so undo/redo stays one step per operation.
|
|
28
|
+
- **Fire-and-forget.** Results flow back through observables, never return values.
|
|
29
|
+
- **Read current state synchronously** from the store (`db.resources`, `db.read`,
|
|
30
|
+
`db.select`), never from a cached computed, which refreshes only on commit.
|
|
31
|
+
- The `index.ts` barrel feeds the `actions` facet.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ecs/computed/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ecs/computed/ — derived observable values
|
|
7
|
+
|
|
8
|
+
One derived value per file: a `cached` function of a database layer returning an
|
|
9
|
+
`Observe`. Each spec derivation conforms against the computed of the same name.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export const status = cached((db: IndexDatabase) =>
|
|
13
|
+
Observe.withFilter(db.observe.resources.board, BoardState.deriveStatus),
|
|
14
|
+
);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Wire `data/values/` helpers** to the minimal observables they read. The spec
|
|
18
|
+
derivation calls the same helper, so the two agree by construction. Never import
|
|
19
|
+
from `spec/`.
|
|
20
|
+
- **Observe every input.** A computed that reads two resources must observe both, or
|
|
21
|
+
it goes stale when only one changes.
|
|
22
|
+
- **Performance picks the wiring.** Hand-wire minimal resource or index reads on hot
|
|
23
|
+
or large paths. For "how many" use `db.observe.count(components, { where })`, not a
|
|
24
|
+
selected array's `.length`.
|
|
25
|
+
- **An entity-list computed emits ids.** Name it in the implementation's `hydrate`, so
|
|
26
|
+
conformance maps each id through `toData`.
|
|
27
|
+
- A computed with no spec derivation (one helper applied to one field) is covered by
|
|
28
|
+
that helper's unit test.
|
|
29
|
+
- **Type `db` on the lowest layer** that exposes what it reads. A computed may read
|
|
30
|
+
service observables (`db.services.*`), since services sit below it.
|
|
31
|
+
- The `index.ts` barrel feeds the `computed` facet.
|