@adobe/data-ai 0.9.84 → 0.9.87

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 (35) hide show
  1. package/.claude/rules/cohesion.md +2 -2
  2. package/.claude/rules/data-modelling.md +2 -0
  3. package/.claude/rules/ecs.md +275 -0
  4. package/.claude/rules/element.md +3 -1
  5. package/.claude/rules/features/data/state.md +2 -0
  6. package/.claude/rules/features/ecs/conformance.md +5 -3
  7. package/.claude/rules/features/ecs/systems.md +61 -5
  8. package/.claude/rules/features/ui/binding-element.md +226 -0
  9. package/.claude/rules/features/ui/lazy-wrapper.md +27 -0
  10. package/.claude/rules/features/ui/presentation-test.md +38 -0
  11. package/.claude/rules/{presentation.md → features/ui/presentation.md} +2 -32
  12. package/.claude/rules/function-references.md +2 -2
  13. package/.claude/rules/hooks.md +57 -0
  14. package/.claude/rules/layout.md +132 -0
  15. package/.claude/rules/lazy-element.md +1 -1
  16. package/.claude/rules/lit.md +115 -0
  17. package/.claude/rules/namespace.md +93 -135
  18. package/.claude/rules/observe.md +183 -0
  19. package/.claude/rules/react.md +102 -0
  20. package/.claude/rules/service.md +164 -0
  21. package/.claude/rules/type-casts.md +2 -2
  22. package/.claude-plugin/plugin.json +1 -1
  23. package/README.md +24 -4
  24. package/package.json +3 -3
  25. package/skills/build-actions/SKILL.md +3 -1
  26. package/skills/build-application/SKILL.md +5 -3
  27. package/skills/build-computed/SKILL.md +2 -0
  28. package/skills/build-data/SKILL.md +4 -0
  29. package/skills/build-feature/SKILL.md +12 -15
  30. package/skills/build-game/SKILL.md +4 -2
  31. package/skills/build-indexes/SKILL.md +2 -0
  32. package/skills/build-services/SKILL.md +2 -0
  33. package/skills/build-systems/SKILL.md +3 -3
  34. package/skills/graph-execute/SKILL.md +13 -0
  35. package/.claude/rules/archetypes.md +0 -60
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  paths:
3
- - 'packages/**/*.ts'
4
- - 'packages/**/*.tsx'
3
+ - '**/*.ts'
4
+ - '**/*.tsx'
5
5
  ---
6
6
 
7
7
  # Cohesion — make related code share a name
@@ -1,4 +1,6 @@
1
1
  ---
2
+ paths:
3
+ - '**/data/*.ts'
2
4
  ---
3
5
 
4
6
  # Data modelling — locality of knowledge
