@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.
- package/.claude/rules/cohesion.md +2 -2
- package/.claude/rules/data-modelling.md +2 -0
- package/.claude/rules/ecs.md +275 -0
- package/.claude/rules/element.md +3 -1
- package/.claude/rules/features/data/state.md +2 -0
- package/.claude/rules/features/ecs/conformance.md +5 -3
- package/.claude/rules/features/ecs/systems.md +61 -5
- package/.claude/rules/features/ui/binding-element.md +226 -0
- package/.claude/rules/features/ui/lazy-wrapper.md +27 -0
- package/.claude/rules/features/ui/presentation-test.md +38 -0
- package/.claude/rules/{presentation.md → features/ui/presentation.md} +2 -32
- package/.claude/rules/function-references.md +2 -2
- package/.claude/rules/hooks.md +57 -0
- package/.claude/rules/layout.md +132 -0
- package/.claude/rules/lazy-element.md +1 -1
- package/.claude/rules/lit.md +115 -0
- package/.claude/rules/namespace.md +93 -135
- package/.claude/rules/observe.md +183 -0
- package/.claude/rules/react.md +102 -0
- package/.claude/rules/service.md +164 -0
- package/.claude/rules/type-casts.md +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -4
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +3 -1
- package/skills/build-application/SKILL.md +5 -3
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +4 -0
- package/skills/build-feature/SKILL.md +12 -15
- package/skills/build-game/SKILL.md +4 -2
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +3 -3
- package/skills/graph-execute/SKILL.md +13 -0
- package/.claude/rules/archetypes.md +0 -60
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/services/**/*.ts'
|
|
4
|
+
- '**/*-plugin.ts'
|
|
5
|
+
- '**/*-plugin/**/*.ts'
|
|
6
|
+
- '**/*dependent-state*.ts'
|
|
7
|
+
- '**/*dependent-state*/**/*.ts'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Observe pattern
|
|
11
|
+
|
|
12
|
+
`Observe<T>` from `@adobe/data/observe` — a subscription function: `(notify: (value: T) => void) => Unobserve`. Callback may be invoked
|
|
13
|
+
synchronously or asynchronously, zero or more times. Returns `Unobserve` to stop observing.
|
|
14
|
+
|
|
15
|
+
**Always use `Observe.*` helpers to create and manipulate observables. Never write raw subscriptions or manual notify wrappers** — the
|
|
16
|
+
standard helpers cover every composition need and keep the code consistent.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Creation helpers
|
|
21
|
+
|
|
22
|
+
| Helper | Usage |
|
|
23
|
+
| ------------------------------------ | -------------------------------------------------- |
|
|
24
|
+
| `Observe.fromConstant(value)` | `Observe<T>` from a constant value |
|
|
25
|
+
| `Observe.fromProperties({ a, b })` | Combine named observables into `Observe<{ a, b }>` |
|
|
26
|
+
| `Observe.fromArray([obs1, obs2])` | Combine array of observables into `Observe<T[]>` |
|
|
27
|
+
| `Observe.fromPromise(() => Promise)` | Lazy; notifies once when promise resolves |
|
|
28
|
+
| `Observe.createState(initial?)` | `[Observe<T>, (value: T) => void]` — mutable state |
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const constant = Observe.fromConstant(42);
|
|
32
|
+
const combined = Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b });
|
|
33
|
+
const [count, setCount] = Observe.createState(0);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Transformation helpers
|
|
39
|
+
|
|
40
|
+
| Helper | Usage |
|
|
41
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
42
|
+
| `Observe.withMap(obs, fn)` | Transform value; map from one type to another |
|
|
43
|
+
| `Observe.withFilter(obs, fn)` | Transform or filter; return `undefined` to skip |
|
|
44
|
+
| `Observe.withDefault(default, obs)` | Use default when value is undefined |
|
|
45
|
+
| `Observe.withLazy(() => obs)` | Defer expensive observable creation until first subscription |
|
|
46
|
+
| `Observe.withUnwrap(obs)` | Flatten `Observe<Observe<T>>` to `Observe<T>` |
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const doubled = Observe.withMap(count, n => n * 2);
|
|
50
|
+
const max = Observe.withFilter(Observe.fromProperties({ a, b }), ({ a, b }) => Math.max(a, b));
|
|
51
|
+
const withFallback = Observe.withDefault('unknown', maybeName);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Conversion helpers
|
|
57
|
+
|
|
58
|
+
| Helper | Usage |
|
|
59
|
+
| ------------------------ | ----------------------------------- |
|
|
60
|
+
| `Observe.toPromise(obs)` | Resolve with first value (one-shot) |
|
|
61
|
+
|
|
62
|
+
`Observe.toPromise` is for services reading a current value — **never use it in UI elements**.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ⛔ Antipattern: synchronous "peek"
|
|
67
|
+
|
|
68
|
+
**Never read an observable's current value by subscribing and immediately unsubscribing.** The `notify` callback may fire asynchronously
|
|
69
|
+
(auth state, network, derived chains), so the snapshot is stale or `undefined`.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// WRONG — only works if the observable happens to emit synchronously on subscribe
|
|
73
|
+
let value: T | undefined;
|
|
74
|
+
const unobserve = someObservable(v => {
|
|
75
|
+
value = v;
|
|
76
|
+
});
|
|
77
|
+
unobserve();
|
|
78
|
+
// use `value` ← undefined for any async-resolving observable
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// CORRECT — resolve the current value explicitly
|
|
83
|
+
const value = await Observe.toPromise(someObservable);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A stored `unobserve` is **only** ever for deferred teardown (returned from an Effect, called on unmount/cleanup) — never to bookend a
|
|
87
|
+
synchronous read. If a sync call site (e.g. a DOM event handler that must call `preventDefault()` before any `await`) needs the value, do
|
|
88
|
+
the synchronous work first, then `void asyncHelper(...)` that awaits `Observe.toPromise`. In a Lit render path, read reactive values through
|
|
89
|
+
`useObservableValues`, not `toPromise`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ⛔ Antipattern: `undefined` as an Observe data value
|
|
94
|
+
|
|
95
|
+
`@adobe/data/observe` reserves `undefined` to mean **"no value has been emitted yet"** — it gates the synchronous initial emit in
|
|
96
|
+
`Observe.createState`, `fromProperties`, and friends. A data value flowing through an Observe pipeline must use `null` for the absent case,
|
|
97
|
+
**never** `undefined`.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// WRONG — Observe<X | undefined>
|
|
101
|
+
const [projectId, setProjectId] = Observe.createState<ProjectId | undefined>(undefined);
|
|
102
|
+
return Observe.withDefault(undefined, cloudSync.projectName);
|
|
103
|
+
|
|
104
|
+
// CORRECT — Observe<X | null>
|
|
105
|
+
const [projectId, setProjectId] = Observe.createState<ProjectId | null>(null);
|
|
106
|
+
return Observe.withDefault<string | null>(null, cloudSync.projectName);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Why it bites:
|
|
110
|
+
|
|
111
|
+
- `createState<T | undefined>(undefined)` does **not** fire on subscribe (the implementation treats `undefined` as "not yet set"), so
|
|
112
|
+
downstream `useObservableValues` callers wait forever for a value that never arrives.
|
|
113
|
+
- `fromProperties` re-emits when an input flips `undefined → defined`, not when a value changes — using `undefined` as data corrupts the
|
|
114
|
+
dirty-tracking.
|
|
115
|
+
- `null` JSON-stringifies across the wire; `undefined` silently disappears.
|
|
116
|
+
|
|
117
|
+
When auditing new code, flag `Observe<… | undefined>`, `createState<… | undefined>`, `withDefault(undefined`, and `fromConstant(undefined)`
|
|
118
|
+
— each should use `null`. (A plain function returning `undefined`, e.g. an early `return` from an async helper, is unrelated and fine.)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## ⛔ Antipattern: membership-only subscription for value reads (ECS)
|
|
123
|
+
|
|
124
|
+
`db.observe.select(archetype.components)` fires **only when entities enter or leave the archetype** — i.e. on set-membership changes. It
|
|
125
|
+
does **not** fire when a component value changes on an entity already in the set. If the callback reads component **values**, this
|
|
126
|
+
subscription is almost always wrong: it emits once on load, then silently stops reacting when the user edits values (opacity slider,
|
|
127
|
+
selection toggle, drag end). The derived state goes stale with no error — the hardest bug to catch.
|
|
128
|
+
|
|
129
|
+
Pick the subscription by what the callback reads:
|
|
130
|
+
|
|
131
|
+
| Callback needs | Use |
|
|
132
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
133
|
+
| Entity **IDs** only ("how many items," "does this track have children") | `db.observe.select(archetype.components)` |
|
|
134
|
+
| Component **values** (any `db.get(id, 'field')` in a map/filter) | `Database.observeSelectDeep(db, [...fields] as const)` |
|
|
135
|
+
| A single UI element bound to one entity | `db.observe.entity(id)` / `observe.entity(id, archetype)` |
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// WRONG — reads values but only reacts to add/remove; value edits never re-run this
|
|
139
|
+
Observe.withMap(db.observe.select(archetype.components), ids => ids.filter(id => (db.get(id, 'selectedByUsers') ?? []).includes(userId)));
|
|
140
|
+
|
|
141
|
+
// CORRECT — value-tracking subscription; list exactly the fields read downstream
|
|
142
|
+
const SELECTED_FIELDS = ['selectedByUsers'] as const; // hoist the tuple above the factory
|
|
143
|
+
const rows$ = Database.observeSelectDeep(db, SELECTED_FIELDS);
|
|
144
|
+
Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(userId)).map(r => r.id));
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually
|
|
148
|
+
read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate
|
|
149
|
+
(`@adobe/data/observe`'s `withDeduplicate` is reference-equality only). `observeSelectDeep` from a UI element is banned — let each element
|
|
150
|
+
observe its own entity (see `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Common patterns
|
|
155
|
+
|
|
156
|
+
**Derived observable from service:**
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
export const isDevelopment = memoize((service: Pick<EnvironmentService, 'type'>) =>
|
|
160
|
+
Observe.withMap(service.type, type => type === 'development')
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Computed from multiple resources:**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
Observe.withFilter(Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b }), ({ a, b }) => Math.max(a, b));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Lazy creation:** `Observe.withLazy(() => expensiveObs)` — defers until first subscription.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Execute
|
|
175
|
+
|
|
176
|
+
When adding or using Observe:
|
|
177
|
+
|
|
178
|
+
1. Use `Observe.fromConstant` for static values.
|
|
179
|
+
2. Use `Observe.fromProperties` to combine multiple observables.
|
|
180
|
+
3. Use `Observe.withFilter` when mapping and/or filtering.
|
|
181
|
+
4. Use `Observe.createState` for mutable state with a setter.
|
|
182
|
+
5. Use `Observe.withUnwrap` to flatten `Observe<Observe<T>>`.
|
|
183
|
+
6. Call `unobserve()` when cleaning up (e.g. component unmount).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/*.tsx'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React component authoring
|
|
7
|
+
|
|
8
|
+
React components live in the `components/` layer. Consume Observe and void actions from plugins. Use `@adobe/data-react` for `useDatabase`,
|
|
9
|
+
`useObservableValues`, and `DatabaseProvider`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## useDatabase — single context
|
|
14
|
+
|
|
15
|
+
Binding components **must** call `useDatabase` to obtain the main service context. Do **not** access any other React context. Additional
|
|
16
|
+
contexts create a context waterfall and hurt performance. All other services and state are reachable from the database.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Binding component vs presentation
|
|
21
|
+
|
|
22
|
+
**Binding component** — the React component that:
|
|
23
|
+
|
|
24
|
+
- Injects observed values via `useObservableValues`
|
|
25
|
+
- Triggers re-render when those values change
|
|
26
|
+
- Binds action callbacks to the presentation
|
|
27
|
+
|
|
28
|
+
**Presentation** — a pure function (no hooks) that receives data and action callbacks as props and returns JSX. Keep reactive logic in the
|
|
29
|
+
binding component; presentation stays pure.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Props from parent
|
|
34
|
+
|
|
35
|
+
**Do not** pass values from parent except when needed to identify which entity in the database to bind to.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Parent: passes entity so child knows which record to bind to
|
|
39
|
+
{
|
|
40
|
+
values.sprites.map(entity => <Sprite key={entity} entity={entity} />);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Child: uses entity prop to observe the right record
|
|
44
|
+
function Sprite({ entity }: { entity: Entity }) {
|
|
45
|
+
const db = useDatabase();
|
|
46
|
+
const values = useObservableValues(() => ({ sprite: db.observe.entity(entity, db.archetypes.Sprite) }), [entity]);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## useObservableValues
|
|
53
|
+
|
|
54
|
+
_Most_ binding components use a **single** `useObservableValues` call. Collect all observed values in one object. Use `Observe.withDefault`
|
|
55
|
+
for values that may resolve slowly.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
function Counter() {
|
|
59
|
+
const db = useDatabase(counterPlugin);
|
|
60
|
+
const values = useObservableValues(() => ({
|
|
61
|
+
count: db.observe.resources.count,
|
|
62
|
+
}));
|
|
63
|
+
if (!values) return null;
|
|
64
|
+
return presentation.render({ ...values, increment: db.transactions.increment });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Presentation exports
|
|
71
|
+
|
|
72
|
+
Presentation files ONLY export `render` (and localization bundles where appropriate). Nothing else. If you need render args type externally,
|
|
73
|
+
use `Parameters<typeof render>[0]`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Action callbacks (not events)
|
|
78
|
+
|
|
79
|
+
Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples.
|
|
80
|
+
|
|
81
|
+
**Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
increment: db.transactions.increment; // reference — no wrapper needed
|
|
85
|
+
toggleSprite: () => db.transactions.toggleSpriteActive({ entity }); // args required — wrap
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Execute
|
|
91
|
+
|
|
92
|
+
When creating or modifying a React component:
|
|
93
|
+
|
|
94
|
+
1. Call `useDatabase` for the main service context. Do not use any other React context.
|
|
95
|
+
2. Split into binding component (reactive) and presentation (pure).
|
|
96
|
+
3. Use a single `useObservableValues` in the binding component.
|
|
97
|
+
4. Pass observed values and action callbacks to presentation. Pass function references directly when the signature matches; wrap only when
|
|
98
|
+
supplying arguments.
|
|
99
|
+
5. Keep presentation pure — no hooks.
|
|
100
|
+
6. Presentation exports only `render` (and localization bundles where appropriate).
|
|
101
|
+
7. Add `*-presentation.test.tsx` for presentation when appropriate; do not unit test binding components.
|
|
102
|
+
8. Never include business logic within binding components. Move into computed values or action handlers.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/services/**/*.ts'
|
|
4
|
+
- '**/*-service.ts'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Service authoring
|
|
8
|
+
|
|
9
|
+
Asynchronous data services. Live in the `services/` layer. Adhere to the namespace rule for type and function organization.
|
|
10
|
+
|
|
11
|
+
**Data** = readonly JSON values or Blobs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Front-end vs back-end services
|
|
16
|
+
|
|
17
|
+
**Front-end services** — called directly from UI components. Support unidirectional control flow.
|
|
18
|
+
|
|
19
|
+
| Allowed | Not allowed |
|
|
20
|
+
| ------------------------------- | -------------------------------- |
|
|
21
|
+
| `Observe<Data>` | Promise or AsyncGenerator return |
|
|
22
|
+
| Other services (sub-Services) | |
|
|
23
|
+
| Void-returning action functions | |
|
|
24
|
+
|
|
25
|
+
**Back-end services** — usually stateless. Functions typically return `Promise<Data>` or `AsyncGenerator<Data>`. Not called directly from
|
|
26
|
+
UI; front-end services call them.
|
|
27
|
+
|
|
28
|
+
This separation keeps UI components on a strict unidirectional path: data down via void actions, data up via Observe.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Front-end service constraints
|
|
33
|
+
|
|
34
|
+
`Service` interfaces for UI consumption may only contain:
|
|
35
|
+
|
|
36
|
+
- **`Observe<Data>`** — observable properties or factories
|
|
37
|
+
- **Sub-Service**s — nested service interfaces
|
|
38
|
+
- **Action functions** — zero or more `Data` arguments, return `void` only
|
|
39
|
+
- **Factory functions** — create observables or sub-Services
|
|
40
|
+
|
|
41
|
+
Compile-time check: `Assert<AsyncDataService.IsValid<ServiceInterface>>`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Folder structure and cohesion
|
|
46
|
+
|
|
47
|
+
**MUST NOT** place implementation files alongside the interface file. The `<name>-service/` folder MUST contain only the interface file and
|
|
48
|
+
`public.ts`.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
services/<name>-service/
|
|
52
|
+
<name>-service.ts ← interface + contract types; `export * as ServiceName from './public.js'` at bottom
|
|
53
|
+
public.ts ← re-exports factory (and other public exports) from the implementation sub-folder
|
|
54
|
+
<impl-name>/ ← REQUIRED sub-folder for all implementation files
|
|
55
|
+
create-<name>-service.ts ← factory function
|
|
56
|
+
*.ts ← helpers, utils, constants (implementation-private)
|
|
57
|
+
*.test.ts ← tests live here alongside implementation
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Single-file exception**: a bare factory with no helpers, no utilities, and no constants MAY live as a flat file alongside the interface.
|
|
61
|
+
The moment any additional implementation file is needed, the factory MUST move into a sub-folder.
|
|
62
|
+
|
|
63
|
+
**Interface file** (`<name>-service.ts`) — interface declaration and types that define the public service contract. MUST NOT import from any
|
|
64
|
+
implementation file or sub-folder. At the bottom of the file, exports the namespace: `export * as ServiceName from './public.js'`. This
|
|
65
|
+
merges the interface type and the factory namespace under one identifier — callers type against the interface and invoke factory functions
|
|
66
|
+
through the same name.
|
|
67
|
+
|
|
68
|
+
**Separate types file at the interface level** — only permitted if it contains exclusively public-contract types (types that appear in the
|
|
69
|
+
interface's method signatures or `Observe<>` generic arguments). Implementation-detail types — third-party API shapes, `window.*` global
|
|
70
|
+
augmentations, internal utilities — MUST live inside the implementation sub-folder.
|
|
71
|
+
|
|
72
|
+
**`*.test.ts` files MUST NOT appear at the interface folder level.** Every test file belongs inside the implementation sub-folder next to
|
|
73
|
+
the code it tests; there is nothing at the interface level to test.
|
|
74
|
+
|
|
75
|
+
**Implementation sub-folder** — all implementation logic including the factory, utilities, constants, helpers, and their tests. MUST NOT
|
|
76
|
+
depend on the interface file's types via the `public.js` re-export; import directly from `<name>-service.js`.
|
|
77
|
+
|
|
78
|
+
**public.ts** — MUST re-export the factory function and any other public exports from the implementation sub-folder. Does NOT re-export the
|
|
79
|
+
interface — the interface is exported directly from `<name>-service.ts`.
|
|
80
|
+
|
|
81
|
+
**No classes** — factory function or static plain object only. `this` bindings are brittle and block higher-order composition.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Factory export shape — `singletonFactory` + dual export
|
|
86
|
+
|
|
87
|
+
Every service factory MUST be wrapped with `singletonFactory` from `factory-functions.js`. This guarantees one instance per argument tuple
|
|
88
|
+
when the same factory is called multiple times, preventing duplicate subscriptions and torn state.
|
|
89
|
+
|
|
90
|
+
Export **both** the raw `factory` and the singleton-wrapped `create`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// <name>-service/<impl-name>/create-<name>-service.ts
|
|
94
|
+
import { singletonFactory } from 'factory-functions.js';
|
|
95
|
+
|
|
96
|
+
export const factory = (deps: Deps): NameService => {
|
|
97
|
+
// implementation
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// `factory` — for test injection (bypasses the singleton cache; each test gets a fresh instance)
|
|
101
|
+
// `create` — for production use (singleton per deps-tuple)
|
|
102
|
+
export const create = singletonFactory(factory);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`public.ts` re-exports both:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
export { create, factory } from './<impl-name>/create-<name>-service.js';
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Why:** Tests must create fresh instances without sharing a singleton cache. Production callers use `create`; tests that need isolation
|
|
112
|
+
import `factory` directly.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Service interface cleanliness
|
|
117
|
+
|
|
118
|
+
The `<name>-service.ts` interface MUST NOT reference internal implementation tools, third-party SDK names, or infrastructure system
|
|
119
|
+
identifiers in any method signature, property name, or type parameter.
|
|
120
|
+
|
|
121
|
+
**Wrong** — leaks an internal infrastructure name into the UI contract:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
interface SessionService extends Service {
|
|
125
|
+
readonly dunamis: DunamisSdk; // UI now knows Dunamis exists
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Correct** — surfaces only what UI needs, implementation-tool-agnostic:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
interface SessionService extends Service {
|
|
133
|
+
readonly sessionId: Observe<string>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The interface is the contract between UI and data layers. If the underlying tool is swapped (Dunamis → another SDK, cookies → localStorage),
|
|
138
|
+
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
|
|
139
|
+
know about.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Execute
|
|
144
|
+
|
|
145
|
+
When creating or modifying a service:
|
|
146
|
+
|
|
147
|
+
1. Place in `services/<name>-service/`.
|
|
148
|
+
2. Interface in `<name>-service.ts` — types only, extend `Service`, add `Assert<AsyncDataService.IsValid<>>`.
|
|
149
|
+
3. All implementation files go inside a named sub-folder (e.g. `create-<name>-service/`).
|
|
150
|
+
4. At the bottom of `<name>-service.ts`, add `export * as ServiceName from './public.js'`.
|
|
151
|
+
5. In `public.ts`, re-export the factory (and any other public exports) from the implementation sub-folder.
|
|
152
|
+
6. For front-end services: actions return void only; observables observe Data or Service only. For back-end services: functions return
|
|
153
|
+
`Promise<Data>` or `AsyncGenerator<Data>`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Verify
|
|
158
|
+
|
|
159
|
+
When any `src/**/services/**/*.ts` file is in the diff, confirm all three invariants:
|
|
160
|
+
|
|
161
|
+
1. **Interface/implementation separation** — every `.ts` file added or modified in `<name>-service/` is either `<name>-service.ts`,
|
|
162
|
+
`public.ts`, or inside a named sub-folder. Any file at the root of the folder that is neither is a violation.
|
|
163
|
+
2. **`public.ts` completeness** — `public.ts` re-exports the factory function (and any other public exports from the sub-folder).
|
|
164
|
+
3. **Interface file purity** — `<name>-service.ts` contains no imports from implementation sub-folders.
|
package/README.md
CHANGED
|
@@ -18,15 +18,22 @@ It installs two things:
|
|
|
18
18
|
|
|
19
19
|
## Install — Claude Code
|
|
20
20
|
|
|
21
|
-
Skills load as a marketplace plugin; rules install into the project.
|
|
21
|
+
Skills load as a marketplace plugin; rules install into the project. Run these
|
|
22
|
+
three from a shell (agent-runnable):
|
|
22
23
|
|
|
23
24
|
```
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
claude plugin marketplace add adobe/data --scope project
|
|
26
|
+
claude plugin install adobe-data-ai@adobe-data-skills --scope project
|
|
26
27
|
npx @adobe/data-ai@latest install
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
In an interactive Claude session you can instead use the slash-command form of
|
|
31
|
+
the first two — `/plugin marketplace add adobe/data` then
|
|
32
|
+
`/plugin install adobe-data-ai@adobe-data-skills` — but an agent driving a shell
|
|
33
|
+
should use the `claude plugin …` commands above (slash commands aren't
|
|
34
|
+
shell-runnable).
|
|
35
|
+
|
|
36
|
+
- Skills → the `adobe-data-ai` plugin. **Update:** `claude plugin update adobe-data-ai`.
|
|
30
37
|
- Rules → `.claude/rules/adobe-data-ai/`. **Update:** re-run `npx @adobe/data-ai@latest install`.
|
|
31
38
|
|
|
32
39
|
## Install — Cursor (and Codex / other `.agents` agents)
|
|
@@ -58,3 +65,16 @@ by name when you want a single phase or a specialized flow: `build-data`,
|
|
|
58
65
|
`build-ui`, `build-app-entry`; plus `build-game`, `meta-build` (phase-by-phase
|
|
59
66
|
in subagents), `review` (audit output against the rules), and `structure`
|
|
60
67
|
(reason about layout).
|
|
68
|
+
|
|
69
|
+
## One-liner: install + build an app
|
|
70
|
+
|
|
71
|
+
Paste one of these into a shell, replacing the quoted text with your own app
|
|
72
|
+
description. It installs the bundle, then runs `build-application` headlessly on
|
|
73
|
+
your prompt.
|
|
74
|
+
|
|
75
|
+
This presumes you are already running the agent within the directory you want to use as your project root.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Find the @adobe/data-ai package on npmjs and then install it in the current directory.
|
|
79
|
+
Next use it's /build-application skill to build <prompt here>
|
|
80
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.87",
|
|
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.9.
|
|
32
|
+
"@adobe/data": "0.9.87"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
36
|
"vitest": "^1.6.0",
|
|
37
|
-
"@adobe/data": "0.9.
|
|
37
|
+
"@adobe/data": "0.9.87"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-actions
|
|
3
|
-
description: Build a feature's ecs action-database — async orchestration over services and transactions. If the feature has async flows.
|
|
3
|
+
description: Build a feature's ecs action-database — async orchestration over services and transactions. If the feature has async flows or side effects outside of the main store.
|
|
4
4
|
input: feature
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Skip if this feature doesn't contain or require actions.
|
|
9
|
+
|
|
8
10
|
Create `ecs/action-database/`: `action-database.ts` (extends `ServiceDatabase`, adds `actions`
|
|
9
11
|
from `./actions/index.js`) plus an `actions/` folder — one async orchestrator per file. An action
|
|
10
12
|
takes `db: ServiceDatabase`, awaits `services/` ports, then commits **exactly one** transaction.
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Skip if this feature doesn't contain or require computed values.
|
|
9
|
+
|
|
8
10
|
Create `ecs/computed-database/`: `computed-database.ts` (extends the previous layer, adds
|
|
9
11
|
`computed` from `./computed/index.js`) plus a `computed/` folder — one `cached` derived value
|
|
10
12
|
per file + barrel. Derivation logic lives in `data/`; a computed only wires a db observable to it.
|
|
@@ -11,6 +11,10 @@ and other `data/` declarations):
|
|
|
11
11
|
- one namespace folder per data type — `<type>/{<type>.ts, schema.ts, public.ts, <helper>.ts}`;
|
|
12
12
|
- `data/state/` — the `State` aggregate plus its pure transforms/derivations.
|
|
13
13
|
|
|
14
|
+
**Always create `data/state/`** — even for a shell feature whose only field is a
|
|
15
|
+
boolean or enum. A feature with ECS resources/transactions but no `State` is
|
|
16
|
+
incomplete: the transaction must wrap a pure transform, not invent the logic. In rare circumstances, there may be no real state in a feature in which case the state type = {}
|
|
17
|
+
|
|
14
18
|
The schema is the single source of truth; the type derives from it (`Schema.ToType`).
|
|
15
19
|
Helpers are pure and unit-tested. Run this first — every other layer imports `data/`.
|
|
16
20
|
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-feature
|
|
3
|
-
description: Build a complete feature by
|
|
3
|
+
description: Build a complete feature step by step
|
|
4
4
|
input: feature
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Run with /x-execute. Each step is a no-op for a layer the feature doesn't use
|
|
20
|
-
(`build-systems` only for real-time features; `build-services` / `build-service-database` /
|
|
21
|
-
`build-actions` only when the feature has async flows).
|
|
8
|
+
/graph-execute
|
|
9
|
+
/build-data
|
|
10
|
+
/build-services
|
|
11
|
+
/build-core-database
|
|
12
|
+
/build-indexes
|
|
13
|
+
/build-transactions
|
|
14
|
+
/build-computed
|
|
15
|
+
/build-service-database
|
|
16
|
+
/build-actions
|
|
17
|
+
/build-systems
|
|
18
|
+
/build-ui
|
|
@@ -15,5 +15,7 @@ transactions = moves/spawns, computed = score/status/winner, ui = view + input.
|
|
|
15
15
|
(see data-lit-tictactoe).
|
|
16
16
|
- **Real-time** adds the `build-systems` phase: a per-frame tick loop (movement,
|
|
17
17
|
collision, lifetime) driven by the scheduler, over many entities in distinct
|
|
18
|
-
archetypes.
|
|
19
|
-
|
|
18
|
+
archetypes. Systems must stay O(1) per touched entity — never project the whole
|
|
19
|
+
`State` into/out of the store each frame (see `features/ecs/systems.md`).
|
|
20
|
+
This is where components, archetypes, indexes, and systems all carry real
|
|
21
|
+
weight.
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Skip if this feature doesn't contain or require indexes.
|
|
9
|
+
|
|
8
10
|
Create `ecs/index-database/`: `index-database.ts` (extends `CoreDatabase`, adds the `indexes`
|
|
9
11
|
facet from `./indexes/index.js`) plus an `indexes/` folder — one index per file + an `index.ts`
|
|
10
12
|
barrel.
|
|
@@ -5,6 +5,8 @@ input: feature
|
|
|
5
5
|
output: feature
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Skip if this feature doesn't contain or require services.
|
|
9
|
+
|
|
8
10
|
Create the feature's `services/` layer: one `<name>-service/` namespace folder per async
|
|
9
11
|
contract — an `interface` (validated with `AsyncDataService.IsValid`), async-only members,
|
|
10
12
|
and `create*` factories.
|