@adobe/data-ai 0.10.21 → 0.10.23

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.
Files changed (65) hide show
  1. package/.claude/rules/README.md +26 -31
  2. package/.claude/rules/app.md +46 -0
  3. package/.claude/rules/archetypes.md +5 -3
  4. package/.claude/rules/data-modelling.md +8 -7
  5. package/.claude/rules/ecs.md +11 -7
  6. package/.claude/rules/element.md +6 -5
  7. package/.claude/rules/features/data/components.md +51 -0
  8. package/.claude/rules/features/data/entities.md +37 -0
  9. package/.claude/rules/features/data/index.md +18 -70
  10. package/.claude/rules/features/data/resources.md +34 -0
  11. package/.claude/rules/features/data/values.md +36 -0
  12. package/.claude/rules/features/ecs/actions.md +31 -0
  13. package/.claude/rules/features/ecs/computed.md +31 -0
  14. package/.claude/rules/features/ecs/conformance.md +85 -0
  15. package/.claude/rules/features/ecs/core.md +43 -0
  16. package/.claude/rules/features/ecs/index.md +72 -0
  17. package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
  18. package/.claude/rules/features/ecs/services.md +36 -0
  19. package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
  20. package/.claude/rules/features/ecs/transactions.md +32 -0
  21. package/.claude/rules/features/index.md +104 -150
  22. package/.claude/rules/features/services/index.md +52 -84
  23. package/.claude/rules/features/spec/index.md +161 -0
  24. package/.claude/rules/features/ui/binding-element.md +5 -5
  25. package/.claude/rules/features/ui/index.md +8 -3
  26. package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
  27. package/.claude/rules/features/ui/presentation-test.md +1 -0
  28. package/.claude/rules/features/ui/presentation.md +1 -0
  29. package/.claude/rules/global/function-references.md +2 -2
  30. package/.claude/rules/global/namespace.md +3 -3
  31. package/.claude/rules/global/react.md +4 -4
  32. package/.claude/rules/lit.md +2 -2
  33. package/.claude/rules/plugin-modelling.md +2 -0
  34. package/.claude/rules/service.md +2 -0
  35. package/.claude/rules/versioning.md +8 -6
  36. package/.claude-plugin/plugin.json +1 -1
  37. package/README.md +8 -0
  38. package/bin/cli.mjs +24 -8
  39. package/package.json +3 -3
  40. package/skills/build-actions/SKILL.md +7 -8
  41. package/skills/build-app-entry/SKILL.md +12 -8
  42. package/skills/build-computed/SKILL.md +8 -7
  43. package/skills/build-core-database/SKILL.md +18 -15
  44. package/skills/build-data/SKILL.md +14 -13
  45. package/skills/build-feature/SKILL.md +2 -1
  46. package/skills/build-game/SKILL.md +6 -4
  47. package/skills/build-indexes/SKILL.md +6 -7
  48. package/skills/build-service-database/SKILL.md +8 -8
  49. package/skills/build-services/SKILL.md +9 -7
  50. package/skills/build-spec/SKILL.md +23 -0
  51. package/skills/build-systems/SKILL.md +12 -18
  52. package/skills/build-transactions/SKILL.md +8 -8
  53. package/skills/build-ui/SKILL.md +5 -4
  54. package/skills/review/SKILL.md +3 -3
  55. package/.claude/rules/features/data/state.md +0 -209
  56. package/.claude/rules/features/services/main-service/actions.md +0 -70
  57. package/.claude/rules/features/services/main-service/archetypes.md +0 -49
  58. package/.claude/rules/features/services/main-service/components.md +0 -41
  59. package/.claude/rules/features/services/main-service/computed.md +0 -82
  60. package/.claude/rules/features/services/main-service/conformance.md +0 -95
  61. package/.claude/rules/features/services/main-service/core-database.md +0 -48
  62. package/.claude/rules/features/services/main-service/index.md +0 -187
  63. package/.claude/rules/features/services/main-service/resources.md +0 -28
  64. package/.claude/rules/features/services/main-service/services.md +0 -25
  65. package/.claude/rules/features/services/main-service/transactions.md +0 -59