@@ -0,0 +1,275 @@
1
+ ---
2
+ paths:
3
+ - '**/*-plugin.ts'
4
+ - '**/*-plugin/**/*.ts'
5
+ - '**/*-database.ts'
6
+ ---
7
+
8
+ # Database.Plugin authoring
9
+
10
+ Plugins are created with `Database.Plugin.create()` from `@adobe/data/ecs`.
11
+
12
+ ## Property order (enforced at runtime)
13
+
14
+ Properties **must** appear in this exact order. All are optional.
15
+
16
+ | # | Property | Signature | Purpose |
17
+ | --- | -------------- | -------------------------------------- | ----------------------------------------- |
18
+ | 1 | `imports` | `Plugin` | Dep plugin: types usable, NOT re-exported |
19
+ | 2 | `extends` | `Plugin` | Base plugin to extend (types re-exported) |
20
+ | 3 | `services` | `(db) => ServiceInstance` | Singleton service factories |
21
+ | 4 | `components` | schema object | ECS component schemas |
22
+ | 5 | `resources` | `{ default: value as Type }` | Global resource schemas |
23
+ | 6 | `archetypes` | `['comp1', 'comp2']` | Standard ECS archetypes |
24
+ | 7 | `indexes` | `{ key, order?, unique?, archetype? }` | Sorted/filtered entity indexes |
25
+ | 8 | `computed` | `(db) => Observe<T>` | Computed observables |
26
+ | 9 | `transactions` | `(store, payload) => void` | Synchronous atomic mutations |
27
+ | 10 | `actions` | `(db, payload) => T` | General functions |
28
+ | 11 | `systems` | `{ create: (db) => fn \| void }` | Per-frame (60fps) or init-only |
29
+
30
+ **Wrong order throws at runtime.**
31
+
32
+ ---
33
+
34
+ ## Composition
35
+
36
+ **Single extension:**
37
+
38
+ ```ts
39
+ export const authPlugin = Database.Plugin.create({
40
+ extends: environmentPlugin,
41
+ services: {
42
+ auth: db => AuthService.createLazy({ services: db.services }),
43
+ },
44
+ });
45
+ ```
46
+
47
+ **Combine** — `extends` accepts only one plugin. For multiple, use `Database.Plugin.combine`:
48
+
49
+ ```ts
50
+ export const generationPlugin = Database.Plugin.create({
51
+ extends: Database.Plugin.combine(aPlugin, bPlugin),
52
+ computed: { ... },
53
+ });
54
+ ```
55
+
56
+ **`imports` vs `extends`** — both make the dep's declarations usable in this plugin's factory bodies and merge it at runtime. The difference
57
+ is the result type: `extends` re-exports the dep's types into this plugin's type; `imports` does NOT. Use `imports` for a dep whose types
58
+ you don't want propagating downstream — it keeps the result type O(local), so the dep's type params (e.g. an index `IX`) never accumulate
59
+ through a deep `combine` chain (the TS2589 fix). The runtime merge means no separate top-level `combine` is needed for the imported plugin.
60
+
61
+ ```ts
62
+ export const consumerPlugin = Database.Plugin.create({
63
+ imports: indexesPlugin, // db.indexes usable here + registered at runtime; IX not re-exported
64
+ extends: corePlugin,
65
+ computed: { ... },
66
+ });
67
+ ```
68
+
69
+ **Final composition:**
70
+
71
+ ```ts
72
+ export const appPlugin = Database.Plugin.combine(corePlugin, themePlugin, dataPlugin, authPlugin, uiPlugin, featurePlugin);
73
+ export type AppPlugin = typeof appPlugin;
74
+ export type AppDatabase = Database.Plugin.ToDatabase<AppPlugin>;
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Data modeling example
80
+
81
+ ```ts
82
+ import { Database } from '@adobe/data/ecs';
83
+ import { Vec3, Vec4, F32 } from '@adobe/data/math';
84
+
85
+ const particleDataPlugin = Database.Plugin.create({
86
+ components: {
87
+ position: Vec3.schema,
88
+ velocity: Vec3.schema,
89
+ color: Vec4.schema,
90
+ mass: F32.schema,
91
+ },
92
+ resources: {
93
+ gravity: { default: 9.8 as number },
94
+ },
95
+ archetypes: {
96
+ Particle: ['position', 'velocity', 'color', 'mass'],
97
+ },
98
+ });
99
+ ```
100
+
101
+ - **Components**: per-entity data. Use schema imports (`Vec3`, `Vec4`, `F32` from `@adobe/data/math`) or type namespaces for custom shapes.
102
+ - **Resources**: global state. Use **only** `{ default: value as Type }`.
103
+ - **Archetypes**: one per entity kind. List all components that kind requires.
104
+
105
+ ---
106
+
107
+ ## Property details
108
+
109
+ ### components
110
+
111
+ Non-persistable values (e.g. HTML elements, DOM refs) must use `ephemeral: true` on the schema.
112
+
113
+ ```ts
114
+ components: {
115
+ layout: Layout.schema,
116
+ layoutElement: { default: null as unknown as HTMLElement, ephemeral: true },
117
+ },
118
+ ```
119
+
120
+ ### resources
121
+
122
+ Use `as Type` to provide the compile-time type. Use `null as unknown as Type` for resources initialized later in a system initializer.
123
+
124
+ ```ts
125
+ resources: {
126
+ themeColor: { default: 'dark' as ThemeColor },
127
+ connection: { default: null as unknown as WebSocket },
128
+ },
129
+ ```
130
+
131
+ ### computed
132
+
133
+ Factory returning `Observe<T>` or `(...args) => Observe<T>`. Receives full db.
134
+
135
+ ### transactions
136
+
137
+ Synchronous, deterministic atomic mutations. Receive `store` and a payload.
138
+
139
+ - `store.update(entity, data)` — update entity components
140
+ - `store.resources.x = value` — mutate resources
141
+ - `store.get(entity, 'component')` — read component value
142
+ - `store.select(archetype.components, { where })` — query entities
143
+
144
+ ### actions
145
+
146
+ UI components that call actions must never consume returned values — see `.claude/rules/binding-element.md` (Actions are fire-and-forget).
147
+
148
+ **At most one transaction per action.** Multiple transactions in a single action corrupt the undo/redo stack.
149
+
150
+ ### systems
151
+
152
+ `create` receives db and may optionally return a per-frame function (60fps) or just initialize values.
153
+
154
+ ---
155
+
156
+ ## Naming conventions
157
+
158
+ | Item | Convention | Example |
159
+ | ----------- | ----------------------------------------------------- | ---------------------------- |
160
+ | File | `*-plugin.ts` (kebab-case) | `layout-plugin.ts` |
161
+ | Export | `*Plugin` (camelCase) | `layoutPlugin` |
162
+ | System | `plugin_name__system` (snake_case, double underscore) | `layout_plugin__system` |
163
+ | Init system | `plugin_name_initialize` | `ui_state_plugin_initialize` |
164
+
165
+ ---
166
+
167
+ ## Type utilities
168
+
169
+ ```ts
170
+ export type MyDatabase = Database.Plugin.ToDatabase<typeof myPlugin>;
171
+ export type MyStore = Database.Plugin.ToStore<typeof myPlugin>;
172
+ ```
173
+
174
+ ---
175
+
176
+ ## State minimalism
177
+
178
+ Before adding any `resource`, `component`, or `computed` to a plugin, identify at least one concrete subscriber that reads it. State that
179
+ nothing subscribes to is dead state — it occupies schema space, complicates serialization, and creates false surface area for future
180
+ maintainers.
181
+
182
+ **Before adding state, ask:**
183
+
184
+ 1. Which UI component, computed, or action reads this?
185
+ 2. Can a direct function call replace this entirely (avoiding a transaction that writes state only to read it back immediately)?
186
+ 3. Is this "tracking in case it's needed" — if yes, do not add it.
187
+
188
+ **Computed vs. type utility:**
189
+
190
+ When a derivation is a pure function of a single typed value with no observable subscription needed, prefer a **type utility function** over
191
+ a computed state:
192
+
193
+ ```ts
194
+ // WRONG — computed state when a type utility suffices
195
+ computed: {
196
+ hasMinimumDuration: db => Observe.withMapData([db.resources.clip], clip => clip.duration >= 8);
197
+ }
198
+
199
+ // CORRECT — type utility; zero subscription overhead, testable in pure unit tests
200
+ // types/clip/clip.ts
201
+ export namespace Clip {
202
+ export const hasMinimumDuration = (clip: Clip): boolean => clip.duration >= 8;
203
+ }
204
+ // Usage: Clip.hasMinimumDuration(clip) — no observable, no subscription
205
+ ```
206
+
207
+ Use a computed only when the result must be observable (i.e. UI needs to react when the underlying data changes). Use a type utility when
208
+ the derivation is needed inline and does not require reactivity.
209
+
210
+ ---
211
+
212
+ ## Type safety
213
+
214
+ Plugin code must be **fully and honestly typed**. No `as` / `as unknown as` casts (including on `db.get()` results), no `db: any` /
215
+ `db: unknown` parameters, and no hand-written "surface" / "launder" types that approximate the real database or store and get cast into. We
216
+ hold database-plugin typing to a strict standard: if the types fight you, the fix is to model the plugin correctly (or fix the framework) —
217
+ never to cast around it.
218
+
219
+ ### Type every `db` / `t` / `store` with its real surface
220
+
221
+ A factory, transaction, action, or system parameter must be the plugin's actual database or store type — or a type **derived** from it
222
+ (`Database.Plugin.ToDatabase`, `ToStore`, `Database.Archetype.RowOf`). It must reflect what the framework actually passes. Do **not**
223
+ declare a narrow structural subset and cast the real value into it. If the body needs services, resources, archetypes, or computeds, make
224
+ them genuinely present on the plugin so the parameter type already carries them:
225
+
226
+ - **`extends`** the right base so the store/db includes what you read and write — e.g. a transaction that mutates a resource archetype must
227
+ live on a plugin whose `extends` chain declares that archetype, or the store type won't have it.
228
+ - **`imports`** a dependency plugin to use its surface inside your factory bodies _without_ re-exporting its types into your result type —
229
+ it stays out of downstream `combine` chains and is runtime-merged + identity-deduped.
230
+
231
+ ### Cross-plugin reads follow rules, not casts
232
+
233
+ - A computed factory can read its **base** plugin's resolved computeds, but not a **sibling** computed declared in the same `computed: {}`
234
+ block (circular). To reuse a sibling's logic, either: (a) put the shared computed on a base plugin and `extends` it; or (b) author each
235
+ computed as a standalone exported factory `(db) => Observe<T>` and have the consumer import that factory and call it directly
236
+ (`otherComputed(db)`) instead of reaching for `db.computed.other`. A direct call builds a fresh `Observe` per call — wrap the factory in
237
+ `memoize` from `@adobe/data/cache` (keyed on `db`) if callers must share one instance.
238
+ - An **inline** system/action body gets a contextually-typed `db`; a **standalone** exported system/action object does not, and cannot
239
+ import its own plugin's db type (circular). Prefer inline definitions where a typed `db` matters.
240
+
241
+ ### Brand domain types at the schema
242
+
243
+ Model a branded value as a branded **scalar** (`number & Brand`) and brand it at the component/resource schema:
244
+ `{ ...numberSchema, default: x as Branded }`. The stored shape equals the runtime shape, so the column types as the brand with **zero
245
+ runtime cost** — reads and writes need no conversion.
246
+
247
+ ### We own `@adobe/data` — fix the root, don't cast
248
+
249
+ If clean, strong typing is genuinely blocked by a framework limitation (the library's types cannot express something correctly), that is
250
+ **not** license to cast. Surface it as a change request to `@adobe/data` and fix it at the source, then type the plugin honestly. The repo
251
+ is maintained by **knye** — raise it with him.
252
+
253
+ ### Casts are a last resort, never a convenience
254
+
255
+ Before any cast, you must have exhausted: the real db/store type, `extends` / `imports`, schema-level branding, and an `@adobe/data` change.
256
+ If a cast is still unavoidable for a runtime invariant the compiler cannot see, funnel it through **one** named, documented boundary helper
257
+ and back it with a compile-time drift guard (`Assert<Extends<…>>`) so it can never silently lie.
258
+
259
+ ## Service registration
260
+
261
+ Services must be registered through `Database.Plugin.create({ services: { name: db => createService(db) } })`, not bolted on via
262
+ `Object.assign`.
263
+
264
+ ---
265
+
266
+ ## Execute
267
+
268
+ When creating or modifying a plugin:
269
+
270
+ 1. Verify property order matches the table (imports, extends, services, components, resources, archetypes, indexes, computed, transactions,
271
+ actions, systems).
272
+ 2. Use `extends` for single-parent, `Database.Plugin.combine()` for multiple peers.
273
+ 3. Ensure services only access `db.services` from extended plugins (not forward references).
274
+ 4. Export `type *Database = Database.Plugin.ToDatabase<typeof *Plugin>` when consumers need typed db access.
275
+ 5. Follow naming conventions for files, exports, and systems.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  paths:
3
- - 'packages/**/*-element.ts'
3
+ - '**/*-element.ts'
4
+ - '**/elements/**/*.ts'
5
+ - '**/ui/**/*.ts'
4
6
  ---
5
7
 
6
8
  # Container element authoring
@@ -10,6 +10,8 @@ pure, fully-tested source of truth for the feature. Modelling everything as
10
10
  a single value keeps each transform a small, trivially-testable function of
11
11
  state: collections of entity sub-types plus scalar fields.
12
12
 
13
+ Every feature that has ECS resources or transactions owns a `State` — including
14
+ a host whose aggregate is a single scalar (`{ playing: boolean }`) or even when there is no state in which case use `{}`.
13
15
  ```ts
