@adobe/data-ai 0.10.23 → 0.10.25

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 (41) hide show
  1. package/.claude/rules/README.md +8 -3
  2. package/.claude/rules/ecs.md +1 -1
  3. package/.claude/rules/element.md +9 -0
  4. package/.claude/rules/features/data/components.md +6 -3
  5. package/.claude/rules/features/data/index.md +4 -1
  6. package/.claude/rules/features/data/state.md +211 -0
  7. package/.claude/rules/features/ecs/conformance.md +12 -6
  8. package/.claude/rules/features/index.md +4 -0
  9. package/.claude/rules/features/services/index.md +2 -0
  10. package/.claude/rules/features/services/main-service/actions.md +72 -0
  11. package/.claude/rules/features/services/main-service/archetypes.md +51 -0
  12. package/.claude/rules/features/services/main-service/components.md +43 -0
  13. package/.claude/rules/features/services/main-service/computed.md +84 -0
  14. package/.claude/rules/features/services/main-service/conformance.md +97 -0
  15. package/.claude/rules/features/services/main-service/core-database.md +50 -0
  16. package/.claude/rules/features/services/main-service/index.md +189 -0
  17. package/.claude/rules/features/services/main-service/indexes.md +65 -0
  18. package/.claude/rules/features/services/main-service/resources.md +30 -0
  19. package/.claude/rules/features/services/main-service/services.md +27 -0
  20. package/.claude/rules/features/services/main-service/systems.md +145 -0
  21. package/.claude/rules/features/services/main-service/transactions.md +61 -0
  22. package/.claude/rules/features/ui/index.md +2 -0
  23. package/.claude/rules/service.md +25 -75
  24. package/.claude-plugin/plugin.json +1 -1
  25. package/package.json +3 -3
  26. package/skills/build-actions/SKILL.md +2 -0
  27. package/skills/build-app-entry/SKILL.md +2 -0
  28. package/skills/build-computed/SKILL.md +2 -0
  29. package/skills/build-core-database/SKILL.md +2 -0
  30. package/skills/build-data/SKILL.md +2 -0
  31. package/skills/build-feature/SKILL.md +3 -0
  32. package/skills/build-indexes/SKILL.md +2 -0
  33. package/skills/build-service-database/SKILL.md +2 -0
  34. package/skills/build-services/SKILL.md +2 -0
  35. package/skills/build-spec/SKILL.md +2 -0
  36. package/skills/build-systems/SKILL.md +2 -0
  37. package/skills/build-transactions/SKILL.md +2 -0
  38. package/skills/build-ui/SKILL.md +2 -0
  39. package/skills/review/SKILL.md +2 -0
  40. package/skills/structure/SKILL.md +2 -0
  41. package/.claude/rules/features/ui/binding-element.md +0 -226
@@ -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 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/`.
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>/`.
@@ -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 `features/ui/binding-element.md` (Actions are fire-and-forget).
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
 
@@ -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/ecs`:
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` (built into the ECS), and
43
- `index` (it would collide with the `index.ts` barrel).
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 has one owning feature.
31
+ a name is one column or one resource, never both (the store throws on a clash), 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
- ## Escape hatch: per-case ambient context
79
+ ## Per-case ambient context
80
80
 
81
- A feature whose action reads ambient per-peer context (a `userId` seeded under a
82
- custom concurrency, as in p2p presence) can't use `checkFeature`. It drives
83
- `Conformance.runActions` directly, feeding it
84
- `Conformance.adaptCases(spec.fns, spec.cases, spec.services, true)` with its own
85
- `makeDb` and `seedContext`. This is the only sanctioned use of the lower-level runners.
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.