@adobe/data-ai 0.10.23 → 0.10.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/README.md +8 -3
- package/.claude/rules/ecs.md +1 -1
- package/.claude/rules/element.md +9 -0
- package/.claude/rules/features/data/components.md +6 -3
- package/.claude/rules/features/data/index.md +4 -1
- package/.claude/rules/features/data/state.md +211 -0
- package/.claude/rules/features/ecs/conformance.md +12 -6
- package/.claude/rules/features/index.md +4 -0
- package/.claude/rules/features/services/index.md +2 -0
- package/.claude/rules/features/services/main-service/actions.md +72 -0
- package/.claude/rules/features/services/main-service/archetypes.md +51 -0
- package/.claude/rules/features/services/main-service/components.md +43 -0
- package/.claude/rules/features/services/main-service/computed.md +84 -0
- package/.claude/rules/features/services/main-service/conformance.md +97 -0
- package/.claude/rules/features/services/main-service/core-database.md +50 -0
- package/.claude/rules/features/services/main-service/index.md +189 -0
- package/.claude/rules/features/services/main-service/indexes.md +65 -0
- package/.claude/rules/features/services/main-service/resources.md +30 -0
- package/.claude/rules/features/services/main-service/services.md +27 -0
- package/.claude/rules/features/services/main-service/systems.md +145 -0
- package/.claude/rules/features/services/main-service/transactions.md +61 -0
- package/.claude/rules/features/ui/index.md +2 -0
- package/.claude/rules/service.md +25 -75
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +2 -0
- package/skills/build-app-entry/SKILL.md +2 -0
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-core-database/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +2 -0
- package/skills/build-feature/SKILL.md +3 -0
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-service-database/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-spec/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +2 -0
- package/skills/build-transactions/SKILL.md +2 -0
- package/skills/build-ui/SKILL.md +2 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/structure/SKILL.md +2 -0
- package/.claude/rules/features/ui/binding-element.md +0 -226
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/main-service/transaction-database/transactions/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# database/transactions/ — atomic mutations
|
|
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 mutation per file: a function taking the transaction store as its
|
|
11
|
+
first argument and pure `data/` args, returning `void`. It reads and
|
|
12
|
+
writes `t.resources` / entities; all its writes commit or roll back
|
|
13
|
+
together.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PlayMoveArgs } from "../../../data/play-move-args/play-move-args.js";
|
|
17
|
+
import type { CoreDatabase } from "../../core-database/core-database.js";
|
|
18
|
+
|
|
19
|
+
export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
|
|
20
|
+
// guard, then mutate t.resources / entities / archetypes
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Type the store parameter as `<Layer>.Store`: **`CoreDatabase.Store`** for a
|
|
25
|
+
transaction that reads/writes entities, resources, or archetypes (the common
|
|
26
|
+
case — all schema lives in `core-database`); **`IndexDatabase.Store`** the
|
|
27
|
+
moment it reads an index. A store *is* the transaction context: it carries the
|
|
28
|
+
index handles and the initiating `t.userId`. There is no separate
|
|
29
|
+
transaction-context type.
|
|
30
|
+
- Keep transactions idempotent under replay where sync/P2P applies —
|
|
31
|
+
validate and silently return on illegal or out-of-turn input rather
|
|
32
|
+
than throwing.
|
|
33
|
+
- Decisions come from pure `data/` helpers; the transaction only applies the
|
|
34
|
+
result — read the touched slice, call the `data/` transform, write the diff.
|
|
35
|
+
- Keep transaction files **single-export** (the `transactions/` barrel is
|
|
36
|
+
`export *`-ed into the plugin facet, so a second export would pollute it).
|
|
37
|
+
- **ECS-based feature (no `data/state/`):** there are no conformance cases, so
|
|
38
|
+
**every transaction carries its own unit test** — `Store.create(plugin)`, run the
|
|
39
|
+
transaction, assert the resulting resources/entities/archetypes. That test is the
|
|
40
|
+
direct substitute for the conformance oracle (see `../../index.md`, Two modes). The
|
|
41
|
+
bullet below applies only to **state-based** features.
|
|
42
|
+
- **Conformance is wired once, centrally, and auto-paired** — not per-file. The
|
|
43
|
+
feature's single `conformance/conformance.test.ts` `Conformance.checkFeature(spec)`
|
|
44
|
+
call conforms transactions: it pulls them off **`plugin.transactions`** (the
|
|
45
|
+
registered facet), pairs each to the **same-named** `data/state` transition, and
|
|
46
|
+
conforms it — seed `fromState(before)`, apply, `Match.assert` `toState ≡ after`
|
|
47
|
+
(state only; service effects are asserted through the action). There is **no**
|
|
48
|
+
`define`/`conforms` adapter and **no** `covers` guard. A transaction taking
|
|
49
|
+
**entity ids** takes them under the transition's own arg key (`{ id }`); the runner
|
|
50
|
+
resolves each id the case's `args` schema marks via the id→entity map `fromState` returns. A
|
|
51
|
+
transaction with **no same-named transition** is infrastructure (`setInput`,
|
|
52
|
+
`setBounds`) or the drag UI op (`dragTodo`) or system-dispatched — it is simply
|
|
53
|
+
**skipped**, no guard needed. Don't add a per-item adapter for a renamed op; add a
|
|
54
|
+
thin same-named transaction instead (see `conformance.md`).
|
|
55
|
+
- An `index.ts` barrel feeds the `transactions` plugin facet — so it must
|
|
56
|
+
re-export **only** the mutations. A read/query helper shared by several
|
|
57
|
+
transactions (`readShip`, `readBoard` — a `(t) => value` function) may live
|
|
58
|
+
beside them in `transactions/`, but keep it **out of the barrel**, or it gets
|
|
59
|
+
registered as a dispatchable transaction. (These are related helpers, so they
|
|
60
|
+
belong here, not in a separate folder — `global/cohesion.md`'s peer-level caution
|
|
61
|
+
doesn't apply.)
|
|
@@ -6,6 +6,8 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# ui/ — user interface
|
|
8
8
|
|
|
9
|
+
> **Legacy feature** (has `data/state/` or `services/main-service/`)? Its data and services follow the legacy rules; the ui rules here still apply.
|
|
10
|
+
|
|
9
11
|
The top layer: UI that binds the feature's `MainService` database (`ecs/main-service.ts`)
|
|
10
12
|
to what the user sees. It owns no business logic — it subscribes to state, hands it to
|
|
11
13
|
a pure presentation, and wires user input to `service.actions.*`.
|
package/.claude/rules/service.md
CHANGED
|
@@ -6,7 +6,7 @@ paths:
|
|
|
6
6
|
|
|
7
7
|
# Service authoring
|
|
8
8
|
|
|
9
|
-
> Inside a feature, `features/services/index.md`
|
|
9
|
+
> Inside a feature, `features/services/index.md` covers the service layer; database-bound factories in `ecs/services/` follow `features/ecs/services.md`.
|
|
10
10
|
|
|
11
11
|
Asynchronous data services. Live in the `services/` layer. Adhere to the namespace rule for type and function organization.
|
|
12
12
|
|
|
@@ -44,74 +44,26 @@ Compile-time check: `Assert<AsyncDataService.IsValid<ServiceInterface>>`.
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## Folder structure
|
|
48
|
-
|
|
49
|
-
**MUST NOT** place implementation files alongside the interface file. The `<name>-service/` folder MUST contain only the interface file and
|
|
50
|
-
`public.ts`.
|
|
47
|
+
## Folder structure
|
|
51
48
|
|
|
52
49
|
```
|
|
53
50
|
services/<name>-service/
|
|
54
|
-
<name>-service.ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*.ts ← helpers, utils, constants (implementation-private)
|
|
59
|
-
*.test.ts ← tests live here alongside implementation
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Single-file exception**: a bare factory with no helpers, no utilities, and no constants MAY live as a flat file alongside the interface.
|
|
63
|
-
The moment any additional implementation file is needed, the factory MUST move into a sub-folder.
|
|
64
|
-
|
|
65
|
-
**Interface file** (`<name>-service.ts`) — interface declaration and types that define the public service contract. MUST NOT import from any
|
|
66
|
-
implementation file or sub-folder. At the bottom of the file, exports the namespace: `export * as ServiceName from './public.js'`. This
|
|
67
|
-
merges the interface type and the factory namespace under one identifier — callers type against the interface and invoke factory functions
|
|
68
|
-
through the same name.
|
|
69
|
-
|
|
70
|
-
**Separate types file at the interface level** — only permitted if it contains exclusively public-contract types (types that appear in the
|
|
71
|
-
interface's method signatures or `Observe<>` generic arguments). Implementation-detail types — third-party API shapes, `window.*` global
|
|
72
|
-
augmentations, internal utilities — MUST live inside the implementation sub-folder.
|
|
73
|
-
|
|
74
|
-
**`*.test.ts` files MUST NOT appear at the interface folder level.** Every test file belongs inside the implementation sub-folder next to
|
|
75
|
-
the code it tests; there is nothing at the interface level to test.
|
|
76
|
-
|
|
77
|
-
**Implementation sub-folder** — all implementation logic including the factory, utilities, constants, helpers, and their tests. MUST NOT
|
|
78
|
-
depend on the interface file's types via the `public.js` re-export; import directly from `<name>-service.js`.
|
|
79
|
-
|
|
80
|
-
**public.ts** — MUST re-export the factory function and any other public exports from the implementation sub-folder. Does NOT re-export the
|
|
81
|
-
interface — the interface is exported directly from `<name>-service.ts`.
|
|
82
|
-
|
|
83
|
-
**No classes** — factory function or static plain object only. `this` bindings are brittle and block higher-order composition.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## Factory export shape — `singletonFactory` + dual export
|
|
88
|
-
|
|
89
|
-
Every service factory MUST be wrapped with `singletonFactory` from `factory-functions.js`. This guarantees one instance per argument tuple
|
|
90
|
-
when the same factory is called multiple times, preventing duplicate subscriptions and torn state.
|
|
91
|
-
|
|
92
|
-
Export **both** the raw `factory` and the singleton-wrapped `create`:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
// <name>-service/<impl-name>/create-<name>-service.ts
|
|
96
|
-
import { singletonFactory } from 'factory-functions.js';
|
|
97
|
-
|
|
98
|
-
export const factory = (deps: Deps): NameService => {
|
|
99
|
-
// implementation
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// `factory` — for test injection (bypasses the singleton cache; each test gets a fresh instance)
|
|
103
|
-
// `create` — for production use (singleton per deps-tuple)
|
|
104
|
-
export const create = singletonFactory(factory);
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
`public.ts` re-exports both:
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
export { create, factory } from './<impl-name>/create-<name>-service.js';
|
|
51
|
+
<name>-service.ts ← interface + contract types; `export * as ServiceName from './public.js'` at bottom
|
|
52
|
+
create.ts ← the implementation factory (or a factory that throws when the app must inject it)
|
|
53
|
+
create-fake.ts ← an inert fake with every method present
|
|
54
|
+
public.ts ← re-exports `create` and `createFake`
|
|
111
55
|
```
|
|
112
56
|
|
|
113
|
-
**
|
|
114
|
-
|
|
57
|
+
- **Interface file** (`<name>-service.ts`) — the interface and the types in its method
|
|
58
|
+
signatures. It never imports an implementation file. The namespace export at the
|
|
59
|
+
bottom merges the interface type and the factories under one name: callers type
|
|
60
|
+
against the interface and call `ServiceName.create`.
|
|
61
|
+
- **An implementation that grows helpers** moves into a named sub-folder
|
|
62
|
+
(`create-<name>-service/` with its helpers and their tests); `public.ts` re-exports
|
|
63
|
+
its factory. Implementation-only types (third-party API shapes, `window.*`
|
|
64
|
+
augmentations) live there too, never in the interface file.
|
|
65
|
+
- **No classes** — factory functions or plain objects only. `this` bindings are brittle
|
|
66
|
+
and block higher-order composition.
|
|
115
67
|
|
|
116
68
|
---
|
|
117
69
|
|
|
@@ -124,7 +76,7 @@ identifiers in any method signature, property name, or type parameter.
|
|
|
124
76
|
|
|
125
77
|
```ts
|
|
126
78
|
interface SessionService extends Service {
|
|
127
|
-
readonly
|
|
79
|
+
readonly analyticsSdk: AnalyticsSdk; // UI now knows which SDK is used
|
|
128
80
|
}
|
|
129
81
|
```
|
|
130
82
|
|
|
@@ -136,7 +88,7 @@ interface SessionService extends Service {
|
|
|
136
88
|
}
|
|
137
89
|
```
|
|
138
90
|
|
|
139
|
-
The interface is the contract between UI and data layers. If the underlying tool is swapped (
|
|
91
|
+
The interface is the contract between UI and data layers. If the underlying tool is swapped (one SDK → another, cookies → localStorage),
|
|
140
92
|
the interface must not change. Any leak of an internal tool name into the interface couples the UI to an implementation detail it must not
|
|
141
93
|
know about.
|
|
142
94
|
|
|
@@ -146,21 +98,19 @@ know about.
|
|
|
146
98
|
|
|
147
99
|
When creating or modifying a service:
|
|
148
100
|
|
|
149
|
-
1. Place in `services/<name>-service/`.
|
|
101
|
+
1. Place it in `services/<name>-service/`.
|
|
150
102
|
2. Interface in `<name>-service.ts` — types only, extend `Service`, add `Assert<AsyncDataService.IsValid<>>`.
|
|
151
|
-
3.
|
|
103
|
+
3. Add `create.ts`, `create-fake.ts` and `public.ts`; move the implementation into a sub-folder once it needs helpers.
|
|
152
104
|
4. At the bottom of `<name>-service.ts`, add `export * as ServiceName from './public.js'`.
|
|
153
|
-
5.
|
|
154
|
-
6. For front-end services: actions return void only; observables observe Data or Service only. For back-end services: functions return
|
|
105
|
+
5. For front-end services: actions return void only; observables observe Data or Service only. For back-end services: functions return
|
|
155
106
|
`Promise<Data>` or `AsyncGenerator<Data>`.
|
|
156
107
|
|
|
157
108
|
---
|
|
158
109
|
|
|
159
110
|
## Verify
|
|
160
111
|
|
|
161
|
-
When any `
|
|
112
|
+
When any `services/**` file is in the diff, confirm:
|
|
162
113
|
|
|
163
|
-
1. **Interface
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
3. **Interface file purity** — `<name>-service.ts` contains no imports from implementation sub-folders.
|
|
114
|
+
1. **Interface file purity** — `<name>-service.ts` imports no implementation file.
|
|
115
|
+
2. **`public.ts` completeness** — it re-exports `create` and `createFake` (and any other public factory).
|
|
116
|
+
3. **No implementation-only types in the interface** — they live with the implementation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.25",
|
|
4
4
|
"description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"skills"
|
|
30
30
|
],
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@adobe/data": "0.10.
|
|
32
|
+
"@adobe/data": "0.10.25"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
36
|
"vitest": "^1.6.0",
|
|
37
|
-
"@adobe/data": "0.10.
|
|
37
|
+
"@adobe/data": "0.10.25"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create `ecs/actions/`: `action-database.ts` (extends the previous layer, adds `actions`
|
|
9
11
|
from `./index.js`), one action per file, and a barrel. Every spec action has a
|
|
10
12
|
same-named action. It awaits services and commits through at most one transaction
|
|
@@ -5,6 +5,8 @@ input: app
|
|
|
5
5
|
output: app
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
A legacy feature (has `data/state/` or `services/main-service/`) exposes its `MainService` from `services/main-service/`; compose it from there.
|
|
9
|
+
|
|
8
10
|
Create `src/app/`:
|
|
9
11
|
|
|
10
12
|
- `schema.ts` — the composed plugin: the base feature's `MainService.plugin`, `imports`
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Skip if the spec has no derivations and the UI needs no derived values.
|
|
9
11
|
|
|
10
12
|
Create `ecs/computed/`: `computed-database.ts` (extends the previous layer, adds
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create:
|
|
9
11
|
|
|
10
12
|
- `ecs/core/archetypes.ts` — `Database.archetypes(components, { … })`, spreading each
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create the feature's `data/` layer: pure Data declarations, depending only on
|
|
9
11
|
`@adobe/data` and the `data/` of features this one builds on.
|
|
10
12
|
|
|
@@ -5,6 +5,9 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
If the feature already has `data/state/` or `services/main-service/`, it uses the legacy
|
|
9
|
+
state-based layout: extend it in place per those legacy rules; don't migrate it unless asked.
|
|
10
|
+
|
|
8
11
|
/graph-execute
|
|
9
12
|
/build-data
|
|
10
13
|
/build-services
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Skip if the feature needs no indexed lookups.
|
|
9
11
|
|
|
10
12
|
Create `ecs/indexes/`: `index-database.ts` (extends `CoreDatabase`, adds `indexes` from
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Skip if the feature has no services.
|
|
9
11
|
|
|
10
12
|
Create `ecs/services/service-database.ts` (extends the previous layer), registering
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Skip if the feature talks to nothing outside itself.
|
|
9
11
|
|
|
10
12
|
Create one `services/<name>-service/` namespace per service: the interface (an async
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create the feature's `spec/` folder, the specification the ECS will be conformed to:
|
|
9
11
|
|
|
10
12
|
- `state.ts` — `State` built from `data/` types (singletons named for resources,
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Skip unless the feature is real-time (games, simulations).
|
|
9
11
|
|
|
10
12
|
Create `ecs/systems/system-database.ts`: `Database.Plugin.create({ extends:
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create `ecs/transactions/`: `transaction-database.ts` (extends the previous layer, adds
|
|
9
11
|
`transactions` from `./index.js`), one mutation per file, and a barrel. Type the store
|
|
10
12
|
`CoreDatabase.Store`, or `IndexDatabase.Store` once it reads an index. Guard by entity
|
package/skills/build-ui/SKILL.md
CHANGED
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? Extend it in place per the legacy rules; don't migrate unless asked.
|
|
9
|
+
|
|
8
10
|
Create `ui/`: one folder per UI unit —
|
|
9
11
|
`<name>/{<name>.ts (lazy wrapper), <name>-element.ts (container), <name>-presentation.ts (pure render), <name>.css.ts}`.
|
|
10
12
|
The element subscribes to `MainService` state and calls `service.actions.*`; the
|
package/skills/review/SKILL.md
CHANGED
|
@@ -6,6 +6,8 @@ input: a path to built output (a layer, a feature, or a whole app)
|
|
|
6
6
|
output: a review — OPTIMAL, or a list of issues each tagged code | skill | rule
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
Check a legacy feature (has `data/state/` or `services/main-service/`) against the legacy rules; don't flag the layout itself.
|
|
10
|
+
|
|
9
11
|
Read the files under the given path and assess them against the relevant `features/`
|
|
10
12
|
rules (each layer's rule and the cross-cutting rules — `namespace`, `data-modelling`,
|
|
11
13
|
`type-casts`, `cohesion`, …):
|
|
@@ -3,5 +3,7 @@ name: structure
|
|
|
3
3
|
description: Use when laying out or reasoning about a feature's file/folder structure and layer dependencies.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Legacy feature (has `data/state/` or `services/main-service/`)? It keeps its state-based layout (the legacy rules); don't migrate unless asked.
|
|
7
|
+
|
|
6
8
|
See `features/index.md` (and the per-folder `features/` rules) for how features are
|
|
7
9
|
structured and layered.
|
|
@@ -1,226 +0,0 @@
|
|
|
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 (allowed 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 `hooks.md` for hook-first lifecycle).
|
|
42
|
-
|
|
43
|
-
## `@property` allowlist
|
|
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.actions.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:** `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 `hooks.md` and `features/ui/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 `features/ui/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.actions.updateThing({ value: normalized });
|
|
222
|
-
};
|
|
223
|
-
return presentation.render({ total: clamped, onSave });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|