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