@adobe/data-ai 0.10.21 → 0.10.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude/rules/README.md +26 -31
  2. package/.claude/rules/app.md +46 -0
  3. package/.claude/rules/archetypes.md +5 -3
  4. package/.claude/rules/data-modelling.md +8 -7
  5. package/.claude/rules/ecs.md +11 -7
  6. package/.claude/rules/element.md +6 -5
  7. package/.claude/rules/features/data/components.md +51 -0
  8. package/.claude/rules/features/data/entities.md +37 -0
  9. package/.claude/rules/features/data/index.md +18 -70
  10. package/.claude/rules/features/data/resources.md +34 -0
  11. package/.claude/rules/features/data/values.md +36 -0
  12. package/.claude/rules/features/ecs/actions.md +31 -0
  13. package/.claude/rules/features/ecs/computed.md +31 -0
  14. package/.claude/rules/features/ecs/conformance.md +85 -0
  15. package/.claude/rules/features/ecs/core.md +43 -0
  16. package/.claude/rules/features/ecs/index.md +72 -0
  17. package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
  18. package/.claude/rules/features/ecs/services.md +36 -0
  19. package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
  20. package/.claude/rules/features/ecs/transactions.md +32 -0
  21. package/.claude/rules/features/index.md +104 -150
  22. package/.claude/rules/features/services/index.md +52 -84
  23. package/.claude/rules/features/spec/index.md +161 -0
  24. package/.claude/rules/features/ui/binding-element.md +5 -5
  25. package/.claude/rules/features/ui/index.md +8 -3
  26. package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
  27. package/.claude/rules/features/ui/presentation-test.md +1 -0
  28. package/.claude/rules/features/ui/presentation.md +1 -0
  29. package/.claude/rules/global/function-references.md +2 -2
  30. package/.claude/rules/global/namespace.md +3 -3
  31. package/.claude/rules/global/react.md +4 -4
  32. package/.claude/rules/lit.md +2 -2
  33. package/.claude/rules/plugin-modelling.md +2 -0
  34. package/.claude/rules/service.md +2 -0
  35. package/.claude/rules/versioning.md +8 -6
  36. package/.claude-plugin/plugin.json +1 -1
  37. package/README.md +8 -0
  38. package/bin/cli.mjs +24 -8
  39. package/package.json +3 -3
  40. package/skills/build-actions/SKILL.md +7 -8
  41. package/skills/build-app-entry/SKILL.md +12 -8
  42. package/skills/build-computed/SKILL.md +8 -7
  43. package/skills/build-core-database/SKILL.md +18 -15
  44. package/skills/build-data/SKILL.md +14 -13
  45. package/skills/build-feature/SKILL.md +2 -1
  46. package/skills/build-game/SKILL.md +6 -4
  47. package/skills/build-indexes/SKILL.md +6 -7
  48. package/skills/build-service-database/SKILL.md +8 -8
  49. package/skills/build-services/SKILL.md +9 -7
  50. package/skills/build-spec/SKILL.md +23 -0
  51. package/skills/build-systems/SKILL.md +12 -18
  52. package/skills/build-transactions/SKILL.md +8 -8
  53. package/skills/build-ui/SKILL.md +5 -4
  54. package/skills/review/SKILL.md +3 -3
  55. package/.claude/rules/features/data/state.md +0 -209
  56. package/.claude/rules/features/services/main-service/actions.md +0 -70
  57. package/.claude/rules/features/services/main-service/archetypes.md +0 -49
  58. package/.claude/rules/features/services/main-service/components.md +0 -41
  59. package/.claude/rules/features/services/main-service/computed.md +0 -82
  60. package/.claude/rules/features/services/main-service/conformance.md +0 -95
  61. package/.claude/rules/features/services/main-service/core-database.md +0 -48
  62. package/.claude/rules/features/services/main-service/index.md +0 -187
  63. package/.claude/rules/features/services/main-service/resources.md +0 -28
  64. package/.claude/rules/features/services/main-service/services.md +0 -25
  65. package/.claude/rules/features/services/main-service/transactions.md +0 -59
@@ -1,161 +1,115 @@
1
1
  ---
