@adobe/data-ai 0.9.82 → 0.9.84

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 (59) hide show
  1. package/.claude/rules/README.md +42 -0
  2. package/.claude/rules/archetypes.md +60 -0
  3. package/.claude/rules/cohesion.md +66 -0
  4. package/.claude/rules/data-modelling.md +71 -0
  5. package/.claude/rules/element.md +130 -0
  6. package/.claude/rules/features/data/index.md +35 -0
  7. package/.claude/rules/features/data/state.md +54 -0
  8. package/.claude/rules/features/ecs/actions.md +32 -0
  9. package/.claude/rules/features/ecs/archetypes.md +46 -0
  10. package/.claude/rules/features/ecs/components.md +41 -0
  11. package/.claude/rules/features/ecs/computed.md +26 -0
  12. package/.claude/rules/features/ecs/conformance.md +69 -0
  13. package/.claude/rules/features/ecs/index.md +179 -0
  14. package/.claude/rules/features/ecs/indexes.md +63 -0
  15. package/.claude/rules/features/ecs/resources.md +28 -0
  16. package/.claude/rules/features/ecs/services.md +24 -0
  17. package/.claude/rules/features/ecs/systems.md +133 -0
  18. package/.claude/rules/features/ecs/transactions.md +45 -0
  19. package/.claude/rules/features/index.md +95 -0
  20. package/.claude/rules/features/services/index.md +34 -0
  21. package/.claude/rules/features/ui/index.md +31 -0
  22. package/.claude/rules/function-references.md +35 -0
  23. package/.claude/rules/lazy-element.md +127 -0
  24. package/.claude/rules/namespace.md +179 -0
  25. package/.claude/rules/plugin-modelling.md +170 -0
  26. package/.claude/rules/presentation.md +83 -0
  27. package/.claude/rules/type-casts.md +94 -0
  28. package/.claude-plugin/plugin.json +1 -1
  29. package/README.md +39 -81
  30. package/bin/cli.mjs +81 -26
  31. package/package.json +9 -2
  32. package/skills/build-actions/SKILL.md +14 -0
  33. package/skills/build-app-entry/SKILL.md +15 -0
  34. package/skills/build-application/SKILL.md +10 -0
  35. package/skills/build-computed/SKILL.md +14 -0
  36. package/skills/build-core-database/SKILL.md +24 -0
  37. package/skills/build-data/SKILL.md +18 -0
  38. package/skills/build-feature/SKILL.md +21 -0
  39. package/skills/build-game/SKILL.md +19 -0
  40. package/skills/build-indexes/SKILL.md +14 -0
  41. package/skills/build-service-database/SKILL.md +16 -0
  42. package/skills/build-services/SKILL.md +14 -0
  43. package/skills/build-systems/SKILL.md +24 -0
  44. package/skills/build-transactions/SKILL.md +15 -0
  45. package/skills/build-ui/SKILL.md +13 -0
  46. package/skills/meta-build/SKILL.md +43 -0
  47. package/skills/review/SKILL.md +25 -0
  48. package/skills/structure/SKILL.md +7 -0
  49. package/skills/x-alpha/SKILL.md +7 -0
  50. package/skills/x-beta/SKILL.md +7 -0
  51. package/skills/x-double/SKILL.md +7 -0
  52. package/skills/x-execute/SKILL.md +16 -0
  53. package/skills/x-log/SKILL.md +12 -0
  54. package/skills/x-pow/SKILL.md +9 -0
  55. package/skills/x-subagent/SKILL.md +8 -0
  56. package/skills/x-trace/SKILL.md +11 -0
  57. package/skills/x-validate/SKILL.md +67 -0
  58. package/skills/bar/SKILL.md +0 -8
  59. package/skills/foo/SKILL.md +0 -8
