@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
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: build-spec
3
+ description: Build a feature's spec/ — the State, its actions and derivations, and their cases. Test tier.
4
+ input: feature
5
+ output: feature
6
+ ---
7
+
8
+ Create the feature's `spec/` folder, the specification the ECS will be conformed to:
9
+
10
+ - `state.ts` — `State` built from `data/` types (singletons named for resources,
11
+ `ReadonlyMap` entity collections), pinned with `Conformance.StateMatches`; `create.ts`,
12
+ `samples.ts`, `public.ts`.
13
+ - One file per **action** (each operation the app performs) and per **derivation**
14
+ (each derived value it shows), each with its inert `*.cases.ts`. A real-time feature
15
+ adds `systems.ts`: one pure function per ECS system it will have, named the same,
16
+ each with cases, plus `noOp` states every system leaves unchanged.
17
+ - `transforms.ts` (the barrel of every action and derivation), `spec.ts`
18
+ (`Conformance.spec`) and `spec.test.ts` (`Conformance.checkSpec`).
19
+
20
+ Build one action or derivation plus its cases at a time. Logic the ECS will share goes
21
+ into `data/values/` helpers, never here. Gate: `checkSpec` passes.
22
+
23
+ The how is in the auto-loading `features/spec/index.md` rule.
@@ -1,27 +1,21 @@
1
1
  ---
2
2
  name: build-systems
3
- description: Build a feature's system-database — the real-time tick loop (systems the scheduler runs each frame). For real-time features only.
3
+ description: Build a real-time feature's ecs/systems layer — the per-frame tick loop. Optional.
4
4
  input: feature
5
5
  output: feature
6
6
  ---
7
7
 
8
- Skip if this feature doesn't contain or require systems.
9
- Most games that use a canvas will use systems but most applications will not.
8
+ Skip unless the feature is real-time (games, simulations).
10
9
 
11
- Create `services/main-service/system-database/system-database.ts`: `Database.Plugin.create({ extends:
12
- Database.Plugin.combine(<currentTop>.plugin, scheduler), systems })` — combine `scheduler` with the
13
- feature's **current top** main-service layer (`ActionDatabase` / `ServiceDatabase` / `ComputedDatabase`,
14
- whichever it built), never a hardcoded `ComputedDatabase`, so systems *and* any services/actions
15
- compose into the one `MainService`. The `systems`
16
- map is declared **inline** (see `features/services/main-service/systems.md` — inline is required for `db` to be
17
- typed and for system-name inference; a `systems/` folder is optional, only for extracted
18
- per-frame body helpers).
10
+ Create `ecs/systems/system-database.ts`: `Database.Plugin.create({ extends:
11
+ Database.Plugin.combine(<currentTop>.plugin, scheduler), systems: { … } })`, with the
12
+ `systems` map declared **inline** so `db` and the system names are inferred. Hot
13
+ per-row work writes columns in place; discrete events dispatch transactions. Order
14
+ systems under `schedule`; it is the only statement of order.
19
15
 
20
- - A system is a `SystemDeclaration`: `{ create, schedule?: { before?, after?, during? } }`.
21
- `create(db)` runs once (capture queries / index handles / closures); the returned function
22
- advances the world one tick. Return `void` for an init-only system (seeding entities).
23
- - Order under `schedule` (mirror the `data/` step's internal sequence); drive the loop by
24
- combining `scheduler` (rAF), gated by the `schedulerState` resource.
16
+ Add `frame: { args, unmodelled?, cases? }` to the implementation: a writer per frame
17
+ arg, each system the spec can't model (wasm, init-only) with its reason, and
18
+ whole-frame cases in `ecs/conformance/frame.cases.ts`. Gate: every
19
+ system's own cases and the frame cases pass.
25
20
 
26
- **Only for real-time features** (games, sims) — turn-based features skip this phase entirely.
27
- Comes after `build-computed`. The how is in the auto-loading `features/services/main-service/systems.md` rule.
21
+ The how is in the auto-loading `features/ecs/systems.md` rule.
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: build-transactions
3
- description: Build a feature's transaction-database — atomic mutations over the store.
3
+ description: Build a feature's ecs/transactions layer.
4
4
  input: feature
5
5
  output: feature
6
6
  ---
7
7
 
8
- Create `services/main-service/transaction-database/`: `transaction-database.ts` (extends the
9
- preceding layer — `IndexDatabase` if the feature built indexes, else `CoreDatabase` — adds
10
- `transactions` from `./transactions/index.js`) plus a `transactions/` folder — one mutation per
11
- file + barrel. Type the store param `CoreDatabase.Store` (entities/resources/archetypes) or
12
- `IndexDatabase.Store` (reads an index).
8
+ Create `ecs/transactions/`: `transaction-database.ts` (extends the previous layer, adds
9
+ `transactions` from `./index.js`), one mutation per file, and a barrel. Type the store
10
+ `CoreDatabase.Store`, or `IndexDatabase.Store` once it reads an index. Guard by entity
11
+ kind, and apply `data/values/` helpers; never import `spec/`.
13
12
 
14
- Comes after `index-database` (or `core-database` if no indexes).
13
+ Build the transactions the spec actions will need. They are conformed through the
14
+ actions that call them, so their gate is `build-actions`; add any missing one there.
15
15
 
16
- The how is in the auto-loading `features/services/main-service/transactions.md` rule.
16
+ The how is in the auto-loading `features/ecs/transactions.md` rule.
@@ -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.