2
2
  paths:
3
- - '**/features/*/data/**/*.ts'
4
- - '**/features/*/services/**/*.ts'
5
- - '**/features/*/ui/**/*.ts'
3
+ - '**/features/*/**/*.ts'
4
+ - '**/features/*/**/*.tsx'
6
5
  ---
7
6
 
8
- # Feature architecture — a verifiable spec and an optimized implementation
9
-
10
- Each feature is built twice in one codebase: a **pure specification** you can
11
- trust, and an **efficient implementation** proven equivalent to it.
12
-
13
- - **`data/` is the specification.** The whole feature modelled as one immutable
14
- `State` with pure transformations over it — correct by construction, fully
15
- unit-tested, performance irrelevant. This is the source of truth.
16
- - **`services/main-service/` is the implementation.** The same model physically
17
- arranged for mutation efficiency — a reactive Entity-Component-System, an
18
- implementation detail behind the service. Its reads and writes wrap the
19
- `data/` functions where practical; where they must be hand-optimized, unit
20
- tests verify they still agree with the `data/` precedent.
21
-
22
- Because the optimized `main-service` is largely mechanical given the `data/`
23
- spec, it can be generated and kept honest by AI rules, with the conformance
24
- tests as the safety net. Net result: write a slow-but-verifiable app, then
25
- derive a fast one that provably behaves the same.
26
-
27
- ## Two modes: state-based vs. ECS-based
28
-
29
- The "built twice" structure above is the **state-based** mode. Every feature is in
30
- one of two modes, discriminated by **the presence of `data/state/`**:
31
-
32
- - **State-based** — a **Functional State Specification (FSS)** is the source of
33
- truth: the pure `data/State` aggregate with its transitions and derivations, and
34
- the ECS is a conformance-verified implementation of it. Adds `data/state/` (State,
35
- the pure transforms with sibling `*.cases.ts`, the `transforms.ts` barrel, the `spec.ts`
36
- manifest, `spec.test.ts`), `services/main-service/conformance/`, and the `state`
37
- projection computed. **This is how every new feature is authored.**
38
- - **ECS-based** — the **ECS is the source of truth**, authored directly:
39
- **no `data/state/`**, no FSS, no conformance. This is a **legacy** shape — features
40
- written before the state-based approach existed. New features are **never** authored
41
- this way; ECS-based exists only to describe and maintain those older features.
42
-
43
- **Discriminator:** `data/state/` present → state-based; absent → ECS-based. The
44
- `data/<type>` value folders (the serializable types backing components/resources,
45
- the wire, and persistence) exist in **both** modes — only the aggregate `State`
46
- spec and the conformance layer are mode-specific. `data/` never disappears; the
47
- *aggregate spec* does.
48
-
49
- **Testing follows the mode.** A state-based feature is verified by conformance: the
50
- shared `cases` drive both the pure spec and the ECS, so a transaction or action with
51
- a same-named transition needs no test of its own. An **ECS-based feature has no
52
- conformance cases, so every transaction and every action carries its own unit
53
- test** — the direct substitute for the conformance oracle (see `services/main-service/transactions.md`
54
- and `actions.md`).
55
-
56
- ## The layers
57
-
58
- **The layers organize by the *kind of type* each folder holds, not by a strict
59
- dependency wall.** `data/` holds **value types** (pure, serializable data) and
60
- the pure declarations over them; `services/` holds **service types** (interfaces
61
- **and** implementations); `ui/` holds presentation. The rule is about *what
62
- lives where*: a service is never authored under `data/`, a value type is never
63
- authored under `services/`.
64
-
65
- **`ui/` isolation is strict and inviolable.** `ui/` sits at the top: neither
66
- `data/` nor `services/` may ever import from `ui/`, no exceptions. Presentation
67
- depends on the model; the model never depends on presentation.
68
-
69
- Beneath that hard line, `data/` and `services/` are **not** strictly ordered.
70
- Value types are fundamental — a `data/` **type** depends on nothing but
71
- `@adobe/data` and other `data/` types. But the **transition functions** over
72
- them may depend on `services/` **without restriction**: they import whatever they
73
- need — service interfaces they inject, and any associated utilities the service
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 application is a set of **features**, each its own `features/<name>/` folder
92
- with the same layers. One base feature (`features/main/`) is the host; the rest
93
- are peers that load lazily.
94
-
95
- **Keep each feature small; grow by adding features, not by bloating one.** A
96
- feature is meant to fit in the head — a handful of files per layer. When a
97
- part of a feature keeps growing, that is usually the signal to split it into
98
- its own peer feature rather than let one feature's folders balloon.
99
-
100
- - **Dependencies point toward the base, never out of it.** A peer feature may
101
- build on another feature's `data/` types and declarations (kept acyclic). The
102
- base must not depend on its children — with one sanctioned exception below.
103
- - **The base `imports` every peer's *schema* plugin** —
104
- `Database.Plugin.create({ imports })`, not `extends`. `imports` merges the
105
- peer's `services/main-service/core-database` (components / resources /
106
- archetypes) into the shared store at runtime **without** pulling its types or
107
- behavior into the base's type or bundle (`extends` would do both, and cost
108
- quadratically). So one store knows every feature's schema — data coexists,
109
- persists, and syncs — while the base stays decoupled. Import the peer's
110
- `services/main-service/core-database.ts` plugin (schema only) — under its
111
- feature-qualified name `<Peer>CoreDatabase` (see the cross-feature naming rule
112
- in `services/main-service/index.md`): its indexes, transactions, computed,
113
- services, and UI stay out until the feature loads. A column two features share
114
- (e.g. `name`) lives in `data/` and is referenced by identity, so
115
- `combinePlugins` dedupes it.
116
- - **Peers load lazily by being used.** `DatabaseElement`, on connect, walks up to
117
- the nearest ancestor database and `extend`s it with its own plugin — so the
118
- first time a feature element renders, its full plugin (indexes, transactions,
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/` — read them for concrete,
149
- current examples of this structure. `data-lit-todo` is the most complete
150
- (multi-feature, capability services, indexes, conformance tests);
151
- `data-lit-tictactoe` is the minimal turn-based reference.
152
-
153
- ## Per-layer detail
154
-
155
- See the rules under each folder: `data/`, `services/` — the `main-service/`
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/services.ts'
4
- - '**/features/*/services/*-service/**/*.ts'
3
+ - '**/features/*/services/**/*.ts'
5
4
  ---
6
5
 
7
6
  # services/ — the feature's services
8
7
 
9
- Everything a feature exposes as a service lives here. Two kinds:
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
- - **`main-service/`** — the one entrypoint. The ECS-backed reactive state
12
- service (materialisation + reads/writes over it); the only service the `ui/`
13
- and external consumers bind to. It has its own subtree of rules
14
- (`main-service/index.md` and the layer files under it). Every feature with
15
- state has exactly one.
16
- - **`<name>-service/`** — async **capability contracts**: ports to the outside
17
- world (processing, persistence, observation, generation). Reached only
18
- *through* `main-service` (its service/action layers wire them in), never by the
19
- UI. A feature has zero or more.
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 rest of this rule governs the capability contracts; `main-service` follows
22
- its own subtree.
22
+ ## The interface — an async data service
23
23
 
24
- ## `services/services.ts` — the injectable service map
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
- The folder root exports one `Services` type: the feature's capability services
27
- keyed by short name (the `-service` suffix dropped), the single source of truth for
28
- service injection.
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
- - **Transitions inject with `Pick<Services, …>`** (`data/state.md`), never a
41
- re-declared inline `{ analytics: AnalyticsService }` — so the key/type live in one
42
- place.
43
- - **The ecs `service-database` is pinned to it.** After its `ServiceDatabase` type,
44
- a drift-guard asserts the resolved services match the map, so `db.services` (what
45
- actions call) and `Services` (what transitions inject) can't diverge:
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 non-service type sits *directly* in `services/` **only** when it is genuinely
103
- shared across more than one service — rare, but it happens.
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.transactions.playTrack
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.transactions.updateThing({ value: normalized });
221
+ this.service.actions.updateThing({ value: normalized });
222
222
  };
223
223
  return presentation.render({ total: clamped, onSave });
224
224
  }