14
16
  // state.ts — the aggregate, plus the namespace of transforms/derivations.
15
17
  export type State = {
@@ -8,9 +8,11 @@ paths:
8
8
  The bridge that keeps the ecs implementation honest against the `data/` spec.
9
9
  **Test-only**: imported solely by `*.test.ts` and in no facet barrel, so it never
10
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/`.
11
+ a runtime declaration). **Never call `fromState` / `toState` (or a shared
12
+ clear-all + reinsert helper used only for that projection) from systems,
13
+ transactions, or UI** — that is a full-store rewrite, not an O(1) ECS update.
14
+ **Feature-level**: one projection per feature, reused by every transaction test
15
+ and the system tick-loop test — don't nest it under `transaction-database/`.
14
16
 
15
17
  The property, per `{ before, args, after }` case:
16
18
  `toState(apply(fromState(before), args)) ≡ spec(before, args)`.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  paths:
3
3
  - '**/features/*/ecs/**/systems/**/*.ts'
4
+ - '**/systems/*.ts'
4
5
  ---
5
6
 
6
7
  # database/systems/ — the real-time tick loop
@@ -63,9 +64,8 @@ const plugin = Database.Plugin.create({
63
64
  HUD) are notified. Reserve direct `db.store` writes for the hot per-row paths
64
65
  (movement, aging) that run on every entity every frame and shouldn't pay
65
66
  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.
67
+ - **Optimize for execution speed in systems** if there are multiple items, then make sure we are using efficient store queries and reading/writing minimal data columns. If this is not hot path, then the simplified data functions can be used, but in hot paths we only consider data functions to be specification. (ie: only use them if it's clearly not a hot path or you cannot write anything more performant by directly using ecs store)
68
+ - **The step math may live in `data/`.** but only for non-hot path
69
69
  - **Iterate archetypes per `archetypes.md`.** Express selection with
70
70
  `queryArchetypes(include, { exclude })`; when a system destroys/migrates rows
71
71
  (bullets expiring, entities dying) iterate **tail → head** so hole-fills don't
@@ -96,8 +96,8 @@ const plugin = Database.Plugin.create({
96
96
  - Games or simulations with immediate mode rendering may want to store a session
97
97
  resource of the canvas and render to it within a system. @adobe/data-gpu provides
98
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.
99
+ - All simulation systems should be optimized for performance.
100
+ Avoid allocating intermediate objects if possible; read/write columns directly if hot-path. You can use transactions if needed, for instance if we are writing non hot path things which we have other things observing. (Only transactions are observable, not direct writes)
101
101
 
102
102
  ## Driving the loop — the scheduler
103
103
 
@@ -131,3 +131,59 @@ an inline body grows too large — the schedule and registration still live inli
131
131
  `system-database/tick-loop.test.ts`. Test **selection/detection** logic (which
132
132
  entities collide, which pair resolves) separately, with seeded edge-case geometries
133
133
  — it's a system concern, not a transaction, and where subtle bugs hide.
134
+
135
+ # Archetypes — iteration rules
136
+
137
+ ## Express selection in the query, not the loop
138
+
139
+ `queryArchetypes(include, { exclude })` accepts both required and excluded
140
+ component lists. Use them. Don't query a wider set and then skip rows or
141
+ archetypes with an `if`.
142
+
143
+ ```ts
144
+ // ❌ post-filter
145
+ for (const arch of db.store.queryArchetypes(["position", "rotation"])) {
146
+ if (arch.components.has("_worldMatrix")) continue;
147
+ ...
148
+ }
149
+
150
+ // ✅ declarative
151
+ for (const arch of db.store.queryArchetypes(
152
+ ["position", "rotation"],
153
+ { exclude: ["_worldMatrix"] },
154
+ )) {
155
+ ...
156
+ }
157
+ ```
158
+
159
+ ## When every row migrates out, iterate tail → head
160
+
161
+ Archetypes are densely packed. Removing or migrating a row that isn't the
162
+ last one triggers a hole-fill: the tail row is moved into the gap.
163
+ Iterating `0 → rowCount-1` while migrating every row pays this cost on
164
+ every iteration. Iterating `rowCount-1 → 0` means each removal is from
165
+ the tail — no shift, indices ahead of the cursor stay valid.
166
+
167
+ ```ts
168
+ // ❌ shifts the tail into every hole, and the snapshot allocation is
169
+ // only there to survive the shifts.
170
+ const ids = [...];
171
+ for (let i = 0; i < arch.rowCount; i++) ids[i] = arch.columns.id.get(i);
172
+ for (const id of ids) db.store.update(id, { _worldMatrix: Mat4x4.identity });
173
+
174
+ // ✅ no shifts, no allocation
175
+ for (let i = arch.rowCount - 1; i >= 0; i--) {
176
+ db.store.update(arch.columns.id.get(i), { _worldMatrix: Mat4x4.identity });
177
+ }
178
+ ```
179
+
180
+ If only *some* rows migrate (filter inside the loop), snapshot the ids
181
+ you'll touch — forward iteration is fine because rows you don't touch
182
+ stay put.
183
+
184
+ ## Don't snapshot what the query already filters
185
+
186
+ A snapshot of "all entity ids in this archetype right now" is only
187
+ needed when forward iteration would invalidate. Reverse iteration
188
+ removes the need; an `exclude` clause removes the need to look at rows
189
+ that don't qualify in the first place.
@@ -0,0 +1,226 @@
1
+ ---
2
+ paths:
3
+ - 'src/**/*element*.ts'
4
+ - 'src/**/elements/**/*.ts'
5
+ - 'src/**/ui/**/*-element.ts'
6
+ ---
7
+
8
+ # Binding elements
9
+
10
+ Binding elements are thin wires between application state and the UI — not designed for unit-testability. Non-trivial logic doesn't belong
11
+ here, it must be pushed into either the action/transaction layer or into presentations.
12
+
13
+ ## Scope
14
+
15
+ This rule applies to **binding elements** — Lit elements that read state from a service via `useObservableValues`. If you are not using Lit then still try to adhere to a similar pattern to this with the same focus on separation of concerns and functional presentation if possible.
16
+ The discriminator is the base class:
17
+
18
+ - `ApplicationElement<T>` — from either `@adobe/data-lit` or `src/applications/application-element.ts`. Every app-level binding element
19
+ extends this (directly or transitively). Concrete app bases like `FooElement`, `VideoGenerationElement`, `ImageEditorElement`,
20
+ `StoryGenerationElement`, `ImageGenerationElement` are thin subclasses that bind a specific `MainService` type and inherit this contract.
21
+ - `DatabaseElement<T>` — the lower-level `@adobe/data-lit` base used by apps with no intermediate `*Element` layer (e.g.
22
+ `HelloWorldElement`).
23
+
24
+ Plain `LitElement` subclasses that expose their API through `@property` alone (shared UI primitives like `media-timeline`, `model-picker`,
25
+ `toast-with-image`) are **not** binding elements. They don't subscribe to a service; their props are the entire contract. Out of scope for
26
+ this rule.
27
+
28
+ Presentation files (`*-presentation.ts`), CSS files (`*.css.ts`), tests (`*.test.ts`, `*.spec.ts`), and stories are also out of scope.
29
+
30
+ ## The shape
31
+
32
+ A binding element is a thin wire between a service and a presentation. The class body contains:
33
+
34
+ 1. Optional static `styles` property definition
35
+ 2. Zero or more `@property` declarations (whitelisted below).
36
+ 3. One `render()` method whose body is: one `useObservableValues(...)` call, another lifecycle hook call (in rare cases), a null-guard, and
37
+ `return presentation.render({ ...values, ...callbacks })`.
38
+
39
+ No other members. No `@state`. No private handler fields. No lifecycle methods (`connectedCallback`, `updated`, `firstUpdated`,
40
+ `disconnectedCallback`) except on app-entrypoint classes where the lifecycle concern is app-boot shaped but cannot be expressed as a hook
41
+ (see `.claude/rules/hooks.md` for hook-first lifecycle).
42
+
43
+ ## `@property` whitelist
44
+
45
+ The allowed set depends on **class role**:
46
+
47
+ - **App entrypoint classes** — the (typically empty-body) subclass of `ApplicationElement<T>` that binds a specific `MainService` type.
48
+ Examples: `VideoGenerationElement`, `HelloWorldElement`, `TodoElement`. These accept injected services and top-level inputs from the host
49
+ shell.
50
+ - **Concrete binding elements** — classes that extend an app entrypoint (or another concrete binding) and render UI. Examples: `Hud`,
51
+ `FooRotationControlElement`, `CommunitySubmissionConnector`. These accept only:
52
+ - Entity-id-shaped locator props (`entityId`, `entityIds`, `trackId`) that identify _where_ to load this element's data from.
53
+ - Rare parent-forwarded flags passed straight to the presentation without branching on them (e.g. `highVolumeFlag` in `audio-view.ts`).
54
+ Prefer promoting the flag into a service computed.
55
+
56
+ All other values come from the service.
57
+
58
+ ## Observed values must be raw reads
59
+
60
+ Every expression inside `useObservableValues` is either a direct read of a service observable (`this.service.observe.*`) or a direct read of
61
+ a computed (`this.service.computed.*`). No `.map`, `.filter`, `.reduce`, arithmetic, conditionals, or `Observe.withMap`-inline composition.
62
+
63
+ If derivation is needed: ECS Database apps place it in a database computed (`plugins/computed`, read back as `this.service.computed.*`);
64
+ non-ECS apps place it in a dependent-state service function (`dependent-state-service`). If it is used only by this element, define it
65
+ locally in a sibling file and import it. Computed and dependent-state must use utilities from `Obsereve.*` rather than trying to call
66
+ observable functions.
67
+
68
+ **Bad - doesn't use utilities from `Obsereve.*`**
69
+
70
+ ```ts
71
+ return notify => {
72
+ const unobserve = oValue(value => {
73
+ notify(value ** 2);
74
+ });
75
+ return unobserve;
76
+ };
77
+ ```
78
+
79
+ **Good - uses utilities from `Obsereve.*`**
80
+
81
+ ```ts
82
+ return Observe.withMap(oValue => value ** 2);
83
+ ```
84
+
85
+ ## Callbacks are one-liners
86
+
87
+ - Each callback passed to the presentation invokes a UI action with optional arguments or a transaction with optional arguments.
88
+ - Callbacks must not encode domain knowledge, branch on state, or build complex shapes. Logic belongs in the UI action layer or transactions
89
+ layer.
90
+ - UI action functions and transactions must be called only via `this.service`, they must never be imported directly.
91
+ - Never consume the return value of a UI action or transaction call.
92
+ - Simple argument transformations are allowed — arithmetic, negation, fixed arguments, simple ternaries that select an argument value.
93
+
94
+ ```ts
95
+ render() {
96
+ return presentation.render({
97
+ // UI action call without parameters — good
98
+ saveProgress: this.service.actions.saveProgress,
99
+ // UI action call with fixed argument — good
100
+ setLandscape: () => this.service.actions.setOrientation('landscape'),
101
+ // UI action call with simple ternary — good
102
+ jump: (heroMode: boolean) => this.service.actions.move(heroMode ? 100 : 10),
103
+ // transaction call - good
104
+ play: this.service.transactions.playTrack
105
+ });
106
+ }
107
+ ```
108
+
109
+ ## No derivation math in `render()`
110
+
111
+ **Derivation math** is forbidden in `render()`. Simple argument assembly in callbacks is the only exception — see **Action callbacks are
112
+ one-liners** above.
113
+
114
+ ## Lifecycle goes through hooks
115
+
116
+ Any need for mount/unmount/update behavior routes through a hook. **Reuse order, one hook per file, presentation vs binding placement, and
117
+ testing notes:** `.claude/rules/hooks.md`.
118
+
119
+ Never inline lifecycle logic in the binding. App-entrypoint classes may override lifecycle methods when the concern is app-boot shaped
120
+ (service wiring, ancestor lookup); even then, prefer a hook.
121
+
122
+ Rare cases when an additional hook can be used in `render()`:
123
+
124
+ - focus on element
125
+ - scroll to element
126
+ - clean up on un-mount
127
+ - and other lifecycle-bound behaviour
128
+
129
+ Use `useEffect` for cleanup on unmount rather than `disconnectedCallback`:
130
+
131
+ ```ts
132
+ render() {
133
+ useEffect(() => {
134
+ return () => this.service.actions.someCleanup();
135
+ }, []); // empty array → runs cleanup on unmount only
136
+
137
+ const values = useObservableValues(() => ({ ... }));
138
+ if (!values) return nothing;
139
+ return presentation.render(values);
140
+ }
141
+ ```
142
+
143
+ In binding-elements `render()` doesn't need to be explicitly decorated with `withHooks()` because it's done implicitly in the
144
+ `ApplicationElement` base class.
145
+
146
+ ## No timers, emitters, or broadcast channels
147
+
148
+ - Never `setInterval`, `setTimeout`, or `requestAnimationFrame`.
149
+ - Never `new EventEmitter`. Cross-element state flows through service observables.
150
+ - Never `new BroadcastChannel`. Cross-tab concerns go through a dedicated service.
151
+
152
+ ## No direct database reads
153
+
154
+ Never call `this.service.get()`, `this.service.read()`, `this.service.select()`, or any synchronous accessor. The service interface does not
155
+ expose them by design; reaching for them means a missing computed.
156
+
157
+ ## Presentation contract
158
+
159
+ A binding element imports a sibling `*-presentation.ts` file that exports exactly `render` (required) and optionally `unlocalized`. Nothing
160
+ else — no types, no styles, no helpers. Consumers derive the props type via `Parameters<typeof render>[0]`. Presentation stays hook-free by
161
+ default; element hooks live here or in shared modules per `.claude/rules/hooks.md` and `.claude/rules/presentation.md`.
162
+
163
+ **For refactors whose stated scope does not include presentations**, the matching `*-presentation.ts` file must not change. Verify with
164
+ `git diff --name-only | grep presentation`. If a scope-limited refactor requires a presentation signature change, stop and reconsider the
165
+ binding-side helper — the scope fence exists because presentations are the public contract between binding and render output.
166
+
167
+ Callback prop names use **verb** or **verbNoun** form. Never `on*` prefix. The binding-side variable matches the presentation prop name 1:1.
168
+ See `.claude/rules/presentation.md` for full examples.
169
+
170
+ ## Size budget
171
+
172
+ A compliant binding element file is small enough that the `render()` body fits on screen without scrolling. If it doesn't, a concern has not
173
+ been extracted yet — find it.
174
+
175
+ ## Deletion test
176
+
177
+ Delete everything except the class body. What remains should be: static styles assignment, property declarations, one
178
+ `useObservableValues(...)` call, another lifecycle hook call (in rare cases), a null-guard, and one render call. Anything else is a
179
+ violation.
180
+
181
+ ## Good examples (in-tree)
182
+
183
+ - `src/applications/image-generation/elements/sub-navigation-panel/sub-navigation-panel.ts` — concrete binding element; one-liner action
184
+ callback
185
+ - `src/applications/foo/elements/properties-panel/views/audio-view.ts` — entity-id-only + parent-forwarded flag
186
+ - `src/applications/image-editor/elements/hud/hud.ts` — concrete binding element (extends `ImageEditorElement`); `useObservableValues` reads
187
+ a dependentState observable; forwards to presentation with spread.
188
+ - `src/applications/video-generation/elements/commerce-coachmark/commerce-coachmark.ts` — localization-only binding; trivially small and
189
+ compliant.
190
+
191
+ ## Counter-example (do not land)
192
+
193
+ ```ts
194
+ @customElement(tagName)
195
+ export class BadControlElement extends FooElement {
196
+ @property({ attribute: false }) entityIds: readonly Entity[] = [];
197
+ @state() private _cachedSum = 0; // violation: @state
198
+ private _handleChange?: (e: Event) => void; // violation: private handler
199
+
200
+ connectedCallback() {
201
+ // violation: lifecycle override
202
+ super.connectedCallback();
203
+ this._handleChange = e => (this._cachedSum += Number((e.target as HTMLInputElement).value));
204
+ }
205
+
206
+ @withHooks // Redundant
207
+ render() {
208
+ const values = useObservableValues(() => ({
209
+ items: this.service.observe.select(/* ... */),
210
+ total: Observe.withMap(
211
+ // violation: logic in observed values
212
+ this.service.observe.select(/* ... */),
213
+ rows => rows.reduce((a, r) => a + r.duration, 0)
214
+ ),
215
+ }));
216
+ const clamped = Math.max(0, Math.min(100, values.total)); // violation: derivation in render
217
+ const onSave = (v: number) => {
218
+ // violation: logic in callback + onX name
219
+ const normalized = Math.round(v * 100) / 100;
220
+ if (!Number.isFinite(normalized)) return;
221
+ this.service.transactions.updateThing({ value: normalized });
222
+ };
223
+ return presentation.render({ total: clamped, onSave });
224
+ }
225
+ }
226
+ ```
@@ -0,0 +1,27 @@
1
+ ---
2
+ paths:
3
+ - 'src/**/elements/**/*.ts'
4
+ - 'src/**/ui/**/*.ts'
5
+ ---
6
+
7
+ # Lazy Wrapper
8
+
9
+ A lazy element wrapper is a strongly typed function that emits a lit template and dynamically loads the actual element code to ensure no loading till first real usage.
10
+ Consumers of this element DO NOT use the <todo-row...> markup, instead they import this lazy function and call it with `{TodoRow({...})}`.
11
+
12
+ # Example from data-lit-todo
13
+
14
+ # todo-row.ts
15
+ import { html, type TemplateResult } from "lit";
16
+ import type { Entity } from "@adobe/data/ecs";
17
+
18
+ export const TodoRow = (args: {
19
+ entity: Entity;
20
+ index: number;
21
+ }): TemplateResult => {
22
+ void import("./todo-row-element.js");
23
+ return html`<todo-row
24
+ .entity=${args.entity}
25
+ .index=${args.index}
26
+ ></todo-row>`;
27
+ };