@adobe/data-ai 0.10.23 → 0.10.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/README.md +8 -3
- package/.claude/rules/ecs.md +1 -1
- package/.claude/rules/element.md +9 -0
- package/.claude/rules/features/data/components.md +6 -3
- package/.claude/rules/features/data/index.md +4 -1
- package/.claude/rules/features/data/state.md +211 -0
- package/.claude/rules/features/ecs/conformance.md +12 -6
- package/.claude/rules/features/index.md +4 -0
- package/.claude/rules/features/services/index.md +2 -0
- package/.claude/rules/features/services/main-service/actions.md +72 -0
- package/.claude/rules/features/services/main-service/archetypes.md +51 -0
- package/.claude/rules/features/services/main-service/components.md +43 -0
- package/.claude/rules/features/services/main-service/computed.md +84 -0
- package/.claude/rules/features/services/main-service/conformance.md +97 -0
- package/.claude/rules/features/services/main-service/core-database.md +50 -0
- package/.claude/rules/features/services/main-service/index.md +189 -0
- package/.claude/rules/features/services/main-service/indexes.md +65 -0
- package/.claude/rules/features/services/main-service/resources.md +30 -0
- package/.claude/rules/features/services/main-service/services.md +27 -0
- package/.claude/rules/features/services/main-service/systems.md +145 -0
- package/.claude/rules/features/services/main-service/transactions.md +61 -0
- package/.claude/rules/features/ui/index.md +2 -0
- package/.claude/rules/service.md +25 -75
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +2 -0
- package/skills/build-app-entry/SKILL.md +2 -0
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-core-database/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +2 -0
- package/skills/build-feature/SKILL.md +3 -0
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-service-database/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-spec/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +2 -0
- package/skills/build-transactions/SKILL.md +2 -0
- package/skills/build-ui/SKILL.md +2 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/structure/SKILL.md +2 -0
- package/.claude/rules/features/ui/binding-element.md +0 -226
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
> **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-based features only** (a feature with a `data/state/` spec). Test-only: imported
|
|
11
|
+
only by `*.test.ts`, in no facet barrel. The `data/state` cases (authored per
|
|
12
|
+
`../../../data/state.md`, gathered by the `spec.ts` manifest) are the shared truth; the
|
|
13
|
+
`@adobe/data-testing` runner replays them against the ECS. This folder holds only the
|
|
14
|
+
**feature-specific projection** (`projection.ts`) plus one `conformance.test.ts`.
|
|
15
|
+
Reference: `data-lit-tictactoe` (minimal), `data-lit-todo` (entities + `hydrate`).
|
|
16
|
+
|
|
17
|
+
**The projection helpers and `Match` are test-only — never a runtime branch.**
|
|
18
|
+
`fromState`/`toState` rewrite the whole store out-of-band; runtime code reads through
|
|
19
|
+
observables/indexes and writes through transactions.
|
|
20
|
+
|
|
21
|
+
## `projection.ts` — the store ⇄ State mapping (the one per-feature piece)
|
|
22
|
+
|
|
23
|
+
A single aggregated export — three test-only helpers, one `export`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export const projection = { fromState, toState, toData };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `fromState(store, state)` seeds a store to a `State` (clear, insert entities, set
|
|
30
|
+
resources) and **returns the `id → entity` map** it built (`ReadonlyMap<Id, Entity>`).
|
|
31
|
+
The ecs mints its own ids, so the runner turns this map into the resolver that maps a
|
|
32
|
+
case's spec-id to the seeded entity — no feature writes id resolution by hand. Return
|
|
33
|
+
`void` for an index-addressed / singleton feature (ids then resolve to `Entity.none`).
|
|
34
|
+
- `toState(store)` reads the whole store back, built on `toData`.
|
|
35
|
+
- `toData(store, entity)` reads one entity as its `data/` value — the single place the
|
|
36
|
+
ecs↔data mapping lives. Its **return type** is what tells conformance which derivations
|
|
37
|
+
are entity-list computeds. Present only when the feature has entities.
|
|
38
|
+
|
|
39
|
+
## `conformance.test.ts` — one call
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Conformance } from "@adobe/data-testing";
|
|
43
|
+
import { spec } from "../../../data/state/spec.js";
|
|
44
|
+
Conformance.checkFeature(spec);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`checkFeature` replays the manifest's shared cases against the ECS: it pulls the
|
|
48
|
+
transactions/actions off `spec.plugin`, the computeds off `spec.computedPlugin`, seeds
|
|
49
|
+
each case's `before` over `State.create()`, synthesizes each service's recording double
|
|
50
|
+
from `spec.services` + the case's `responses`, round-trips `State.samples` through
|
|
51
|
+
`spec.projection`, and hydrates the id-emitting computeds named in `spec.hydrate` through
|
|
52
|
+
`toData`. It shares `spec` with `spec.test.ts` — same cases, substituted implementation.
|
|
53
|
+
|
|
54
|
+
Everything `checkFeature` needs lives on the manifest (`../../../data/state.md`):
|
|
55
|
+
`plugin`, `computedPlugin`, `projection`, `hydrate`, `services`, `state`, `fns`, `cases`.
|
|
56
|
+
The manifest's compile-time guards mean the two calls (`checkSpec` + `checkFeature`) are
|
|
57
|
+
the whole conformance surface — no per-op wiring, no coverage guard to hand-maintain.
|
|
58
|
+
|
|
59
|
+
- **Name-pairing.** Each ecs op conforms to its **same-named** transition/derivation. An
|
|
60
|
+
op with no same-named transition (infrastructure like `setInput`, a streaming action,
|
|
61
|
+
a richer UI op) is skipped — so when the real op is renamed (`dragTodo`), add a thin
|
|
62
|
+
**same-named** op (`reorderTodo`) for the transition to pair with; never a per-item
|
|
63
|
+
adapter.
|
|
64
|
+
- **Projection round-trip.** When `State.samples` is non-empty the runner adds a
|
|
65
|
+
`toState ∘ fromState ≡ identity` test per sample.
|
|
66
|
+
- **Comparison.** Ordering is carried by the value's type — `ReadonlyArray` positional,
|
|
67
|
+
`ReadonlySet`/`ReadonlyMap` order-independent. Entity ids compare up to an id-bijection
|
|
68
|
+
from the schema-marked spec-ids (no matcher). Float noise is absorbed by
|
|
69
|
+
`match?: { tolerance }` (default `0.01`). A minted scalar uses `Match.anyNumber` /
|
|
70
|
+
`Match.anyString`.
|
|
71
|
+
|
|
72
|
+
## The escape hatch — per-surface `seedContext` / custom concurrency
|
|
73
|
+
|
|
74
|
+
A feature that needs **ambient per-case context differing per surface** — a user-scoped
|
|
75
|
+
`userId` seeded before the raw transaction *and* independently before the action dispatch
|
|
76
|
+
(**p2p presence**) — cannot use `checkFeature` (it has no place to thread two different
|
|
77
|
+
`seedContext`s or a custom concurrency). It drives the lower-level runners directly,
|
|
78
|
+
feeding them the manifest's cases via `Conformance.adaptCases`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Conformance } from "@adobe/data-testing";
|
|
82
|
+
import { spec } from "../../../data/state/spec.js";
|
|
83
|
+
|
|
84
|
+
Conformance.runTransactions({
|
|
85
|
+
createStore, fromState, toState,
|
|
86
|
+
transitions: Conformance.adaptCases(spec.fns, spec.cases, spec.services, false),
|
|
87
|
+
transactions,
|
|
88
|
+
seedContext: (store, _before, args) => seedUserId(store, (args as { mark: string }).mark),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`adaptCases(fns, cases, services, injectServices)` turns the manifest into the discovery
|
|
93
|
+
map the runners consume — `injectServices: false` for the transaction store (a
|
|
94
|
+
transaction never receives services), `true` for the action db. `runActions` /
|
|
95
|
+
`runComputeds` take the same shape and their own `makeDb` / `seedContext` / concurrency.
|
|
96
|
+
This is the **only** sanctioned use of the lower runners; ordinary features use
|
|
97
|
+
`checkFeature`.
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
9
|
+
|
|
10
|
+
A thin assembler: `Database.Plugin.create({ components?, resources?, archetypes? })`
|
|
11
|
+
over the facet files, exposed through the `CoreDatabase` namespace. This is the
|
|
12
|
+
single-file namespace form (one value export — `plugin` — everything else a type;
|
|
13
|
+
see `global/namespace.md`).
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Database, type Store } from "@adobe/data/ecs";
|
|
17
|
+
import { components } from "./components.js";
|
|
18
|
+
import { resources } from "./resources.js";
|
|
19
|
+
import { archetypes } from "./archetypes.js";
|
|
20
|
+
|
|
21
|
+
const coreDatabasePlugin = Database.Plugin.create({ components, resources, archetypes });
|
|
22
|
+
|
|
23
|
+
export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
|
|
24
|
+
// Resolved component map, at module scope so the imported `Store` namespace isn't
|
|
25
|
+
// shadowed by `CoreDatabase.Store` below.
|
|
26
|
+
type CoreComponents = Store.Components<Database.Plugin.ToStore<typeof coreDatabasePlugin>>;
|
|
27
|
+
|
|
28
|
+
export namespace CoreDatabase {
|
|
29
|
+
export const plugin = coreDatabasePlugin;
|
|
30
|
+
/** The store transactions operate on — a store *is* the transaction context. */
|
|
31
|
+
export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
|
|
32
|
+
/** Index-declaration type bound to this database's components. */
|
|
33
|
+
export type Index = Database.Index<CoreComponents>;
|
|
34
|
+
/** A component name, and a component-key tuple — an archetype's defining shape, or the key
|
|
35
|
+
* list of a `count` / `select` query. */
|
|
36
|
+
export type Component = Extract<keyof typeof components, string>;
|
|
37
|
+
export type Archetype = readonly Component[];
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Derive `Component` / `Archetype` from `components`, never the plugin
|
|
42
|
+
|
|
43
|
+
Read the keys straight from the leaf `components` module
|
|
44
|
+
(`Extract<keyof typeof components, string>`). Do **not** derive them from the
|
|
45
|
+
plugin (`Store.Components<ToStore<plugin>>`, i.e. `CoreComponents`): the plugin is
|
|
46
|
+
built *from* `archetypes`, so a plugin-derived `Archetype` is **circular** the
|
|
47
|
+
moment it is used to `satisfies`-check an archetype tuple — and the cycle
|
|
48
|
+
collapses the entire database type to `{}`. The leaf derivation is cycle-free and
|
|
49
|
+
usable both in `archetypes.ts` (to check shared bases) and downstream (to check
|
|
50
|
+
`count` / `select` query keys).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/main-service.ts'
|
|
4
|
+
- '**/features/*/services/main-service/*-database/*-database.ts'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# services/main-service/ — the feature service (ECS implementation)
|
|
8
|
+
|
|
9
|
+
> **Legacy state-based layout**, kept for existing features (`data/state/` + `services/main-service/`). New features use the layered layout in `../../index.md`.
|
|
10
|
+
|
|
11
|
+
`main-service/` is the feature's single entrypoint service (see `../index.md`),
|
|
12
|
+
implemented as a reactive Entity-Component-System — an implementation detail
|
|
13
|
+
behind the service. Everything ECS-specific lives here: the schema that stores `data/` types as
|
|
14
|
+
entity state (`core-database/`), the derived and mutating logic over it
|
|
15
|
+
(`indexes/`, `transactions/`, `computed/`), the database-bound service factories
|
|
16
|
+
(`services/`), the async orchestrators (`actions/`), and the layered plugins
|
|
17
|
+
that compose it all.
|
|
18
|
+
|
|
19
|
+
The distinction from `data/`: a `data/` type is ECS-agnostic (a plain
|
|
20
|
+
serializable value); an ECS construct is *how that type is stored and operated
|
|
21
|
+
on* inside a database. `mark: PlayerMark.schema` is not a data-model fact — it
|
|
22
|
+
is "the ECS keeps a PlayerMark here".
|
|
23
|
+
|
|
24
|
+
## The core schema — `core-database/`
|
|
25
|
+
|
|
26
|
+
A feature's whole schema — every component, resource, and archetype — lives in
|
|
27
|
+
one `core-database/` folder: `core-database.ts` plus one file per facet the
|
|
28
|
+
feature uses (all three facets are optional on `Database.Plugin.create`):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
core-database/
|
|
32
|
+
core-database.ts # Database.Plugin.create({ components?, resources?, archetypes? }) → CoreDatabase
|
|
33
|
+
components.ts # export const components = Database.components({ … })
|
|
34
|
+
resources.ts # export const resources = Database.resources({ … })
|
|
35
|
+
archetypes.ts # export const archetypes = Database.archetypes(components, { … })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`core-database.ts` is a thin assembler; each facet file exports one typed object
|
|
39
|
+
built by a `Database.*` helper. No per-item files, no barrels. Create only the
|
|
40
|
+
facet files the feature needs — a singleton feature with no entities is just
|
|
41
|
+
`resources.ts` + `core-database.ts` (no empty `components({})` boilerplate).
|
|
42
|
+
|
|
43
|
+
### Schema scopes
|
|
44
|
+
|
|
45
|
+
State divides on two orthogonal axes — **scope** (shared with peers vs. local to
|
|
46
|
+
this client) and **lifetime** (durable across reloads vs. ephemeral) — giving
|
|
47
|
+
four scopes, each stamping a flag pair:
|
|
48
|
+
|
|
49
|
+
| scope | shared? | durable? | flags |
|
|
50
|
+
|-------|:--:|:--:|-------|
|
|
51
|
+
| **document** | yes | yes | — |
|
|
52
|
+
| **settings** | no | yes | `nonShared` |
|
|
53
|
+
| **presence** | yes | no | `nonPersistent` |
|
|
54
|
+
| **session** | no | no | `nonPersistent` + `nonShared` |
|
|
55
|
+
|
|
56
|
+
`Database.components` / `Database.resources` group declarations by scope and
|
|
57
|
+
stamp the flags. Every scope key is optional — declare only the ones you use
|
|
58
|
+
(most features are document-only):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// components.ts
|
|
62
|
+
export const components = Database.components({
|
|
63
|
+
document: { todo: True.schema, name: Name.schema, complete: Boolean.schema },
|
|
64
|
+
session: { dragPosition: DragPosition.schema }, // omit settings / presence
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// resources.ts — entries must carry a `default` (enforced at the call)
|
|
68
|
+
export const resources = Database.resources({
|
|
69
|
+
settings: { displayCompleted: Boolean.schema },
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A component/resource *value* is a plain `data/`-schema re-export (or a small
|
|
74
|
+
inline literal); its scope — and therefore its flags — is stated only by which
|
|
75
|
+
group it sits in. Hover a scope key for its meaning. The database does not yet
|
|
76
|
+
act on these flags (see the `nonShared` schema note); they model
|
|
77
|
+
shared/local × durable/ephemeral now, and the durable-shared subset
|
|
78
|
+
(`document`) is the set a bare store could be rebuilt from for reconciliation.
|
|
79
|
+
|
|
80
|
+
**Shared columns live in `data/`.** A column two features share (e.g. a `name`
|
|
81
|
+
on both todos and users) is a `data/` type both reference by identity
|
|
82
|
+
(`Name.schema`), so `combinePlugins` dedupes it. Only the `document` scope
|
|
83
|
+
preserves schema identity (it applies no flags) — never share a
|
|
84
|
+
settings/presence/session column across features by identity.
|
|
85
|
+
|
|
86
|
+
### Archetypes
|
|
87
|
+
|
|
88
|
+
`archetypes.ts` declares the feature's entity shapes with
|
|
89
|
+
`Database.archetypes(components, { … })`, which validates every key against the
|
|
90
|
+
component map and preserves each archetype's literal tuple — no per-archetype
|
|
91
|
+
`as const`. Archetypes are a packing convenience, not serialized data; one may
|
|
92
|
+
span scopes (a document entity with an ephemeral `dragPosition` slot). An
|
|
93
|
+
archetype mirroring a `data/` row type carries a private compile-time
|
|
94
|
+
drift-guard (see `archetypes.md`).
|
|
95
|
+
|
|
96
|
+
## Layered composition (behaviour)
|
|
97
|
+
|
|
98
|
+
Above the schema, behaviour is a chain of `Database.Plugin`s, each its own
|
|
99
|
+
**namespace folder** — `<layer>-database/` holding the eponymous
|
|
100
|
+
`<layer>-database.ts` (the plugin, which `extends` the previous layer) plus the
|
|
101
|
+
facet folder it adds. **Name each layer for the facet it adds — never for the
|
|
102
|
+
feature or app.** A feature creates only the layers it uses:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
main-service/
|
|
106
|
+
core-database/ # the schema (above)
|
|
107
|
+
index-database/ # extends core; indexes/
|
|
108
|
+
transaction-database/ # extends index; transactions/
|
|
109
|
+
service-database/ # extends transaction; services/
|
|
110
|
+
computed-database/ # extends service; computed/
|
|
111
|
+
action-database/ # extends computed; actions/
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each exports a namespace mirroring the plugin — the composed object is a
|
|
115
|
+
`Database` (the `@adobe/data` term) even though the folder is `services/main-service/`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
export type IndexDatabase = Database.Plugin.ToDatabase<typeof indexDatabasePlugin>;
|
|
119
|
+
export namespace IndexDatabase {
|
|
120
|
+
export const plugin = indexDatabasePlugin;
|
|
121
|
+
export type Store = Database.Plugin.ToStore<typeof indexDatabasePlugin>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### The assembled database — `MainService`
|
|
126
|
+
|
|
127
|
+
*Which* layer is topmost varies with the facets a feature uses (service /
|
|
128
|
+
computed / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/`
|
|
129
|
+
— need the *whole* feature database but must not name that layer, or adding or
|
|
130
|
+
dropping a layer rewrites them all. Alias it once from the **folder-eponymous
|
|
131
|
+
`main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in
|
|
132
|
+
its same-named file), as a namespace so consumers reach `MainService.plugin` /
|
|
133
|
+
`MainService.Store`:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// services/main-service/main-service.ts
|
|
137
|
+
export { SystemDatabase as MainService } from "./system-database/system-database.js";
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every consumer references **`MainService`** (`.plugin`, `.Store`) — never the
|
|
141
|
+
topmost layer. Add or drop a layer → change only this one line.
|
|
142
|
+
|
|
143
|
+
**Cross-feature naming.** Inside a feature the core and assembled databases are
|
|
144
|
+
the bare `CoreDatabase` / `MainService`. When *another* feature or package
|
|
145
|
+
imports one — a peer built on it, the base `imports` a peer's `core-database`, a
|
|
146
|
+
downstream package — reference it **feature-qualified**: `<Feature>CoreDatabase`
|
|
147
|
+
/ `<Feature>MainService`, re-exported so from the feature's barrel
|
|
148
|
+
(`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so
|
|
149
|
+
two features' `CoreDatabase`s never collide.
|
|
150
|
+
|
|
151
|
+
## What each layer takes as input
|
|
152
|
+
|
|
153
|
+
- **`plugin`** — every layer `extends` the previous layer's `plugin`; that is
|
|
154
|
+
the one universal thread up the chain.
|
|
155
|
+
- **`Store`** (via `ToStore`) — used *only* by **transactions** (`t:
|
|
156
|
+
<Layer>.Store`). A store carries the index handles and the initiating
|
|
157
|
+
`userId`, so a store *is* the transaction context; there is no separate
|
|
158
|
+
transaction-context type. Use **`CoreDatabase.Store`** for a transaction that
|
|
159
|
+
reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
|
|
160
|
+
moment it reads an index.
|
|
161
|
+
- **the whole `Database`** — **services**, **computed**, and **actions** each
|
|
162
|
+
take `service: <Layer>` (the lowest layer whose database exposes what they
|
|
163
|
+
read/call): services read observables and call transactions; computed reads
|
|
164
|
+
`service.observe.*` and, once it needs a service's own reactive surface,
|
|
165
|
+
`service.services.*`; actions call `service.services.*` then
|
|
166
|
+
`service.transactions.*`.
|
|
167
|
+
|
|
168
|
+
Each subfolder has its own rule. Modelling a plugin's authored vs. derived
|
|
169
|
+
surface is covered by `plugin-modelling.md`.
|
|
170
|
+
|
|
171
|
+
## Facets are flat until they aren't
|
|
172
|
+
|
|
173
|
+
A **behavioural** facet folder (`indexes/`, `transactions/`, `computed/`,
|
|
174
|
+
`actions/`) holds one file per item plus an `index.ts` barrel — flat while that
|
|
175
|
+
reads well, roughly up to half a dozen items. When a cohesive cluster grows past
|
|
176
|
+
that, promoting it into a themed subfolder with its own `index.ts` (re-exported
|
|
177
|
+
by the facet barrel) is a standard move — only once the folder actually feels
|
|
178
|
+
crowded:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
transactions/
|
|
182
|
+
order/ # a cohesive cluster: fractional-ordering helpers
|
|
183
|
+
index.ts next-order.ts normalize-order.ts select-ordered-todos.ts
|
|
184
|
+
create-todo.ts drag-todo.ts …
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The **schema** facets (components / resources / archetypes) are *single files*
|
|
188
|
+
in `core-database/`, not folders. A whole feature should stay small enough that
|
|
189
|
+
subfoldering rarely bites — grow by adding features, not by bloating one.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/index-database/indexes/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# database/indexes/ — ECS indexes
|
|
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 index descriptor per file: an object `satisfies CoreDatabase.Index` that
|
|
11
|
+
tells the store to maintain a lookup keyed on a component, so queries and
|
|
12
|
+
computed values can find entities without scanning.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import type { CoreDatabase } from "../../core-database/core-database.js";
|
|
16
|
+
|
|
17
|
+
export const byComplete = {
|
|
18
|
+
key: "complete",
|
|
19
|
+
} as const satisfies CoreDatabase.Index;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`CoreDatabase.Index` is bound to every component (all scopes live in the one
|
|
23
|
+
`core-database`). Archetype-scoped indexes carry an extra `archetype` field the
|
|
24
|
+
`Index` helper can't express, so declare those with a bare `as const` (no
|
|
25
|
+
`satisfies`) and let `index-database.ts`'s `create()` validate them.
|
|
26
|
+
|
|
27
|
+
Name the export for what it indexes (`byComplete`, `byOwner`). An
|
|
28
|
+
`index.ts` barrel re-exports every index; it feeds the `indexes` facet on
|
|
29
|
+
`index-database.ts`.
|
|
30
|
+
|
|
31
|
+
## Discrete keys only — derive a bucket for spatial / range queries
|
|
32
|
+
|
|
33
|
+
An index matches keys by **equality on a discrete value**, so a continuous or
|
|
34
|
+
range-valued column (a `position`, a timestamp, a score) can't be indexed as-is
|
|
35
|
+
— `{ key: "position" }` buckets by exact value and finds nothing near a point.
|
|
36
|
+
Derive a discrete **bucket key** — but prefer a **computed-key extractor**
|
|
37
|
+
(`key: { cell: (c) => cellKey(c.position!) }`, naming the read columns in
|
|
38
|
+
`components`) over storing the derived key as its own component. The extractor
|
|
39
|
+
recomputes the key from its source columns, so the index stays correct with no
|
|
40
|
+
maintenance; a stored bucket column must instead be recomputed at every
|
|
41
|
+
insertion/update site (or by a system) and silently drifts if one is missed. The
|
|
42
|
+
derivation itself is a pure `data/` helper.
|
|
43
|
+
|
|
44
|
+
Bucketed broad-phase only pays off at scale (hundreds+ candidates per query);
|
|
45
|
+
below that a linear scan over an archetype wins. Index when the scan is a
|
|
46
|
+
**proven** cost, not preemptively.
|
|
47
|
+
|
|
48
|
+
## Scoping, arrays, and many-to-many
|
|
49
|
+
|
|
50
|
+
- **Coverage is by columns.** An index applies to every archetype whose
|
|
51
|
+
components are a superset of the key's columns. Two archetypes that share a
|
|
52
|
+
column are both indexed — so a unique index keyed on a shared column (e.g.
|
|
53
|
+
`name`, held by both users and todos) would collide.
|
|
54
|
+
- **Scope with `archetype`.** `{ key: "name", archetype: "User", unique: true }`
|
|
55
|
+
restricts the index to one archetype, excluding others that share the column.
|
|
56
|
+
`archetype` is **not** on the `Database.Index<C>` `satisfies` helper (it carries
|
|
57
|
+
no archetype map), so declare such an index with `as const` (no `satisfies`)
|
|
58
|
+
and let `index-database.ts`'s `create()` validate it against the real facet.
|
|
59
|
+
- **Array columns fan out automatically.** Keying on a `string[]` column makes a
|
|
60
|
+
multi-value index — one bucket entry per element — and `find({ col: element })`
|
|
61
|
+
takes the element, not the array.
|
|
62
|
+
- **Model a many-to-many as a denormalized array + two indexes.** A
|
|
63
|
+
`todo.assignees: string[]` (owned/displayed by one feature) plus a unique
|
|
64
|
+
`usersByName` (name → user) and a multi-value `todosByAssignee` (assignee →
|
|
65
|
+
todos) gives efficient navigation in both directions from one join key.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/**/core-database/resources.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# core-database/resources.ts — the feature's resources
|
|
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
|
+
A resource is a singleton component — one instance held on a single entity. One
|
|
11
|
+
file, one export: `resources`, built with `Database.resources(...)`. It is
|
|
12
|
+
identical to `Database.components` except **every entry must carry a `default`**
|
|
13
|
+
— a missing `default` is a compile error here, at the declaration site.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Database } from "@adobe/data/ecs";
|
|
17
|
+
import { Boolean } from "@adobe/data/schema";
|
|
18
|
+
|
|
19
|
+
export const resources = Database.resources({
|
|
20
|
+
settings: { displayCompleted: Boolean.schema }, // per-device view toggle
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Give each resource a `default` — spread the data-type's schema and add one, or
|
|
25
|
+
bind a `data/` / primitive schema that already carries a sensible default.
|
|
26
|
+
- **Scope = which group it sits in** (see `services/main-service/index.md`). A per-device
|
|
27
|
+
preference is `settings` (durable, `nonShared`); a synced-but-unsaved value is
|
|
28
|
+
`presence`; a saved shared value is `document`.
|
|
29
|
+
- Because there is only ever one instance, linear-memory packing buys nothing —
|
|
30
|
+
never choose a struct schema for efficiency here.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/service-database/services/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# database/services/ — database-bound service factories
|
|
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 factory per file: `create<Name>Service(service, …)` that binds a service
|
|
11
|
+
implementation to a live database — reading its observables and invoking
|
|
12
|
+
its transactions. This is where the async `services/` contracts (or
|
|
13
|
+
framework services like `AgenticService`) are wired to ECS state.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export const createAgentService = (service: TransactionDatabase, mark: PlayerMark): AgenticService =>
|
|
17
|
+
/* … reads service.observe.*, calls service.transactions.* … */;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Type `service` on the lowest layer exposing what the factory reads/calls (never
|
|
21
|
+
the `service-database` itself — that would be a cycle).
|
|
22
|
+
|
|
23
|
+
An `index.ts` barrel re-exports the factories; `service-database.ts`
|
|
24
|
+
registers them under the `services` facet, each keyed by the name
|
|
25
|
+
consumers read it as (`service.services.agent`). Defining a standalone
|
|
26
|
+
service contract (not yet bound to a database) belongs in the feature
|
|
27
|
+
`services/` layer.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/system-database/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# database/systems/ — the real-time tick loop
|
|
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
|
+
Systems are the framework's real-time backbone: functions the scheduler runs
|
|
11
|
+
**every frame**, in dependency order, to advance a simulation. A feature only
|
|
12
|
+
has a `systems/` folder when it is real-time (a game, a physics/particle sim).
|
|
13
|
+
Turn-based features never need one — they mutate through transactions on user
|
|
14
|
+
input and stop.
|
|
15
|
+
|
|
16
|
+
A system is a `SystemDeclaration` — `{ create, schedule?: { before?, after?, during? } }`.
|
|
17
|
+
`create` runs ONCE at construction (capture closures / query args); it returns the
|
|
18
|
+
per-frame function (or `void` for an init-only system, e.g. seeding the first
|
|
19
|
+
entities). Ordering lives under **`schedule`** (not top-level).
|
|
20
|
+
|
|
21
|
+
**Declare the `systems` map inline in `system-database.ts`.** The `create: (db) => …`
|
|
22
|
+
arrow must be written inline in `Plugin.create` for two reasons: only there is `db`
|
|
23
|
+
strongly typed (to the assembled plugin's database, with a **writable `store`**), and
|
|
24
|
+
a pre-typed standalone `create` reference breaks the scheduler's system-name inference
|
|
25
|
+
(the name union collapses to `never`, which then rejects every `schedule` entry). Keep
|
|
26
|
+
each `create` thin by delegating the real per-frame work to a pure `data/` step
|
|
27
|
+
function; if an ecs-specific body is substantial, extract it to a `systems/<name>.ts`
|
|
28
|
+
helper `(db) => () => void` (annotate its `db`) and call it from the inline `create`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// in system-database.ts
|
|
32
|
+
import { Database, scheduler } from "@adobe/data/ecs";
|
|
33
|
+
import { Motion } from "../../data/motion/motion.js";
|
|
34
|
+
|
|
35
|
+
const plugin = Database.Plugin.create({
|
|
36
|
+
// combine scheduler with the CURRENT TOP layer — ComputedDatabase here, but
|
|
37
|
+
// ActionDatabase / ServiceDatabase if the feature built those (systems come last).
|
|
38
|
+
extends: Database.Plugin.combine(ComputedDatabase.plugin, scheduler),
|
|
39
|
+
systems: {
|
|
40
|
+
control: { create: (db) => () => { /* input → ship; db.transactions.fireBullet() */ } },
|
|
41
|
+
movement: {
|
|
42
|
+
schedule: { after: ["control"], before: ["collision"] },
|
|
43
|
+
create: (db) => () => {
|
|
44
|
+
const dt = db.store.resources.frameDelta;
|
|
45
|
+
for (const arch of db.store.queryArchetypes(["position", "velocity"])) {
|
|
46
|
+
const pos = arch.columns.position, vel = arch.columns.velocity;
|
|
47
|
+
for (let i = 0; i < arch.rowCount; i++) {
|
|
48
|
+
pos.set(i, Motion.advance(pos.get(i), vel.get(i), dt)); // in-place, no migration
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
collision: { schedule: { after: ["movement"] }, create: (db) => () => { /* … */ } },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Systems mutate `db.store` directly** — that is the writable surface a system
|
|
59
|
+
receives (unlike the readonly `.store` consumers get). In-place column writes
|
|
60
|
+
(`col.set(i, …)`) for hot per-row work that stays in the same archetype;
|
|
61
|
+
`db.store.update / insert / delete` for lifecycle (spawning, dying) that changes
|
|
62
|
+
archetype membership. Resources: `db.store.resources.x = …`.
|
|
63
|
+
- **Dispatch a transaction for a discrete atomic event** — when the effect is a
|
|
64
|
+
self-contained event that already has a conformant transaction (score a hit,
|
|
65
|
+
lose a life, spawn a wave, fire a bullet), call `db.transactions.*` instead of
|
|
66
|
+
re-implementing it: you reuse the `data/`-verified logic and observers (a reactive
|
|
67
|
+
HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
|
|
68
|
+
(movement, aging) that run on every entity every frame and shouldn't pay
|
|
69
|
+
per-entity transaction overhead.
|
|
70
|
+
- **Optimize hot paths for speed.** With many entities, query with efficient
|
|
71
|
+
`queryArchetypes` and read/write minimal columns directly; avoid intermediate
|
|
72
|
+
allocations. On a hot path treat `data/` step functions as **spec only** — use
|
|
73
|
+
them when the path isn't hot or you can't beat them by writing the store
|
|
74
|
+
directly; off hot paths, reuse them freely. (Only transactions are observable —
|
|
75
|
+
direct store writes fire no observers.)
|
|
76
|
+
- **Iterate archetypes per `archetypes.md`.** Express selection with
|
|
77
|
+
`queryArchetypes(include, { exclude })`; when a system destroys/migrates rows
|
|
78
|
+
(bullets expiring, entities dying) iterate **tail → head** so hole-fills don't
|
|
79
|
+
invalidate the cursor. A per-frame system touching many rows is exactly where
|
|
80
|
+
those rules pay off. **A `db.transactions.*` call mid-scan migrates rows too** —
|
|
81
|
+
a transaction that inserts/deletes invalidates any live archetype cursor, so
|
|
82
|
+
snapshot the ids you'll act on (or reverse-iterate) *before* dispatching
|
|
83
|
+
transactions inside a per-frame loop.
|
|
84
|
+
- **Replicate the oracle's top-level guard in every system.** If the `data/`
|
|
85
|
+
`step` opens with `if (isGameOver(state)) return state`, each split system must
|
|
86
|
+
early-return on the same condition — a frozen frame is only a true no-op if
|
|
87
|
+
*every* system honors the guard.
|
|
88
|
+
- **Index reads go through `db.indexes`** (plural) — the writable `db.store` a
|
|
89
|
+
system receives is typed *without* its index handles, so a broad-phase query is
|
|
90
|
+
`db.indexes.byCell.find({ cell })`, not `db.store.indexes.…`.
|
|
91
|
+
- **Ordering is declared under `schedule`, not implied.** `schedule.before` /
|
|
92
|
+
`schedule.after` are hard constraints (name sibling systems); `schedule.during`
|
|
93
|
+
is a soft same-tier hint. Systems with no ordering relation share a tier
|
|
94
|
+
(conceptually parallel) — never rely on declaration order. When you split a
|
|
95
|
+
monolithic `data/` `step` into several ordered systems, the schedule **must
|
|
96
|
+
mirror the step's internal sequence** (advance → fire → age → collide → refill)
|
|
97
|
+
— otherwise the tick diverges from the oracle even though each system is
|
|
98
|
+
individually correct. Watch the subtle case: if a spawn step (fire) is followed
|
|
99
|
+
by a step that also processes the just-spawned entity (the new bullet is aged
|
|
100
|
+
and advanced this same tick), you can't advance *all* bodies in one system
|
|
101
|
+
before the spawn — split that entity's advance out so it runs *after* the spawn,
|
|
102
|
+
alongside the step that owns it.
|
|
103
|
+
- Immediate-mode rendering: store the canvas in a session resource and render to
|
|
104
|
+
it inside a system (see @adobe/data-gpu for patterns).
|
|
105
|
+
|
|
106
|
+
## Driving the loop — the scheduler
|
|
107
|
+
|
|
108
|
+
Systems don't run themselves. Combine the built-in scheduler so they run on
|
|
109
|
+
`requestAnimationFrame`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { Database, scheduler } from "@adobe/data/ecs";
|
|
113
|
+
// in system-database.ts:
|
|
114
|
+
Database.Plugin.create({ extends: Database.Plugin.combine(ComputedDatabase.plugin, scheduler), systems });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The scheduler adds a `schedulerState` resource (`"running" | "paused" |
|
|
118
|
+
"disposed"`) — gate start/pause/resume through it (start `"paused"` if the game
|
|
119
|
+
begins on a user action). A headless host (tests, server sim) omits the
|
|
120
|
+
scheduler and drives frames itself by calling `db.system.functions[name]()` for
|
|
121
|
+
each `name` in `db.system.order` — so a simulation is fully runnable and testable
|
|
122
|
+
with no rAF and no rendering attached.
|
|
123
|
+
|
|
124
|
+
## Layer
|
|
125
|
+
|
|
126
|
+
`system-database.ts` extends the feature's **current top** main-service layer (`ActionDatabase` /
|
|
127
|
+
`ServiceDatabase` / `ComputedDatabase` — whichever it built; systems come last in the pipeline, so
|
|
128
|
+
they must sit atop any service/action layers, not a hardcoded `ComputedDatabase`), combined with
|
|
129
|
+
`scheduler`, and declares the `systems` map **inline** (see above —
|
|
130
|
+
this is the one facet not split one-per-file, because `create`'s `db` is only typed
|
|
131
|
+
inline and standalone declarations break name inference). A `systems/` folder is
|
|
132
|
+
optional: it holds extracted per-frame body helpers (`(db) => () => void`) only when
|
|
133
|
+
an inline body grows too large — the schedule and registration still live inline in
|
|
134
|
+
`system-database.ts`. The headless conformance test drives one frame via
|
|
135
|
+
`db.system.order` / `db.system.functions` and compares to the `data/` `step` oracle,
|
|
136
|
+
**reusing the feature projection** (`services/main-service/conformance.md`); it lives at
|
|
137
|
+
`system-database/tick-loop.test.ts`. Test **selection/detection** logic (which
|
|
138
|
+
entities collide, which pair resolves) separately, with seeded edge-case geometries
|
|
139
|
+
— it's a system concern, not a transaction, and where subtle bugs hide.
|
|
140
|
+
|
|
141
|
+
## Iterating archetype rows
|
|
142
|
+
|
|
143
|
+
A per-frame system reads/writes archetype columns directly. The row-iteration
|
|
144
|
+
rules — declarative `queryArchetypes` selection, tail → head migration, snapshot
|
|
145
|
+
avoidance — are general ECS concerns in the rules-root `archetypes.md`.
|