@adobe/data-ai 0.10.21 → 0.10.22
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 +26 -31
- package/.claude/rules/app.md +46 -0
- package/.claude/rules/archetypes.md +5 -3
- package/.claude/rules/data-modelling.md +8 -7
- package/.claude/rules/ecs.md +11 -7
- package/.claude/rules/element.md +6 -5
- package/.claude/rules/features/data/components.md +51 -0
- package/.claude/rules/features/data/entities.md +37 -0
- package/.claude/rules/features/data/index.md +18 -70
- package/.claude/rules/features/data/resources.md +34 -0
- package/.claude/rules/features/data/values.md +36 -0
- package/.claude/rules/features/ecs/actions.md +31 -0
- package/.claude/rules/features/ecs/computed.md +31 -0
- package/.claude/rules/features/ecs/conformance.md +85 -0
- package/.claude/rules/features/ecs/core.md +43 -0
- package/.claude/rules/features/ecs/index.md +72 -0
- package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
- package/.claude/rules/features/ecs/services.md +36 -0
- package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
- package/.claude/rules/features/ecs/transactions.md +32 -0
- package/.claude/rules/features/index.md +104 -150
- package/.claude/rules/features/services/index.md +52 -84
- package/.claude/rules/features/spec/index.md +161 -0
- package/.claude/rules/features/ui/binding-element.md +5 -5
- package/.claude/rules/features/ui/index.md +8 -3
- package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
- package/.claude/rules/features/ui/presentation-test.md +1 -0
- package/.claude/rules/features/ui/presentation.md +1 -0
- package/.claude/rules/global/function-references.md +2 -2
- package/.claude/rules/global/namespace.md +3 -3
- package/.claude/rules/global/react.md +4 -4
- package/.claude/rules/lit.md +2 -2
- package/.claude/rules/plugin-modelling.md +2 -0
- package/.claude/rules/service.md +2 -0
- package/.claude/rules/versioning.md +8 -6
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +7 -8
- package/skills/build-app-entry/SKILL.md +12 -8
- package/skills/build-computed/SKILL.md +8 -7
- package/skills/build-core-database/SKILL.md +18 -15
- package/skills/build-data/SKILL.md +14 -13
- package/skills/build-feature/SKILL.md +2 -1
- package/skills/build-game/SKILL.md +6 -4
- package/skills/build-indexes/SKILL.md +6 -7
- package/skills/build-service-database/SKILL.md +8 -8
- package/skills/build-services/SKILL.md +9 -7
- package/skills/build-spec/SKILL.md +23 -0
- package/skills/build-systems/SKILL.md +12 -18
- package/skills/build-transactions/SKILL.md +8 -8
- package/skills/build-ui/SKILL.md +5 -4
- package/skills/review/SKILL.md +3 -3
- package/.claude/rules/features/data/state.md +0 -209
- package/.claude/rules/features/services/main-service/actions.md +0 -70
- package/.claude/rules/features/services/main-service/archetypes.md +0 -49
- package/.claude/rules/features/services/main-service/components.md +0 -41
- package/.claude/rules/features/services/main-service/computed.md +0 -82
- package/.claude/rules/features/services/main-service/conformance.md +0 -95
- package/.claude/rules/features/services/main-service/core-database.md +0 -48
- package/.claude/rules/features/services/main-service/index.md +0 -187
- package/.claude/rules/features/services/main-service/resources.md +0 -28
- package/.claude/rules/features/services/main-service/services.md +0 -25
- package/.claude/rules/features/services/main-service/transactions.md +0 -59
|
@@ -1,161 +1,115 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/features
|
|
4
|
-
- '**/features
|
|
5
|
-
- '**/features/*/ui/**/*.ts'
|
|
3
|
+
- '**/features/*/**/*.ts'
|
|
4
|
+
- '**/features/*/**/*.tsx'
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
# Feature architecture —
|
|
9
|
-
|
|
10
|
-
Each feature is built
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`data
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
namespace exposes — as ordinary imports, not type-only. So the strict wall is
|
|
75
|
-
`ui/` above `data/` + `services/`; the `data/ ↔ services/` boundary is the loose
|
|
76
|
-
one. A feature creates only the layers it uses.
|
|
77
|
-
|
|
78
|
-
| Layer | Role |
|
|
79
|
-
|-------|------|
|
|
80
|
-
| `data/` | The spec: `State`, pure transforms & derivations, entity sub-types. Pure, tested. |
|
|
81
|
-
| `services/main-service/` | The implementation: the ECS materialisation + reactive reads/writes. The **sole entrypoint** — the only service the `ui/` binds to. |
|
|
82
|
-
| `services/<name>-service/` | Async capability contracts (ports to the outside world). Reached only *through* `main-service` (its actions/services wire them in), never by the UI. Optional. |
|
|
83
|
-
| `ui/` | Presentation. |
|
|
84
|
-
|
|
85
|
-
`services/` holds two kinds of service: the one `main-service/` (ECS-backed
|
|
86
|
-
state, its own subtree of rules) and any async capability contracts. The UI, and
|
|
87
|
-
every external consumer, sees only `main-service`.
|
|
7
|
+
# Feature architecture — layered, specified, implemented
|
|
8
|
+
|
|
9
|
+
Each feature is built up in layers, each simple and testable on its own:
|
|
10
|
+
|
|
11
|
+
- **`data/`** declares the feature's pure Data: value types, components, resources,
|
|
12
|
+
and entities (component tuples).
|
|
13
|
+
- **`services/`** declares and implements the async services the feature talks to.
|
|
14
|
+
- **`spec/`** is the feature's **specification**: one immutable `State` and pure
|
|
15
|
+
functions over it. It is test tier only.
|
|
16
|
+
- **`ecs/`** is the **implementation**: a reactive Entity-Component-System, proven
|
|
17
|
+
equivalent to the spec by conformance.
|
|
18
|
+
- **`ui/`** is presentation.
|
|
19
|
+
|
|
20
|
+
Write the slow, obviously-correct spec first; the ECS is then largely mechanical, and
|
|
21
|
+
conformance keeps it honest.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
features/<feature>/
|
|
28
|
+
data/ runtime values/ components/ resources/ entities/
|
|
29
|
+
services/ runtime <name>-service/ namespaces, services.ts
|
|
30
|
+
spec/ TEST TIER State, actions, derivations, cases, spec.ts
|
|
31
|
+
ecs/ runtime core/ indexes/ transactions/ services/ computed/ actions/ systems/
|
|
32
|
+
main-service.ts, conformance/ (TEST TIER)
|
|
33
|
+
ui/<x>/ runtime <x>-presentation.ts, <x>-element.ts
|
|
34
|
+
app/ runtime schema.ts, versioning/, main.ts
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A feature creates only the layers and folders it uses.
|
|
38
|
+
|
|
39
|
+
## Dependencies
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
runtime: data → services → ecs → ui → app
|
|
43
|
+
spec: data + services → spec (test tier)
|
|
44
|
+
tests: ecs/conformance → spec + ecs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Each layer imports only from the layers before it.** A higher feature's layer
|
|
48
|
+
may import the same or a lower layer of a feature it builds on.
|
|
49
|
+
- **No runtime file imports `spec/`.** The spec is consumed only by tests. Logic the
|
|
50
|
+
spec and the implementation share (ordering math, name formatting) lives in
|
|
51
|
+
`data/values/` helpers, which both import.
|
|
52
|
+
- **Presentations import only `data/` and sibling `ui/` lazy wrappers** (a
|
|
53
|
+
convention); elements bind to `ecs/main-service`.
|
|
54
|
+
- **A base feature never imports a feature built on it.** The one exception is a
|
|
55
|
+
presentation calling a higher feature's lazy wrapper (`lazy-element.md`), which
|
|
56
|
+
loads that feature's element and plugin on first render.
|
|
57
|
+
|
|
58
|
+
## Building a feature, one layer at a time
|
|
59
|
+
|
|
60
|
+
Each step has a gate that must pass before the next:
|
|
61
|
+
|
|
62
|
+
| # | Step | Gate |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| 1 | `data/values` | pins compile; helper tests |
|
|
65
|
+
| 2 | `data/components`, `resources`, `entities` | compiles |
|
|
66
|
+
| 3 | `services/` | per-service unit tests |
|
|
67
|
+
| 4 | `spec/`: State, `create`, `samples`, then one action or derivation + cases at a time | `checkSpec` |
|
|
68
|
+
| 5 | `ecs/core` + `ecs/conformance/projection.ts` | samples round-trip |
|
|
69
|
+
| 6 | `ecs` indexes, transactions, services, computed, actions — one op at a time | `checkFeature` |
|
|
70
|
+
| 7 | `ecs/systems` (real-time only) | per-system and frame conformance |
|
|
71
|
+
| 8 | `ui/` presentations, then elements | presentation tests |
|
|
72
|
+
| 9 | `app/` | versioning and persistence tests; the app builds |
|
|
88
73
|
|
|
89
74
|
## One app, many features
|
|
90
75
|
|
|
91
|
-
An
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
computed, services) is added to the shared live database and its code chunk is
|
|
120
|
-
fetched. Gate that first render behind a user action (a button, a tab) so the
|
|
121
|
-
load is genuinely on-demand.
|
|
122
|
-
- **The base reaches a child only through a lazy element wrapper** — a tiny
|
|
123
|
-
`Foo()` that `void import()`s the child element. That dynamic import is the one
|
|
124
|
-
allowed core→child seam; the heavy element and its main-service stay in the
|
|
125
|
-
child's own chunk.
|
|
126
|
-
|
|
127
|
-
## Spec and implementation, kept honest
|
|
128
|
-
|
|
129
|
-
The tie between `data/` (spec) and `main-service` (implementation) is
|
|
130
|
-
**conformance**, one property —
|
|
131
|
-
`toState(apply(fromState(before), args)) ≡ transform(before, args)`: each
|
|
132
|
-
main-service mutation, seeded and read back through a test-only store↔`State`
|
|
133
|
-
projection, equals the pure `data/` transform it stands for. The spec-owned cases
|
|
134
|
-
are authored as inert `data/state/*.cases.ts` and gathered by the `data/state/spec.ts`
|
|
135
|
-
**manifest**, which both the pure `spec.test.ts` (`Conformance.checkSpec(spec)`) and the
|
|
136
|
-
ecs `services/main-service/conformance/conformance.test.ts`
|
|
137
|
-
(`Conformance.checkFeature(spec)`) import. `checkFeature` pairs each ECS op to its
|
|
138
|
-
same-named transition, replays the shared cases, and round-trips the projection (see
|
|
139
|
-
`services/main-service/conformance.md`); the manifest's compile-time guards keep the two
|
|
140
|
-
calls the whole conformance surface. So conforming the implementation is "substitute the
|
|
141
|
-
implementation, reuse the expectations" — `main-service` is largely mechanical and
|
|
142
|
-
agent-generated, with the spec as oracle. *How* to author each layer lives in the
|
|
143
|
-
per-folder rules below.
|
|
76
|
+
An app is a set of features under `features/<name>/`. One base feature
|
|
77
|
+
(`features/main/`) hosts; the rest build on it and load lazily.
|
|
78
|
+
|
|
79
|
+
- **Keep each feature small; grow by adding features.**
|
|
80
|
+
- **One owner per component and resource name.** The lowest feature that needs a
|
|
81
|
+
name declares it in its `data/`; features built on it import that schema object
|
|
82
|
+
(identity is what the ECS compares) and spread the owner's barrel into their own.
|
|
83
|
+
- **`app/` composes the features** (`../app.md`): its schema combines each feature's
|
|
84
|
+
plugins, it owns versioning, and it injects services that must be provided at
|
|
85
|
+
runtime.
|
|
86
|
+
- **Features built on the base load lazily**: a feature's element extends the shared
|
|
87
|
+
live database with its plugin the first time it connects. Gate that first render
|
|
88
|
+
behind a user action.
|
|
89
|
+
|
|
90
|
+
## Enforcement
|
|
91
|
+
|
|
92
|
+
One TS project per layer (see `data-lit-todo`'s tsconfigs):
|
|
93
|
+
|
|
94
|
+
- **`tsconfig.data.json`** — `data/` + `services/`, composite.
|
|
95
|
+
- **`tsconfig.spec.json`** — `spec/`, references data.
|
|
96
|
+
- **`tsconfig.ecs.json`** — `ecs/` minus `conformance/`, references data.
|
|
97
|
+
- **`tsconfig.test.json`** — everything else, non-emitting: `ui/`, `app/`,
|
|
98
|
+
`ecs/conformance/`, and every `*.test.ts`. It references ecs + spec.
|
|
99
|
+
|
|
100
|
+
So `spec/` importing `ecs/`, or `ecs/` importing `spec/`, is a compile error (TS6307).
|
|
101
|
+
`ui/` and `app/` sit in the test project, so for them "never import `spec/`" is a
|
|
102
|
+
convention, not a compile error. The package's `typecheck` script is
|
|
103
|
+
`tsc -b tsconfig.test.json`, which builds all of them.
|
|
144
104
|
|
|
145
105
|
## Reference implementations
|
|
146
106
|
|
|
147
107
|
Working samples ship inside `@adobe/data` at
|
|
148
|
-
`node_modules/@adobe/data/references/<sample>/src
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
subtree (components, resources, archetypes, computed, indexes, transactions,
|
|
157
|
-
systems, conformance) and capability-contract services — and `ui/`.
|
|
158
|
-
Always-on conventions live in `global/` (`namespace.md`, `cohesion.md`,
|
|
159
|
-
`type-casts.md`, `function-references.md`, `react.md` — deliberately repo-wide);
|
|
160
|
-
other cross-cutting patterns at the rules root — `data-modelling.md`,
|
|
161
|
-
`archetypes.md` (row iteration).
|
|
108
|
+
`node_modules/@adobe/data/references/<sample>/src/`:
|
|
109
|
+
|
|
110
|
+
- **`data-lit-todo`** — the most complete: two features, services, indexes, cross-feature composition, and persistence.
|
|
111
|
+
- **`data-lit-tictactoe`** — the minimal turn-based reference.
|
|
112
|
+
- **`data-lit-space-rock-game`** and **`data-gpu-hopper`** — real-time, with per-system and frame conformance.
|
|
113
|
+
|
|
114
|
+
The per-folder rules live beside this file (`data/`, `services/`, `spec/`, `ecs/`,
|
|
115
|
+
`ui/`). Always-on conventions are in `global/`.
|
|
@@ -1,103 +1,71 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '**/features/*/services
|
|
4
|
-
- '**/features/*/services/*-service/**/*.ts'
|
|
3
|
+
- '**/features/*/services/**/*.ts'
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# services/ — the feature's services
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
A service is the boundary between the feature and the outside world (generation,
|
|
9
|
+
analytics, networking). Each is a namespace folder (`global/namespace.md`) holding its
|
|
10
|
+
interface, its implementation and a fake. Services depend only on `data/`.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
```
|
|
13
|
+
services/
|
|
14
|
+
analytics-service/
|
|
15
|
+
analytics-service.ts # the interface + `export * as AnalyticsService from "./public.js"`
|
|
16
|
+
create.ts # the implementation
|
|
17
|
+
create-fake.ts # the fake
|
|
18
|
+
public.ts # export { create }; export { createFake };
|
|
19
|
+
services.ts # type Services = { … }
|
|
20
|
+
```
|
|
20
21
|
|
|
21
|
-
The
|
|
22
|
-
its own subtree.
|
|
22
|
+
## The interface — an async data service
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
```ts
|
|
25
|
+
export interface AnalyticsService extends Service {
|
|
26
|
+
todoCreated: (args: { readonly name: string }) => void;
|
|
27
|
+
randomTodoRequested: () => Promise<Timing>;
|
|
28
|
+
}
|
|
29
|
+
type _Valid = Assert<AsyncDataService.IsValid<AnalyticsService>>;
|
|
30
|
+
export * as AnalyticsService from "./public.js";
|
|
31
|
+
```
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
service
|
|
33
|
+
- Members are async only: `void | Promise<T> | AsyncGenerator<T> | Observe<T>`. They
|
|
34
|
+
send and receive Data, or async callbacks and observers of Data, which keeps a
|
|
35
|
+
service portable across processes and lazily loadable.
|
|
36
|
+
- The interface is the only place `interface` is used. Validate it with
|
|
37
|
+
`AsyncDataService.IsValid`.
|
|
38
|
+
- A service that carries non-Data configuration the app injects (a host plugin, a
|
|
39
|
+
callback) cannot pass `IsValid`; skip the assert for it, and keep such services rare.
|
|
40
|
+
- A type only one service uses lives in its interface file (`Timing` beside
|
|
41
|
+
`AnalyticsService`).
|
|
42
|
+
|
|
43
|
+
## `create` and `createFake`
|
|
44
|
+
|
|
45
|
+
- **`create`** is the implementation. When it can't be built without runtime input
|
|
46
|
+
(a transport, a host-provided plugin), make `create` throw, telling the reader
|
|
47
|
+
which input is missing; the app injects the real one with
|
|
48
|
+
`Database.create(plugin, { services })`.
|
|
49
|
+
- **`createFake`** returns the service with every method present and inert. It is an
|
|
50
|
+
ordinary export (the bundler drops it when unused). Conformance calls it only to
|
|
51
|
+
list the service's methods; each case supplies the returns (`spec/index.md`).
|
|
52
|
+
- Access them statically (`AnalyticsService.createFake`) so tree-shaking works.
|
|
53
|
+
|
|
54
|
+
## `services.ts` — the injectable map
|
|
29
55
|
|
|
30
56
|
```ts
|
|
31
|
-
// services/services.ts
|
|
32
|
-
import type { AnalyticsService } from "./analytics-service/analytics-service.js";
|
|
33
|
-
import type { NameGeneratorService } from "./name-generator-service/name-generator-service.js";
|
|
34
57
|
export type Services = {
|
|
35
58
|
readonly analytics: AnalyticsService;
|
|
36
59
|
readonly nameGenerator: NameGeneratorService;
|
|
37
60
|
};
|
|
38
61
|
```
|
|
39
62
|
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`type _Pin = Assert<Equal<ServiceDatabase["services"], Services>>`.
|
|
47
|
-
- **Inherited services**, when a peer feature builds on another, intersect the
|
|
48
|
-
parent map: `export type Services = MainServices & { readonly baz: BazService }`.
|
|
49
|
-
- A feature with no capability services needs no `services.ts`.
|
|
50
|
-
|
|
51
|
-
## Capability contracts
|
|
52
|
-
|
|
53
|
-
Each is a namespace folder (`global/namespace.md`); the export and folder both carry the
|
|
54
|
-
`-service` suffix. A service is the boundary between pure feature code and the
|
|
55
|
-
outside world, which is why its members are async — enabling cross-process
|
|
56
|
-
portability and lazy loading (`AsyncDataService.createLazy`).
|
|
57
|
-
|
|
58
|
-
- The contract is an `interface` — the only place `interface` is used in the
|
|
59
|
-
codebase — validated immediately with
|
|
60
|
-
`Assert<AsyncDataService.IsValid<typeof MyService>>`.
|
|
61
|
-
- Members are async only: `void | Promise<T> | AsyncGenerator<T> | Observe<T>`.
|
|
62
|
-
- Provide `create*` factories.
|
|
63
|
-
|
|
64
|
-
## Test doubles are a test-tier `*.fake.ts` — never in the production barrel
|
|
65
|
-
|
|
66
|
-
A service is the seam consumers swap out under test. Ship a **shape-only recording
|
|
67
|
-
template** alongside the interface, in the same namespace folder: `<name>.fake.ts`, a
|
|
68
|
-
**single export** `createFake` that returns the service with each method present but
|
|
69
|
-
inert (void methods do nothing; value methods return a placeholder). It is **NOT**
|
|
70
|
-
re-exported through `public.ts` — so `MyService.createFake` is never reachable from
|
|
71
|
-
runtime code — and it imports the contract with `import type` only. The conformance
|
|
72
|
-
manifest (`data/state.md`) imports it directly; nothing else does.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
// analytics-service/analytics.fake.ts — test tier, not on the namespace
|
|
76
|
-
import type { AnalyticsService } from "./analytics-service.js";
|
|
77
|
-
export const createFake = (): AnalyticsService => ({
|
|
78
|
-
serviceName: "analytics",
|
|
79
|
-
todoToggled: () => {}, // void method: inert
|
|
80
|
-
randomTodoRequested: () => Promise.resolve({ startedAt: 0 }), // value method: placeholder
|
|
81
|
-
// …one entry per method
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The template supplies only the service's **shape** (the runner calls it once to
|
|
86
|
-
enumerate methods — no Proxy). It invents **no return values**: a conformance case
|
|
87
|
-
schedules each value-returning method's returns in its `responses` and asserts the
|
|
88
|
-
calls in its `effects` (`data/state.md`), so the case owns both the input and the
|
|
89
|
-
expectation. A `*.fake.ts` therefore takes no response parameter and hardcodes nothing
|
|
90
|
-
a case asserts.
|
|
91
|
-
|
|
92
|
-
## Where the I/O types live
|
|
93
|
-
|
|
94
|
-
Most input/output types belong to a single service — declare them **on that
|
|
95
|
-
service's namespace** (`MyService.SomeInput`) and expose them only when something
|
|
96
|
-
external actually references them; not everything does.
|
|
97
|
-
|
|
98
|
-
A **service type** may be non-serializable (callbacks, function signatures) —
|
|
99
|
-
that's what distinguishes it from a `data/` type. If an I/O value is a plain
|
|
100
|
-
serializable value, prefer a `data/` type instead.
|
|
63
|
+
- Spec actions inject with `Pick<Services, …>`.
|
|
64
|
+
- `ecs/services/` registers the same keys, pinned with
|
|
65
|
+
`Assert<Equal<ServiceDatabase["services"], Services>>`.
|
|
66
|
+
- A feature built on another intersects the lower map:
|
|
67
|
+
`type Services = MainServices & { readonly baz: BazService }`.
|
|
68
|
+
- A feature with no services has no `services/` folder.
|
|
101
69
|
|
|
102
|
-
A
|
|
103
|
-
|
|
70
|
+
A service that reads the database or calls its transactions is not a `services/`
|
|
71
|
+
service. It is a database-bound factory in `ecs/services/`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/spec/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# spec/ — the feature's specification
|
|
7
|
+
|
|
8
|
+
The whole feature as **one immutable `State`** plus pure functions over it: an
|
|
9
|
+
**action** spec for every operation the app performs, and a **derivation** for every
|
|
10
|
+
derived value it shows. It is slow, obvious and fully tested, and it is the oracle the
|
|
11
|
+
ECS is conformed against (`../ecs/conformance.md`).
|
|
12
|
+
|
|
13
|
+
`spec/` is **test tier**: no runtime file imports it. It depends on `data/`,
|
|
14
|
+
`services/` (types and fakes) and `@adobe/data-testing`.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
spec/
|
|
18
|
+
state.ts create.ts samples.ts public.ts # State namespace: create + samples
|
|
19
|
+
toggle-complete.ts toggle-complete.cases.ts # one action or derivation + its cases
|
|
20
|
+
visible-todos.ts visible-todos.cases.ts
|
|
21
|
+
transforms.ts # barrel of every action + derivation
|
|
22
|
+
systems.ts <system>.cases.ts # real-time only: one function per ECS system
|
|
23
|
+
spec.ts spec.test.ts # the manifest + Conformance.checkSpec
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## `State`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export type State = {
|
|
30
|
+
readonly displayCompleted: boolean; // singleton → resource `displayCompleted`
|
|
31
|
+
readonly selectedTodo: Entity; // a reference singleton
|
|
32
|
+
readonly entities: ReadonlyMap<number, Todo>; // an entity collection, keyed by id
|
|
33
|
+
};
|
|
34
|
+
type _Pin = Assert<Conformance.StateMatches<State, typeof resources, typeof components>>;
|
|
35
|
+
export * as State from "./public.js";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Every non-`Map` field is a singleton, named for its resource.** A single game
|
|
39
|
+
object (a ship, a frog) is a resource too.
|
|
40
|
+
- **Every `ReadonlyMap` field is an entity collection**, keyed by numeric id. Its
|
|
41
|
+
value is an `data/entities` type with no `id` (identity is the key), and its field
|
|
42
|
+
names are component names. All collections share one id space, so cases use
|
|
43
|
+
distinct ids across maps.
|
|
44
|
+
- **Pin it** with `Conformance.StateMatches`. Conformance finds schemas by these names,
|
|
45
|
+
so the pin is what makes a mismatch a compile error. A feature with no components
|
|
46
|
+
passes `{}`.
|
|
47
|
+
- A value computed from other state (a board picture folded from placed marks) is a
|
|
48
|
+
**derivation**, not a `State` field.
|
|
49
|
+
- `create()` is the default each case deltas over; `samples` are full states the
|
|
50
|
+
projection round-trips.
|
|
51
|
+
|
|
52
|
+
## Actions and derivations — `<name>.ts`
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export const toggleComplete = (
|
|
56
|
+
state: Pick<State, "entities">,
|
|
57
|
+
{ id, analytics }: { readonly id: number } & Pick<Services, "analytics">,
|
|
58
|
+
): Pick<State, "entities"> => {
|
|
59
|
+
analytics.todoToggled();
|
|
60
|
+
const target = state.entities.get(id);
|
|
61
|
+
if (target === undefined) return { entities: state.entities };
|
|
62
|
+
return { entities: new Map(state.entities).set(id, { ...target, complete: !target.complete }) };
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **An action** is `(state: Pick<State, …reads>, args) => Pick<State, …writes>`, sync
|
|
67
|
+
or async. It specs one `MainService` action of the same name. Return only the
|
|
68
|
+
fields it writes; no `...state` spread.
|
|
69
|
+
- **Inject services with `Pick<Services, …>`** alongside the data args, and call their
|
|
70
|
+
methods. A spec never needs a service's helpers.
|
|
71
|
+
- **Guard a no-op by returning an unchanged patch**, never by throwing. Reserve
|
|
72
|
+
`throw` for a precondition a case names with `throws`.
|
|
73
|
+
- **A derivation** is `(state) => value`. It specs one computed of the same name.
|
|
74
|
+
- **Per-frame work** in a real-time feature is a `systems.ts` barrel: one pure
|
|
75
|
+
function per ECS system, named the same, with the ordinary transition signature
|
|
76
|
+
(below). There is no hand-written `step`: the frame order comes from the systems'
|
|
77
|
+
`schedule`.
|
|
78
|
+
- Share logic with the ECS through `data/values/` helpers, never by the ECS importing
|
|
79
|
+
the spec.
|
|
80
|
+
|
|
81
|
+
## Cases — `<name>.cases.ts`
|
|
82
|
+
|
|
83
|
+
Inert data, annotated `Conformance.SpecCases<State, typeof fn>` for an action or
|
|
84
|
+
`Conformance.SpecDerivations<typeof fn>` for a derivation (the annotation is what
|
|
85
|
+
tells them apart).
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
export const cases: Conformance.SpecCases<State, typeof toggleComplete> = {
|
|
89
|
+
args: { type: "object", properties: { id: Entity.schema } }, // marks entity-reference args
|
|
90
|
+
cases: [{
|
|
91
|
+
name: "marks an incomplete todo complete",
|
|
92
|
+
before: { entities: new Map([[1, { name: "a", complete: false, order: 0 }]]) },
|
|
93
|
+
args: { id: 1 },
|
|
94
|
+
after: { entities: new Map([[1, { name: "a", complete: true, order: 0 }]]) },
|
|
95
|
+
effects: { analytics: [["todoToggled"]] },
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **`before`** is a delta over `State.create()`; **`after`** is the writes patch; a
|
|
101
|
+
derivation case is `{ name, input, value }`.
|
|
102
|
+
- **`args` is data only.** Services never appear in cases. A value-returning service
|
|
103
|
+
method's results are scheduled in **`responses`** — calling one with nothing
|
|
104
|
+
scheduled, or more times than scheduled, fails the case — and calls to assert go in
|
|
105
|
+
**`effects`** (`Array` = ordered, `Set` = any order).
|
|
106
|
+
- **Ids are plain numbers** everywhere; conformance compares them up to an id
|
|
107
|
+
bijection. Mark entity-reference args in the `args` schema.
|
|
108
|
+
- **`throws: true | "substring"`** replaces `after` for a precondition failure.
|
|
109
|
+
- A spec-only fixture helper (`marksOf("XX O ")`) may build case values.
|
|
110
|
+
|
|
111
|
+
## `transforms.ts`, `spec.ts`, `spec.test.ts`
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// transforms.ts — exactly the conformed actions and derivations
|
|
115
|
+
export { toggleComplete } from "./toggle-complete.js";
|
|
116
|
+
export { visibleTodos } from "./visible-todos.js";
|
|
117
|
+
|
|
118
|
+
// spec.ts
|
|
119
|
+
export const spec = Conformance.spec({
|
|
120
|
+
state: State,
|
|
121
|
+
fns: transforms,
|
|
122
|
+
schemas: { ...components, ...resources }, // when State holds entity references
|
|
123
|
+
services: { analytics: AnalyticsService.createFake }, // when an action injects one
|
|
124
|
+
cases: { toggleComplete, visibleTodos },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// spec.test.ts
|
|
128
|
+
Conformance.checkSpec(spec);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`Conformance.spec` checks at compile time that every fn has exactly one case module,
|
|
132
|
+
and that `services` lists exactly the services the actions and systems inject.
|
|
133
|
+
|
|
134
|
+
## Systems (real-time features)
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// systems.ts — one pure function per ECS system, each guarding itself
|
|
138
|
+
export const movement = whilePlaying((s, { dt }) => ({ asteroids: stepAsteroids(s, { dt }) }));
|
|
139
|
+
export const collision = whilePlaying((s) => ({ ...resolveHits(s) }));
|
|
140
|
+
|
|
141
|
+
// spec.ts
|
|
142
|
+
export const spec = Conformance.spec({ …,
|
|
143
|
+
systems: {
|
|
144
|
+
fns: systems,
|
|
145
|
+
cases: { movement, collision },
|
|
146
|
+
noOp: [{ name: "frozen once the game is over", before: { status: "over" }, args: { dt: 1 } }],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **Mirror the ECS systems one to one.** When a system fuses two logical steps, its
|
|
152
|
+
function composes both; when two systems split one step, split the function too
|
|
153
|
+
(the handoff must be a `State` field).
|
|
154
|
+
- **One `SpecCases` module per system**; `checkSpec` runs them against the functions.
|
|
155
|
+
- **`noOp`** lists states every system must leave unchanged (game over, paused). Each
|
|
156
|
+
is checked against every system, so a shared guard needs one case, not one per
|
|
157
|
+
system.
|
|
158
|
+
- **Whole-frame cases** depend on the ECS's schedule, so they live with the
|
|
159
|
+
implementation (`../ecs/conformance.md`), not here.
|
|
160
|
+
- A system the spec can't model (wasm physics, init-only seeding) has no function;
|
|
161
|
+
the implementation declares it unmodelled (`../ecs/conformance.md`).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- 'src/**/*element*.ts'
|
|
4
|
-
- 'src/**/elements/**/*.ts'
|
|
5
|
-
- 'src/**/ui/**/*-element.ts'
|
|
3
|
+
- '**/src/**/*element*.ts'
|
|
4
|
+
- '**/src/**/elements/**/*.ts'
|
|
5
|
+
- '**/src/**/ui/**/*-element.ts'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Binding elements
|
|
@@ -101,7 +101,7 @@ render() {
|
|
|
101
101
|
// UI action call with simple ternary — good
|
|
102
102
|
jump: (heroMode: boolean) => this.service.actions.move(heroMode ? 100 : 10),
|
|
103
103
|
// transaction call - good
|
|
104
|
-
play: this.service.
|
|
104
|
+
play: this.service.actions.playTrack
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
```
|
|
@@ -218,7 +218,7 @@ export class BadControlElement extends FooElement {
|
|
|
218
218
|
// violation: logic in callback + onX name
|
|
219
219
|
const normalized = Math.round(v * 100) / 100;
|
|
220
220
|
if (!Number.isFinite(normalized)) return;
|
|
221
|
-
this.service.
|
|
221
|
+
this.service.actions.updateThing({ value: normalized });
|
|
222
222
|
};
|
|
223
223
|
return presentation.render({ total: clamped, onSave });
|
|
224
224
|
}
|