@@ -0,0 +1,69 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/conformance/**/*.ts'
4
+ ---
5
+
6
+ # ecs/conformance/ — spec↔implementation projection (test-only)
7
+
8
+ The bridge that keeps the ecs implementation honest against the `data/` spec.
9
+ **Test-only**: imported solely by `*.test.ts` and in no facet barrel, so it never
10
+ enters the runtime bundle (test-support may import the test framework — it is not
11
+ a runtime declaration). **Feature-level**: one projection per feature, reused by
12
+ every transaction test and the system tick-loop test — don't nest it under
13
+ `transaction-database/`.
14
+
15
+ The property, per `{ before, args, after }` case:
16
+ `toState(apply(fromState(before), args)) ≡ spec(before, args)`.
17
+
18
+ Conformance test-support splits by concern along the layer boundary:
19
+
20
+ - **Store-side, here in `ecs/conformance/`:**
21
+ - `from-state.ts` → `fromState(store: CoreDatabase.Store, state: State): void`
22
+ seeds the store (clear tail→head → insert entities → set resources) to a `State`.
23
+ - `to-state.ts` → `toState(store): State` reads the store back — each kind via
24
+ its full named-archetype component set, so the shapes never alias.
25
+ - `expect-conforms.ts` → **one export, `expectConforms({ cases, spec, apply })`**.
26
+ Per case it asserts `spec(before, args) ≡ after` (keeps the case honest), then
27
+ `fromState(before)` → `apply(store, args)` → `toState ≡ after`.
28
+ - **Spec-side, in `data/state/`** (State values, no store — so both the `data/`
29
+ transform tests and this runner import them without a layer violation):
30
+ `conformance-case.ts` (`ConformanceCase<Args>`), the `<transform>.cases.ts`
31
+ cases, and `expect-state-matches.ts` (State equality).
32
+
33
+ ## No cast — build on `Store.create`, not a `Database`
34
+
35
+ A **transaction is `(store, args) => void`**, so transaction conformance needs no
36
+ `Database`: `Store.create(<schema-layer>.plugin)` returns a cast-free writable
37
+ `CoreDatabase.Store` — pass the plugin directly, `Store.create` reads its schema
38
+ facets. Source it from the **lowest layer that declares all the schema** —
39
+ `IndexDatabase`, or `CoreDatabase` if the feature has no indexes — **not**
40
+ `FeatureDatabase`: the store needs only schema, and the behaviour layers
41
+ (transactions / computed / systems) add none.
42
+ `fromState`/`toState`/`apply` all operate on it, and `apply` calls the **raw
43
+ transaction function** directly:
44
+
45
+ ```ts
46
+ expectConforms({ cases, spec: State.hitTarget, apply: (store, args) => hitTarget(store, args) });
47
+ ```
48
+
49
+ A mutation addressed by **entity id** resolves its entities from the seeded store
50
+ inside `apply` (the shared cases stay spec-shaped). Reads never need a cast —
51
+ `Database extends ReadonlyStore`.
52
+
53
+ **Systems** are the one exception: they run via `db.system.functions` and reach
54
+ the store through the db. Get that writable view with the library lens —
55
+ `Database.toSystemDatabase(Database.create(plugin))` (the widening twin of
56
+ `UIService.restrict`, cast-free) — then `fromState(db.store, before)` → drive one
57
+ frame → `toState(db.store)`. Test **selection/detection** logic (which entities
58
+ interact) separately, with seeded edge-case geometries.
59
+
60
+ ## State equality — ordering vs precision, kept separate
61
+
62
+ `expect-state-matches.ts` compares with **`equalsUnordered`** from `@adobe/data`
63
+ (arrays as multisets — archetype hole-fill reorders rows — objects key-order
64
+ independent). Absorb float noise — F32↔f64 storage rounding **and** trig epsilon
65
+ (a quadrant `cos`/`sin` yields ~3e-15 where a case authors `0`) — by quantizing
66
+ every number on both sides onto a small grid (`Math.round(Math.fround(n)*k)/k`)
67
+ before comparing. Ordering and precision stay separate concerns. **Guard the
68
+ projection itself** with one `fromState → toState` identity test on
69
+ representative states.
@@ -0,0 +1,179 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/*.ts'
4
+ ---
5
+
6
+ # ecs/ — the Entity-Component-System layer
7
+
8
+ Everything ECS-specific lives here: the schema that stores `data/` types as
9
+ entity state (`core-database/`), the derived and mutating logic over it
10
+ (`indexes/`, `transactions/`, `computed/`), the database-bound service factories
11
+ (`services/`), the async orchestrators (`actions/`), and the layered plugins
12
+ that compose it all.
13
+
14
+ The distinction from `data/`: a `data/` type is ECS-agnostic (a plain
15
+ serializable value); an ECS construct is *how that type is stored and operated
16
+ on* inside a database. `mark: PlayerMark.schema` is not a data-model fact — it
17
+ is "the ECS keeps a PlayerMark here".
18
+
19
+ ## The core schema — `core-database/`
20
+
21
+ A feature's whole schema — every component, resource, and archetype — lives in
22
+ one `core-database/` folder: `core-database.ts` plus one file per facet the
23
+ feature uses (all three facets are optional on `Database.Plugin.create`):
24
+
25
+ ```
26
+ ecs/core-database/
27
+ core-database.ts # Database.Plugin.create({ components?, resources?, archetypes? }) → CoreDatabase
28
+ components.ts # export const components = Database.components({ … })
29
+ resources.ts # export const resources = Database.resources({ … })
30
+ archetypes.ts # export const archetypes = Database.archetypes(components, { … })
31
+ ```
32
+
33
+ `core-database.ts` is a thin assembler; each facet file exports one typed object
34
+ built by a `Database.*` helper. No per-item files, no barrels. Create only the
35
+ facet files the feature needs — a singleton feature with no entities is just
36
+ `resources.ts` + `core-database.ts` (no empty `components({})` boilerplate).
37
+
38
+ ### Schema scopes
39
+
40
+ State divides on two orthogonal axes — **scope** (shared with peers vs. local to
41
+ this client) and **lifetime** (durable across reloads vs. ephemeral) — giving
42
+ four scopes, each stamping a flag pair:
43
+
44
+ | scope | shared? | durable? | flags |
45
+ |-------|:--:|:--:|-------|
46
+ | **document** | yes | yes | — |
47
+ | **settings** | no | yes | `nonShared` |
48
+ | **presence** | yes | no | `nonPersistent` |
49
+ | **session** | no | no | `nonPersistent` + `nonShared` |
50
+
51
+ `Database.components` / `Database.resources` group declarations by scope and
52
+ stamp the flags. Every scope key is optional — declare only the ones you use
53
+ (most features are document-only):
54
+
55
+ ```ts
56
+ // components.ts
57
+ export const components = Database.components({
58
+ document: { todo: True.schema, name: Name.schema, complete: Boolean.schema },
59
+ session: { dragPosition: DragPosition.schema }, // omit settings / presence
60
+ });
61
+
62
+ // resources.ts — entries must carry a `default` (enforced at the call)
63
+ export const resources = Database.resources({
64
+ settings: { displayCompleted: Boolean.schema },
65
+ });
66
+ ```
67
+
68
+ A component/resource *value* is a plain `data/`-schema re-export (or a small
69
+ inline literal); its scope — and therefore its flags — is stated only by which
70
+ group it sits in. Hover a scope key for its meaning. The database does not yet
71
+ act on these flags (see the `nonShared` schema note); they model
72
+ shared/local × durable/ephemeral now, and the durable-shared subset
73
+ (`document`) is the set a bare store could be rebuilt from for reconciliation.
74
+
75
+ **Shared columns live in `data/`.** A column two features share (e.g. a `name`
76
+ on both todos and users) is a `data/` type both reference by identity
77
+ (`Name.schema`), so `combinePlugins` dedupes it. Only the `document` scope
78
+ preserves schema identity (it applies no flags) — never share a
79
+ settings/presence/session column across features by identity.
80
+
81
+ ### Archetypes
82
+
83
+ `archetypes.ts` declares the feature's entity shapes with
84
+ `Database.archetypes(components, { … })`, which validates every key against the
85
+ component map and preserves each archetype's literal tuple — no per-archetype
86
+ `as const`. Archetypes are a packing convenience, not serialized data; one may
87
+ span scopes (a document entity with an ephemeral `dragPosition` slot). An
88
+ archetype mirroring a `data/` row type carries a private compile-time
89
+ drift-guard (see `archetypes.md`).
90
+
91
+ ## Layered composition (behaviour)
92
+
93
+ Above the schema, behaviour is a chain of `Database.Plugin`s, each its own
94
+ **namespace folder** — `<layer>-database/` holding the eponymous
95
+ `<layer>-database.ts` (the plugin, which `extends` the previous layer) plus the
96
+ facet folder it adds. **Name each layer for the facet it adds — never for the
97
+ feature or app.** A feature creates only the layers it uses:
98
+
99
+ ```
100
+ ecs/
101
+ core-database/ # the schema (above)
102
+ index-database/ # extends core; indexes/
103
+ transaction-database/ # extends index; transactions/
104
+ computed-database/ # extends transaction; computed/
105
+ service-database/ # extends computed; services/
106
+ action-database/ # extends service; actions/
107
+ ```
108
+
109
+ Each exports a namespace mirroring the plugin — the composed object is a
110
+ `Database` (the `@adobe/data` term) even though the folder is `ecs/`:
111
+
112
+ ```ts
113
+ export type IndexDatabase = Database.Plugin.ToDatabase<typeof indexDatabasePlugin>;
114
+ export namespace IndexDatabase {
115
+ export const plugin = indexDatabasePlugin;
116
+ export type Store = Database.Plugin.ToStore<typeof indexDatabasePlugin>;
117
+ }
118
+ ```
119
+
120
+ ### The assembled database — `FeatureDatabase`
121
+
122
+ *Which* layer is topmost varies with the facets a feature uses (computed /
123
+ service / action / system). Consumers — `ui/`, the app entry, `ecs/conformance/`
124
+ — need the *whole* feature database but must not name that layer, or adding or
125
+ dropping a layer rewrites them all. Alias it once:
126
+
127
+ ```ts
128
+ // ecs/feature-database.ts
129
+ export { SystemDatabase as FeatureDatabase } from "./system-database/system-database.js";
130
+ ```
131
+
132
+ Every consumer references **`FeatureDatabase`** (`.plugin`, `.Store`) — never the
133
+ topmost layer. Add or drop a layer → change only this one line.
134
+
135
+ **Cross-feature naming.** Inside a feature the core and assembled databases are
136
+ the bare `CoreDatabase` / `FeatureDatabase`. When *another* feature or package
137
+ imports one — a peer built on it, the base `imports` a peer's `core-database`, a
138
+ downstream package — reference it **feature-qualified**: `<Feature>CoreDatabase`
139
+ / `<Feature>Database`, re-exported so from the feature's barrel
140
+ (`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so
141
+ two features' `CoreDatabase`s never collide.
142
+
143
+ ## What each layer takes as input
144
+
145
+ - **`plugin`** — every layer `extends` the previous layer's `plugin`; that is
146
+ the one universal thread up the chain.
147
+ - **`Store`** (via `ToStore`) — used *only* by **transactions** (`t:
148
+ <Layer>.Store`). A store carries the index handles and the initiating
149
+ `userId`, so a store *is* the transaction context; there is no separate
150
+ transaction-context type. Use **`CoreDatabase.Store`** for a transaction that
151
+ reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
152
+ moment it reads an index.
153
+ - **the whole `Database`** — **computed**, **services**, and **actions** each
154
+ take `db: <Layer>` (the lowest layer whose database exposes what they
155
+ read/call): computed reads `db.observe.*`; services read observables and call
156
+ transactions; actions call `db.services.*` then `db.transactions.*`.
157
+
158
+ Each subfolder has its own rule. Modelling a plugin's authored vs. derived
159
+ surface is covered by `plugin-modelling.md`.
160
+
161
+ ## Facets are flat until they aren't
162
+
163
+ A **behavioural** facet folder (`indexes/`, `transactions/`, `computed/`,
164
+ `actions/`) holds one file per item plus an `index.ts` barrel — flat while that
165
+ reads well, roughly up to half a dozen items. When a cohesive cluster grows past
166
+ that, promoting it into a themed subfolder with its own `index.ts` (re-exported
167
+ by the facet barrel) is a standard move — only once the folder actually feels
168
+ crowded:
169
+
170
+ ```
171
+ transactions/
172
+ order/ # a cohesive cluster: fractional-ordering helpers
173
+ index.ts next-order.ts normalize-order.ts select-ordered-todos.ts
174
+ create-todo.ts drag-todo.ts …
175
+ ```
176
+
177
+ The **schema** facets (components / resources / archetypes) are *single files*
178
+ in `core-database/`, not folders. A whole feature should stay small enough that
179
+ subfoldering rarely bites — grow by adding features, not by bloating one.
@@ -0,0 +1,63 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/indexes/**/*.ts'
4
+ ---
5
+
6
+ # database/indexes/ — ECS indexes
7
+
8
+ One index descriptor per file: an object `satisfies CoreDatabase.Index` that
9
+ tells the store to maintain a lookup keyed on a component, so queries and
10
+ computed values can find entities without scanning.
11
+
12
+ ```ts
13
+ import type { CoreDatabase } from "../../core-database/core-database.js";
14
+
15
+ export const byComplete = {
16
+ key: "complete",
17
+ } as const satisfies CoreDatabase.Index;
18
+ ```
19
+
20
+ `CoreDatabase.Index` is bound to every component (all scopes live in the one
21
+ `core-database`). Archetype-scoped indexes carry an extra `archetype` field the
22
+ `Index` helper can't express, so declare those with a bare `as const` (no
23
+ `satisfies`) and let `index-database.ts`'s `create()` validate them.
24
+
25
+ Name the export for what it indexes (`byComplete`, `byOwner`). An
26
+ `index.ts` barrel re-exports every index; it feeds the `indexes` facet on
27
+ `index-database.ts`.
28
+
29
+ ## Discrete keys only — derive a bucket for spatial / range queries
30
+
31
+ An index matches keys by **equality on a discrete value**, so a continuous or
32
+ range-valued column (a `position`, a timestamp, a score) can't be indexed as-is
33
+ — `{ key: "position" }` buckets by exact value and finds nothing near a point.
34
+ Derive a discrete **bucket key** — but prefer a **computed-key extractor**
35
+ (`key: { cell: (c) => cellKey(c.position!) }`, naming the read columns in
36
+ `components`) over storing the derived key as its own component. The extractor
37
+ recomputes the key from its source columns, so the index stays correct with no
38
+ maintenance; a stored bucket column must instead be recomputed at every
39
+ insertion/update site (or by a system) and silently drifts if one is missed. The
40
+ derivation itself is a pure `data/` helper.
41
+
42
+ Bucketed broad-phase only pays off at scale (hundreds+ candidates per query);
43
+ below that a linear scan over an archetype wins. Index when the scan is a
44
+ **proven** cost, not preemptively.
45
+
46
+ ## Scoping, arrays, and many-to-many
47
+
48
+ - **Coverage is by columns.** An index applies to every archetype whose
49
+ components are a superset of the key's columns. Two archetypes that share a
50
+ column are both indexed — so a unique index keyed on a shared column (e.g.
51
+ `name`, held by both users and todos) would collide.
52
+ - **Scope with `archetype`.** `{ key: "name", archetype: "User", unique: true }`
53
+ restricts the index to one archetype, excluding others that share the column.
54
+ `archetype` is **not** on the `Database.Index<C>` `satisfies` helper (it carries
55
+ no archetype map), so declare such an index with `as const` (no `satisfies`)
56
+ and let `index-database.ts`'s `create()` validate it against the real facet.
57
+ - **Array columns fan out automatically.** Keying on a `string[]` column makes a
58
+ multi-value index — one bucket entry per element — and `find({ col: element })`
59
+ takes the element, not the array.
60
+ - **Model a many-to-many as a denormalized array + two indexes.** A
61
+ `todo.assignees: string[]` (owned/displayed by one feature) plus a unique
62
+ `usersByName` (name → user) and a multi-value `todosByAssignee` (assignee →
63
+ todos) gives efficient navigation in both directions from one join key.
@@ -0,0 +1,28 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/core-database/resources.ts'
4
+ ---
5
+
6
+ # core-database/resources.ts — the feature's resources
7
+
8
+ A resource is a singleton component — one instance held on a single entity. One
9
+ file, one export: `resources`, built with `Database.resources(...)`. It is
10
+ identical to `Database.components` except **every entry must carry a `default`**
11
+ — a missing `default` is a compile error here, at the declaration site.
12
+
13
+ ```ts
14
+ import { Database } from "@adobe/data/ecs";
15
+ import { Boolean } from "@adobe/data/schema";
16
+
17
+ export const resources = Database.resources({
18
+ settings: { displayCompleted: Boolean.schema }, // per-device view toggle
19
+ });
20
+ ```
21
+
22
+ - Give each resource a `default` — spread the data-type's schema and add one, or
23
+ bind a `data/` / primitive schema that already carries a sensible default.
24
+ - **Scope = which group it sits in** (see `ecs/index.md`). A per-device
25
+ preference is `settings` (durable, `nonShared`); a synced-but-unsaved value is
26
+ `presence`; a saved shared value is `document`.
27
+ - Because there is only ever one instance, linear-memory packing buys nothing —
28
+ never choose a struct schema for efficiency here.
@@ -0,0 +1,24 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/services/**/*.ts'
4
+ ---
5
+
6
+ # database/services/ — database-bound service factories
7
+
8
+ One factory per file: `create<Name>Service(db, …)` that binds a service
9
+ implementation to a live database — reading its observables and invoking
10
+ its transactions. This is where the async `services/` contracts (or
11
+ framework services like `AgenticService`) are wired to ECS state.
12
+
13
+ ```ts
14
+ export const createAgentService = (db: ComputedDatabase, mark: PlayerMark): AgenticService =>
15
+ /* … reads db.observe.*, calls db.transactions.* … */;
16
+ ```
17
+
18
+ Type `db` on the lowest layer exposing what the factory reads/calls (never the
19
+ `service-database` itself — that would be a cycle).
20
+
21
+ An `index.ts` barrel re-exports the factories; `service-database.ts`
22
+ registers them under the `services` facet, each keyed by the name
23
+ consumers read it as (`db.services.agent`). Defining a standalone service
24
+ contract (not yet bound to a db) belongs in the feature `services/` layer.
@@ -0,0 +1,133 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/systems/**/*.ts'
4
+ ---
5
+
6
+ # database/systems/ — the real-time tick loop
7
+
8
+ Systems are the framework's real-time backbone: functions the scheduler runs
9
+ **every frame**, in dependency order, to advance a simulation. A feature only
10
+ has a `systems/` folder when it is real-time (a game, a physics/particle sim).
11
+ Turn-based features never need one — they mutate through transactions on user
12
+ input and stop.
13
+
14
+ A system is a `SystemDeclaration` — `{ create, schedule?: { before?, after?, during? } }`.
15
+ `create` runs ONCE at construction (capture closures / query args); it returns the
16
+ per-frame function (or `void` for an init-only system, e.g. seeding the first
17
+ entities). Ordering lives under **`schedule`** (not top-level).
18
+
19
+ **Declare the `systems` map inline in `system-database.ts`.** The `create: (db) => …`
20
+ arrow must be written inline in `Plugin.create` for two reasons: only there is `db`
21
+ strongly typed (to the assembled plugin's database, with a **writable `store`**), and
22
+ a pre-typed standalone `create` reference breaks the scheduler's system-name inference
23
+ (the name union collapses to `never`, which then rejects every `schedule` entry). Keep
24
+ each `create` thin by delegating the real per-frame work to a pure `data/` step
25
+ function; if an ecs-specific body is substantial, extract it to a `systems/<name>.ts`
26
+ helper `(db) => () => void` (annotate its `db`) and call it from the inline `create`.
27
+
28
+ ```ts
29
+ // in system-database.ts
30
+ import { Database, scheduler } from "@adobe/data/ecs";
31
+ import { Motion } from "../../data/motion/motion.js";
32
+
33
+ const plugin = Database.Plugin.create({
34
+ extends: Database.Plugin.combine(ComputedDatabase.plugin, scheduler),
35
+ systems: {
36
+ control: { create: (db) => () => { /* input → ship; db.transactions.fireBullet() */ } },
37
+ movement: {
38
+ schedule: { after: ["control"], before: ["collision"] },
39
+ create: (db) => () => {
40
+ const dt = db.store.resources.frameDelta;
41
+ for (const arch of db.store.queryArchetypes(["position", "velocity"])) {
42
+ const pos = arch.columns.position, vel = arch.columns.velocity;
43
+ for (let i = 0; i < arch.rowCount; i++) {
44
+ pos.set(i, Motion.advance(pos.get(i), vel.get(i), dt)); // in-place, no migration
45
+ }
46
+ }
47
+ },
48
+ },
49
+ collision: { schedule: { after: ["movement"] }, create: (db) => () => { /* … */ } },
50
+ },
51
+ });
52
+ ```
53
+
54
+ - **Systems mutate `db.store` directly** — that is the writable surface a system
55
+ receives (unlike the readonly `.store` consumers get). In-place column writes
56
+ (`col.set(i, …)`) for hot per-row work that stays in the same archetype;
57
+ `db.store.update / insert / delete` for lifecycle (spawning, dying) that changes
58
+ archetype membership. Resources: `db.store.resources.x = …`.
59
+ - **Dispatch a transaction for a discrete atomic event** — when the effect is a
60
+ self-contained event that already has a conformant transaction (score a hit,
61
+ lose a life, spawn a wave, fire a bullet), call `db.transactions.*` instead of
62
+ re-implementing it: you reuse the `data/`-verified logic and observers (a reactive
63
+ HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
64
+ (movement, aging) that run on every entity every frame and shouldn't pay
65
+ per-entity transaction overhead.
66
+ - **The step math lives in `data/`.** A system is the ecs wiring: it reads store
67
+ rows, calls a pure `data/` step/derive function, writes the result back — same
68
+ spec↔implementation discipline as transactions. No physics/game math inline.
69
+ - **Iterate archetypes per `archetypes.md`.** Express selection with
70
+ `queryArchetypes(include, { exclude })`; when a system destroys/migrates rows
71
+ (bullets expiring, entities dying) iterate **tail → head** so hole-fills don't
72
+ invalidate the cursor. A per-frame system touching many rows is exactly where
73
+ those rules pay off. **A `db.transactions.*` call mid-scan migrates rows too** —
74
+ a transaction that inserts/deletes invalidates any live archetype cursor, so
75
+ snapshot the ids you'll act on (or reverse-iterate) *before* dispatching
76
+ transactions inside a per-frame loop.
77
+ - **Replicate the oracle's top-level guard in every system.** If the `data/`
78
+ `step` opens with `if (isGameOver(state)) return state`, each split system must
79
+ early-return on the same condition — a frozen frame is only a true no-op if
80
+ *every* system honors the guard.
81
+ - **Index reads go through `db.indexes`** (plural) — the writable `db.store` a
82
+ system receives is typed *without* its index handles, so a broad-phase query is
83
+ `db.indexes.byCell.find({ cell })`, not `db.store.indexes.…`.
84
+ - **Ordering is declared under `schedule`, not implied.** `schedule.before` /
85
+ `schedule.after` are hard constraints (name sibling systems); `schedule.during`
86
+ is a soft same-tier hint. Systems with no ordering relation share a tier
87
+ (conceptually parallel) — never rely on declaration order. When you split a
88
+ monolithic `data/` `step` into several ordered systems, the schedule **must
89
+ mirror the step's internal sequence** (advance → fire → age → collide → refill)
90
+ — otherwise the tick diverges from the oracle even though each system is
91
+ individually correct. Watch the subtle case: if a spawn step (fire) is followed
92
+ by a step that also processes the just-spawned entity (the new bullet is aged
93
+ and advanced this same tick), you can't advance *all* bodies in one system
94
+ before the spawn — split that entity's advance out so it runs *after* the spawn,
95
+ alongside the step that owns it.
96
+ - Games or simulations with immediate mode rendering may want to store a session
97
+ resource of the canvas and render to it within a system. @adobe/data-gpu provides
98
+ example patterns for doing this.
99
+ - All or simulation systems should be optimized for performance.
100
+ Avoid allocatin intermediate objects if possible, read directly from store.
101
+
102
+ ## Driving the loop — the scheduler
103
+
104
+ Systems don't run themselves. Combine the built-in scheduler so they run on
105
+ `requestAnimationFrame`:
106
+
107
+ ```ts
108
+ import { Database, scheduler } from "@adobe/data/ecs";
109
+ // in system-database.ts:
110
+ Database.Plugin.create({ extends: Database.Plugin.combine(ComputedDatabase.plugin, scheduler), systems });
111
+ ```
112
+
113
+ The scheduler adds a `schedulerState` resource (`"running" | "paused" |
114
+ "disposed"`) — gate start/pause/resume through it (start `"paused"` if the game
115
+ begins on a user action). A headless host (tests, server sim) omits the
116
+ scheduler and drives frames itself by calling `db.system.functions[name]()` for
117
+ each `name` in `db.system.order` — so a simulation is fully runnable and testable
118
+ with no rAF and no rendering attached.
119
+
120
+ ## Layer
121
+
122
+ `system-database.ts` extends the previous ecs layer (usually `ComputedDatabase`),
123
+ combined with `scheduler`, and declares the `systems` map **inline** (see above —
124
+ this is the one facet not split one-per-file, because `create`'s `db` is only typed
125
+ inline and standalone declarations break name inference). A `systems/` folder is
126
+ optional: it holds extracted per-frame body helpers (`(db) => () => void`) only when
127
+ an inline body grows too large — the schedule and registration still live inline in
128
+ `system-database.ts`. The headless conformance test drives one frame via
129
+ `db.system.order` / `db.system.functions` and compares to the `data/` `step` oracle,
130
+ **reusing the feature projection** (`ecs/conformance.md`); it lives at
131
+ `system-database/tick-loop.test.ts`. Test **selection/detection** logic (which
132
+ entities collide, which pair resolves) separately, with seeded edge-case geometries
133
+ — it's a system concern, not a transaction, and where subtle bugs hide.
@@ -0,0 +1,45 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/transactions/**/*.ts'
4
+ ---
5
+
6
+ # database/transactions/ — atomic mutations
7
+
8
+ One mutation per file: a function taking the transaction store as its
9
+ first argument and pure `data/` args, returning `void`. It reads and
10
+ writes `t.resources` / entities; all its writes commit or roll back
11
+ together.
12
+
13
+ ```ts
14
+ import { PlayMoveArgs } from "../../../data/play-move-args/play-move-args.js";
15
+ import type { CoreDatabase } from "../../core-database/core-database.js";
16
+
17
+ export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
18
+ // guard, then mutate t.resources / entities / archetypes
19
+ };
20
+ ```
21
+
22
+ - Type the store parameter as `<Layer>.Store`: **`CoreDatabase.Store`** for a
23
+ transaction that reads/writes entities, resources, or archetypes (the common
24
+ case — all schema lives in `core-database`); **`IndexDatabase.Store`** the
25
+ moment it reads an index. A store *is* the transaction context: it carries the
26
+ index handles and the initiating `t.userId`. There is no separate
27
+ transaction-context type.
28
+ - Keep transactions idempotent under replay where sync/P2P applies —
29
+ validate and silently return on illegal or out-of-turn input rather
30
+ than throwing.
31
+ - Decisions come from pure `data/` helpers; the transaction only applies the
32
+ result — read the touched slice, call the `data/` transform, write the diff.
33
+ - **Conform every transaction to its `data/` transform** in its sibling
34
+ `*.test.ts`, via `expectConforms` over the spec's shared cases (see
35
+ `ecs/conformance.md`): seed `fromState(before)`, dispatch, assert `toState ≡
36
+ after`, covering every branch and edge case. A transaction taking **entity
37
+ ids** resolves them from the seeded store in the `apply` closure; one with no
38
+ `data/` analogue (`setBounds`, `setInput`) gets a direct resource assertion.
39
+ - An `index.ts` barrel feeds the `transactions` plugin facet — so it must
40
+ re-export **only** the mutations. A read/query helper shared by several
41
+ transactions (`readShip`, `readBoard` — a `(t) => value` function) may live
42
+ beside them in `transactions/`, but keep it **out of the barrel**, or it gets
43
+ registered as a dispatchable transaction. (These are related helpers, so they
44
+ belong here, not in a separate folder — `cohesion.md`'s peer-level caution
45
+ doesn't apply.)
@@ -0,0 +1,95 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/data/**/*.ts'
4
+ - '**/features/*/services/**/*.ts'
5
+ - '**/features/*/ecs/**/*.ts'
6
+ - '**/features/*/ui/**/*.ts'
7
+ ---
8
+
9
+ # Feature architecture — a verifiable spec and an optimized implementation
10
+
11
+ Each feature is built twice in one codebase: a **pure specification** you can
12
+ trust, and an **efficient implementation** proven equivalent to it.
13
+
14
+ - **`data/` is the specification.** The whole feature modelled as one immutable
15
+ `State` with pure transformations over it — correct by construction, fully
16
+ unit-tested, performance irrelevant. This is the source of truth.
17
+ - **`ecs/` is the implementation.** The same model physically arranged for
18
+ mutation efficiency (a reactive Entity-Component-System). Its reads and writes
19
+ wrap the `data/` functions where practical; where they must be hand-optimized,
20
+ unit tests verify they still agree with the `data/` precedent.
21
+
22
+ Because the optimized `ecs/` layer is largely mechanical given the `data/` spec,
23
+ it can be generated and kept honest by AI rules, with the conformance tests as
24
+ the safety net. Net result: write a slow-but-verifiable app, then derive a fast
25
+ one that provably behaves the same.
26
+
27
+ ## The four layers
28
+
29
+ `ui → ecs → services → data` — higher imports lower, never the reverse. `data/`
30
+ depends on nothing but `@adobe/data` and other `data/` declarations. A feature
31
+ creates only the layers it uses.
32
+
33
+ | Layer | Role |
34
+ |-------|------|
35
+ | `data/` | The spec: `State`, pure transforms & derivations, entity sub-types. Pure, tested. |
36
+ | `services/` | Async capability contracts (ports to the outside world). Optional. |
37
+ | `ecs/` | The implementation: the ECS materialisation + reactive reads/writes over it. |
38
+ | `ui/` | Presentation. |
39
+
40
+ ## One app, many features
41
+
42
+ An application is a set of **features**, each its own `features/<name>/` folder
43
+ with the same four layers. One base feature (`features/main/`) is the host; the
44
+ rest are peers that load lazily.
45
+
46
+ **Keep each feature small; grow by adding features, not by bloating one.** A
47
+ feature is meant to fit in the head — a handful of files per layer. When a
48
+ part of a feature keeps growing, that is usually the signal to split it into
49
+ its own peer feature rather than let one feature's folders balloon.
50
+
51
+ - **Dependencies point toward the base, never out of it.** A peer feature may
52
+ build on another feature's `data/` types and declarations (kept acyclic). The
53
+ base must not depend on its children — with one sanctioned exception below.
54
+ - **The base `imports` every peer's *schema* plugin** —
55
+ `Database.Plugin.create({ imports })`, not `extends`. `imports` merges the
56
+ peer's `core-database` (components / resources / archetypes) into the shared
57
+ store at runtime **without** pulling its types or behavior into the base's type
58
+ or bundle (`extends` would do both, and cost quadratically). So one store knows
59
+ every feature's schema — data coexists, persists, and syncs — while the base
60
+ stays decoupled. Import the peer's `core-database.ts` plugin (schema only) —
61
+ under its feature-qualified name `<Peer>CoreDatabase` (see the cross-feature
62
+ naming rule in `ecs/index.md`): its indexes, transactions, computed, services,
63
+ and UI stay out until the feature loads. A column two features share (e.g.
64
+ `name`) lives in `data/` and is referenced by identity, so `combinePlugins`
65
+ dedupes it.
66
+ - **Peers load lazily by being used.** `DatabaseElement`, on connect, walks up to
67
+ the nearest ancestor database and `extend`s it with its own plugin — so the
68
+ first time a feature element renders, its full plugin (indexes, transactions,
69
+ computed, services) is added to the shared live database and its code chunk is
70
+ fetched. Gate that first render behind a user action (a button, a tab) so the
71
+ load is genuinely on-demand.
72
+ - **The base reaches a child only through a lazy element wrapper** — a tiny
73
+ `Foo()` that `void import()`s the child element. That dynamic import is the one
74
+ allowed core→child seam; the heavy element and its service database stay in the
75
+ child's own chunk.
76
+
77
+ ## Spec and implementation, kept honest
78
+
79
+ The tie between `data/` (spec) and `ecs/` (implementation) is **conformance**,
80
+ one property — `toState(apply(fromState(before), args)) ≡ transform(before, args)`:
81
+ each ecs mutation, seeded and read back through a test-only `ecs ↔ State`
82
+ projection, equals the pure `data/` transform it stands for. The projection and
83
+ its runner live in `ecs/conformance/` (see `ecs/conformance.md`); the shared
84
+ `{ before, args, after }` cases are spec-owned (`data/state/<transform>.cases.ts`),
85
+ so conforming the ecs path is "substitute the implementation, reuse the
86
+ expectations." This lets the ecs layer be largely mechanical and agent-generated,
87
+ with the spec as oracle. *How* to author each layer lives in the per-folder rules
88
+ below.
89
+
90
+ ## Per-layer detail
91
+
92
+ See the rules under each folder: `data/`, `services/`, `ecs/` (components,
93
+ resources, archetypes, computed, indexes, transactions), `ui/`. Cross-cutting
94
+ patterns live at the rules root — `namespace.md`, `data-modelling.md`,
95
+ `archetypes.md` (row iteration), `type-casts.md`, `cohesion.md`.