@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.
Files changed (41) hide show
  1. package/.claude/rules/README.md +8 -3
  2. package/.claude/rules/ecs.md +1 -1
  3. package/.claude/rules/element.md +9 -0
  4. package/.claude/rules/features/data/components.md +6 -3
  5. package/.claude/rules/features/data/index.md +4 -1
  6. package/.claude/rules/features/data/state.md +211 -0
  7. package/.claude/rules/features/ecs/conformance.md +12 -6
  8. package/.claude/rules/features/index.md +4 -0
  9. package/.claude/rules/features/services/index.md +2 -0
  10. package/.claude/rules/features/services/main-service/actions.md +72 -0
  11. package/.claude/rules/features/services/main-service/archetypes.md +51 -0
  12. package/.claude/rules/features/services/main-service/components.md +43 -0
  13. package/.claude/rules/features/services/main-service/computed.md +84 -0
  14. package/.claude/rules/features/services/main-service/conformance.md +97 -0
  15. package/.claude/rules/features/services/main-service/core-database.md +50 -0
  16. package/.claude/rules/features/services/main-service/index.md +189 -0
  17. package/.claude/rules/features/services/main-service/indexes.md +65 -0
  18. package/.claude/rules/features/services/main-service/resources.md +30 -0
  19. package/.claude/rules/features/services/main-service/services.md +27 -0
  20. package/.claude/rules/features/services/main-service/systems.md +145 -0
  21. package/.claude/rules/features/services/main-service/transactions.md +61 -0
  22. package/.claude/rules/features/ui/index.md +2 -0
  23. package/.claude/rules/service.md +25 -75
  24. package/.claude-plugin/plugin.json +1 -1
  25. package/package.json +3 -3
  26. package/skills/build-actions/SKILL.md +2 -0
  27. package/skills/build-app-entry/SKILL.md +2 -0
  28. package/skills/build-computed/SKILL.md +2 -0
  29. package/skills/build-core-database/SKILL.md +2 -0
  30. package/skills/build-data/SKILL.md +2 -0
  31. package/skills/build-feature/SKILL.md +3 -0
  32. package/skills/build-indexes/SKILL.md +2 -0
  33. package/skills/build-service-database/SKILL.md +2 -0
  34. package/skills/build-services/SKILL.md +2 -0
  35. package/skills/build-spec/SKILL.md +2 -0
  36. package/skills/build-systems/SKILL.md +2 -0
  37. package/skills/build-transactions/SKILL.md +2 -0
  38. package/skills/build-ui/SKILL.md +2 -0
  39. package/skills/review/SKILL.md +2 -0
  40. package/skills/structure/SKILL.md +2 -0
  41. package/.claude/rules/features/ui/binding-element.md +0 -226
@@ -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`.