@@ -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 the four feature layers one-to-one, so the
11
- guidance for a folder lives at the same path shape as the code it governs. A
12
- folder's own `index.md` holds just enough to understand that folder; each
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 feature layering as a whole (ui → services → data)
18
+ index.md # the layering as a whole: data → services → spec | ecs → ui → app
20
19
  data/
21
- index.md # data-type namespaces (foundation)
22
- state.md # the State aggregate: pure transforms & derivations
23
- services/
24
- index.md # the services layer: main-service + capability contracts
25
- main-service/ # the feature service (ECS implementation)
26
- index.md # the service + its layered database plugins
27
- components.md resources.md archetypes.md
28
- computed.md indexes.md transactions.md services.md actions.md
29
- systems.md conformance.md
30
- ui/index.md # UI (points to element / presentation / lazy-element file rules)
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 (`namespace.md`, `cohesion.md`, `type-casts.md`,
34
- `function-references.md`, `react.md`) are **intentionally repo-wide** — their
35
- `paths:` globs (`**/*.ts` etc.) apply everywhere, by design, so the folder name
36
- makes that explicit. Alongside `features/`, the remaining rules-root `.md` files
37
- hold the other **cross-cutting** patterns the feature rules reference —
38
- `data-modelling.md`, `archetypes.md` (row iteration), `plugin-modelling.md`,
39
- `versioning.md` (schema history & upgrade-on-load), and
40
- the UI file rules (`element.md`, `lazy-element.md`, `presentation.md`). They live
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 only once an application opts into the feature-folder pattern
46
- (source organized under `features/<name>/`). Editing
47
- `features/<name>/services/main-service/core-database/components.ts` pulls in both
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
- `core-database/archetypes.ts` *file* conventions live in
13
- `features/services/main-service/archetypes.md`.
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/*.ts'
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/data/state.md`). Also used for deterministic-key lookups (an enum, a
58
- name, a stable string).
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* — the single
63
- `ReadonlyMap<number, …>` store is the only home for entities, and queries return
64
- their **ids** (`ReadonlySet<number>` unordered, `ReadonlyArray<number>` ordered).
65
- See `features/data/state.md` for the full `State` shape.
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`
@@ -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` | `{ default: value as Type }` | Global resource schemas |
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. Use **only** `{ default: value as Type }`.
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) must use `ephemeral: true` on the schema.
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 unknown as HTMLElement, ephemeral: true },
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 provide the compile-time type. Use `null as unknown as Type` for resources initialized later in a system initializer.
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 unknown as WebSocket },
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
  | ----------- | ----------------------------------------------------- | ---------------------------- |
@@ -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.transactions.setOperation(op),
88
+ setOperation: (op) => this.service.actions.setOperation(op),
88
89
  // ✅ same signature, pass it through
89
- setOperation: this.service.transactions.setOperation,
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 | `<Presentation {...values} {...callbacks} />` |
121
- | Solid | component function | framework-specific | function arguments | `<Presentation {...values} {...callbacks} />` |
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/services/main-service/index.md`): `typeof
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 data model
6
+ # data/ — the feature's pure Data declarations
7
7
 
8
- The foundation layer: the feature's **data types** and the pure declarations
9
- over them. A data type is a readonly, serializable value suitable for persistence
10
- and for communication over the wire — no functions, no handles, just plain data:
11
- JSON primitives/arrays/objects plus `ReadonlySet`, `ReadonlyMap`, and `Blob`
12
- (serialize a Set/Map-bearing value with `Data.stringify` / `Data.parse`). A `data/` **type** depends on nothing but `@adobe/data` and other
13
- `data/` types, and needs no knowledge of anything built on top of it. The
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
- - The **type is authored by hand** in `<type>.ts` — the readable source of
34
- truth. Write it with the richest types available (`F32`, `Vec3`, unions,
35
- branded aliases) so members carry their semantic meaning and get strong
36
- IntelliSense / hover docs.
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
- The `Assert<Equal<…>>` line is the whole safety net: `Schema.ToType<typeof
74
- schema>` must be exactly the hand-authored type (including readonly/optional),
75
- or the file does not compile. `Schema.ToType` is used **only** inside the
76
- assertion — never as the exported type. Consumers always import the
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
- One folder is special: **`data/state/`** holds the feature's single `State`
80
- aggregate and its pure transforms/derivations (its own rule, `state.md`);
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.