@adobe/data-ai 0.9.91 → 0.9.93
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/data-modelling.md +4 -3
- package/.claude/rules/ecs.md +1 -1
- package/.claude/rules/element.md +1 -1
- package/.claude/rules/features/data/index.md +59 -14
- package/.claude/rules/features/data/state.md +204 -44
- package/.claude/rules/features/index.md +29 -10
- package/.claude/rules/features/services/index.md +19 -0
- package/.claude/rules/features/services/main-service/actions.md +39 -5
- package/.claude/rules/features/services/main-service/computed.md +54 -3
- package/.claude/rules/features/services/main-service/conformance.md +209 -62
- package/.claude/rules/features/services/main-service/index.md +14 -10
- package/.claude/rules/features/services/main-service/services.md +8 -7
- package/.claude/rules/features/services/main-service/transactions.md +15 -6
- package/.claude/rules/features/ui/binding-element.md +4 -4
- package/.claude/rules/global/namespace.md +38 -10
- package/.claude/rules/global/react.md +1 -1
- package/.claude/rules/lit.md +3 -3
- package/.claude/rules/observe.md +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +82 -32
- package/bin/cli.mjs +84 -2
- package/package.json +3 -3
- package/skills/build-app-entry/SKILL.md +1 -1
- package/skills/build-data/SKILL.md +2 -2
- package/skills/build-systems/SKILL.md +1 -1
|
@@ -3,69 +3,216 @@ paths:
|
|
|
3
3
|
- '**/features/*/services/main-service/**/conformance/**/*.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# services/main-service/conformance/ —
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`toState
|
|
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
|
-
|
|
6
|
+
# services/main-service/conformance/ — keeping the ECS honest against the spec
|
|
7
|
+
|
|
8
|
+
Test-only (imported only by `*.test.ts`, in no facet barrel). The `data/state`
|
|
9
|
+
cases are the shared truth; the shared **`@adobe/data/testing`** runner replays
|
|
10
|
+
them against the ECS. This folder holds only the *feature-specific projection*
|
|
11
|
+
(`projection.ts`) plus one `conformance.test.ts` that makes a **single**
|
|
12
|
+
`Conformance.runFeature({...})` call. Reference: `data-lit-tictactoe`'s
|
|
13
|
+
`conformance/` (the zero-config example) and `data-lit-todo` (entity markers +
|
|
14
|
+
`hydrate`); `data-lit-space-rock-game` for the entity-bag / real-time variant.
|
|
15
|
+
|
|
16
|
+
**The projection helpers — `fromState`, `toState`, and the
|
|
17
|
+
`toData(store, entity)` reader — are strictly for conformance tests and MUST
|
|
18
|
+
NEVER run in production code, ever.** `fromState`/`toState` rewrite the whole
|
|
19
|
+
store out-of-band; runtime code reads through observables/indexes and writes
|
|
20
|
+
through transactions. (This conformance `toData(store, entity)` reader is
|
|
21
|
+
unrelated to the library's `db.toData()` store-serialization method, which *is*
|
|
22
|
+
a normal runtime API — the collision is only in the name.) Likewise
|
|
23
|
+
`Match.matches` / `Match.assert` are comparison helpers for `*.test.ts` only —
|
|
24
|
+
never a runtime branch.
|
|
25
|
+
|
|
26
|
+
## What `@adobe/data/testing` gives you
|
|
27
|
+
|
|
28
|
+
Two namespaces, imported only from `*.test.ts` (the module is
|
|
29
|
+
`sideEffects: false`, and `vitest` is an optional peer dependency, satisfied
|
|
30
|
+
here):
|
|
31
|
+
|
|
32
|
+
- **`Match`** — the tolerant, matcher-aware value comparison: `matches(actual,
|
|
33
|
+
expected, options?)` and its throwing wrapper `assert(...)`, plus the matchers
|
|
34
|
+
`Match.anyNumber` / `Match.anyString` / `Match.ref(label)`. Options are
|
|
35
|
+
`{ unordered?: ReadonlySet<string>; tolerance?: number }` — arrays compare **in
|
|
36
|
+
order** (default) unless a key is named in `unordered`, and numbers snap to
|
|
37
|
+
`tolerance` (default `0.01`) to absorb F32↔f64 / trig noise. Framework-agnostic:
|
|
38
|
+
it honors any asymmetric matcher, so vitest's `expect.any(...)` interops.
|
|
39
|
+
- **`Conformance`** — the case types (`Case`, `Cases`, `DerivationCase`,
|
|
40
|
+
`DerivationCases`, `Effects`, `ServiceCall`), the `entity(specId)` identity
|
|
41
|
+
marker, the id `resolver(map)`, the whole-feature driver **`runFeature`**, the
|
|
42
|
+
pure-spec driver **`runSpec`**, and the lower-level per-surface drivers
|
|
43
|
+
`runTransactions` / `runActions` / `runComputeds` (internals of `runFeature`,
|
|
44
|
+
exported for the escape hatch below). Auto-pairing (transition ⇄ op by name),
|
|
45
|
+
effect recording, and id resolution are built **into** the drivers — no
|
|
46
|
+
per-feature helper writes them. The `Effects` type-test also lives here (once),
|
|
47
|
+
so there is no per-feature `conformance-case.type-test.ts` to author.
|
|
48
|
+
|
|
49
|
+
## Projection (store ⇄ State) — the one per-feature piece
|
|
50
|
+
|
|
51
|
+
`projection.ts` is the only feature-specific file, and it is a **single
|
|
52
|
+
aggregated export** — the three test-only helpers in one file, one `export`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// conformance/projection.ts
|
|
56
|
+
export const projection = { fromState, toState, toData };
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `fromState(store, state)` seeds a store to a `State` (clear tail→head, insert
|
|
60
|
+
entities, set resources). It **returns the `id → entity` map**
|
|
61
|
+
(`ReadonlyMap<Id, Entity>`) it built while seeding — the ECS assigns ids from
|
|
62
|
+
its own id-space, so the runner turns this map into a `resolve` via
|
|
63
|
+
`Conformance.resolver`; **no feature writes id resolution by hand**. A feature
|
|
64
|
+
whose transactions are index-addressed or singleton returns `void`, and any id
|
|
65
|
+
then resolves to `Entity.none`.
|
|
66
|
+
- `toState(store)` reads the whole store back, built on `toData`.
|
|
67
|
+
- `toData(store, entity)` reads one entity as its `data/` value — the single
|
|
68
|
+
place the ECS↔data mapping lives; the runner reuses it to hydrate id-list
|
|
69
|
+
computed outputs. **Present only when the feature has entities** (omit for a
|
|
70
|
+
scalar / resource-only feature).
|
|
71
|
+
|
|
72
|
+
This replaces the old separate `create-store.ts` / `from-state.ts` /
|
|
73
|
+
`to-state.ts` / `to-data.ts`. **`create-store` is gone** — `runFeature` builds
|
|
74
|
+
every store/db itself with `Store.create(plugin)` / `Database.create(plugin)`.
|
|
75
|
+
|
|
76
|
+
## ECS conformance is ONE call — `Conformance.runFeature`
|
|
77
|
+
|
|
78
|
+
`conformance/conformance.test.ts` is a single call that conforms the whole
|
|
79
|
+
feature — transaction + action + computed conformance **plus** the projection
|
|
80
|
+
round-trip. No per-surface test file, no `define` callback, no `conforms(...)`
|
|
81
|
+
adapters, no coverage guard.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { State } from "../../../data/state/state.js";
|
|
85
|
+
import { transitions } from "../../../data/state/transitions.js";
|
|
86
|
+
|
|
87
|
+
Conformance.runFeature({
|
|
88
|
+
state: State,
|
|
89
|
+
transitions,
|
|
90
|
+
plugin: MainService.plugin,
|
|
91
|
+
computedPlugin: ComputedDatabase.plugin, // omit if no state/ derivations
|
|
92
|
+
projection,
|
|
93
|
+
hydrate: ["visibleTodos"], // entity-id-list computeds; omit if none
|
|
94
|
+
match: { unordered: new Set(["bullets", "asteroids"]) }, // entity bags / tolerance; omit if none
|
|
95
|
+
ops: { actions: import.meta.glob([...]) }, // ONLY when ops aren't in the plugin facet
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **`state`** is the `State` namespace itself — the runner calls `State.create()`
|
|
100
|
+
for the default each case's `before` deltas over, and round-trips `State.samples`
|
|
101
|
+
through the projection.
|
|
102
|
+
- **`transitions`** is imported from the feature's test-only
|
|
103
|
+
`data/state/transitions.ts` (the single `import.meta.glob` of the `{ fn, cases }`
|
|
104
|
+
source, shared with `spec.test.ts` — see `data/state.md`). Never re-glob it here.
|
|
105
|
+
- **Ops come off the plugin facets.** The runner reads
|
|
106
|
+
`plugin.transactions` / `plugin.actions` / `computedPlugin.computed` and builds
|
|
107
|
+
the stores/dbs itself (`Store.create(plugin)` for transactions,
|
|
108
|
+
`Database.create(plugin)` for actions, `Database.create(computedPlugin)` for
|
|
109
|
+
computeds). Each op pairs to its **same-named** transition/derivation; auto-pairing
|
|
110
|
+
can't forget an item, so there is **no coverage guard** — an op with **no
|
|
111
|
+
same-named transition** is infrastructure or system-dispatched and is simply
|
|
112
|
+
**skipped** (a streaming action with no transition is skipped too).
|
|
113
|
+
- **`ops`** overrides the facet-discovered ops per surface — use it **only** when
|
|
114
|
+
an op isn't registered in the plugin facet: a per-transition action kept out of
|
|
115
|
+
the facet (to bound the plugin's type) is discovered via an
|
|
116
|
+
`ops.actions: import.meta.glob([".../actions/*.ts", "!.../actions/index.ts"], { eager: true })`
|
|
117
|
+
glob (p2p negotiation). Ordinary features omit `ops` entirely.
|
|
118
|
+
- **`computedPlugin`** is the `ComputedDatabase` **layer** plugin — build computed
|
|
119
|
+
conformance from this layer, **not** the assembled `MainService`: a behaviour
|
|
120
|
+
layer above may `withCache` a pre-seed value that a direct `fromState` seed emits
|
|
121
|
+
no transaction to invalidate; the computed layer keeps the seed authoritative.
|
|
122
|
+
Omit it when the feature has no `state/` derivations (a computed with no
|
|
123
|
+
derivation is skipped anyway, and single-`data/<type>` math is covered by that
|
|
124
|
+
helper's own test).
|
|
125
|
+
- **`hydrate`** names the computeds that emit an **entity-id list** (todo's
|
|
126
|
+
`visibleTodos`) so the runner maps each id through `toData` into the value shape
|
|
127
|
+
the derivation yields. Comparison is identity otherwise. Omit if none.
|
|
128
|
+
- **`match`** threads `MatchOptions` through every comparison (see below). Omit if
|
|
129
|
+
none.
|
|
130
|
+
- **Projection round-trip.** When `State.samples` is non-empty, the runner adds a
|
|
131
|
+
`toState ∘ fromState ≡ identity` test per sample — proving the pair round-trips
|
|
132
|
+
faithfully so a symmetric bug in `fromState`/`toState` can't cancel out and mask
|
|
133
|
+
a real ECS defect. No separate `projection.test.ts`.
|
|
134
|
+
|
|
135
|
+
`data-lit-tictactoe` is the zero-config call (no `computedPlugin`, no `hydrate`,
|
|
136
|
+
no `match`, no `ops` — moves are board-index addressed, so no `entity()`
|
|
137
|
+
markers). `data-lit-todo` adds `hydrate: ["visibleTodos"]` and `entity()` markers.
|
|
138
|
+
`data-lit-space-rock-game` adds `match: { unordered: … }` for its entity bags
|
|
139
|
+
(its per-frame transitions are conformed by the systems tick loop, not here — see
|
|
140
|
+
`systems.md`).
|
|
141
|
+
|
|
142
|
+
## The pure spec — `data/state/spec.test.ts`
|
|
143
|
+
|
|
144
|
+
Lives in `data/state/`, not here. One call, importing the same `transitions`:
|
|
46
145
|
|
|
47
146
|
```ts
|
|
48
|
-
|
|
147
|
+
import { State } from "./state.js";
|
|
148
|
+
import { transitions } from "./transitions.js";
|
|
149
|
+
|
|
150
|
+
Conformance.runSpec({ state: State, transitions });
|
|
49
151
|
```
|
|
50
152
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
153
|
+
It discovers every file exporting `cases`, enforces the two-exports rule, and
|
|
154
|
+
dispatches on case shape (transition → state + effects, derivation →
|
|
155
|
+
`fn(input) ≡ value`), seeding each case's `before`/`input` as a delta over
|
|
156
|
+
`State.create()`. Unwraps any `entity(specId)` arg marker to its plain data-id for
|
|
157
|
+
the pure side.
|
|
158
|
+
|
|
159
|
+
## The exception — a per-surface `userId`, via the lower-level runners
|
|
160
|
+
|
|
161
|
+
A feature that needs **ambient per-case context differing per surface** — a
|
|
162
|
+
user-scoped `userId` that must be seeded before the raw transaction *and*
|
|
163
|
+
independently before the action dispatch (**p2p presence**) — does **not** use
|
|
164
|
+
`runFeature`. It calls `Conformance.runTransactions` / `runActions` (still
|
|
165
|
+
exported) directly, each with its own `seedContext` (and any per-surface
|
|
166
|
+
concurrency), because the seam differs between the transaction store and the
|
|
167
|
+
action db. `runFeature` has no place to thread two different `seedContext`s, so
|
|
168
|
+
this feature drops to the lower level. This is the escape hatch — ordinary
|
|
169
|
+
features never touch these drivers directly.
|
|
170
|
+
|
|
171
|
+
## Identity — the `entity(specId)` marker
|
|
172
|
+
|
|
173
|
+
An entity-addressed transition writes its addressed id as `args: { id: entity(2) }`
|
|
174
|
+
— import `entity`, re-exported from the feature's `data/state/conformance-case.ts`.
|
|
175
|
+
`runSpec` unwraps it to the plain data-id; the ECS runner resolves it to the
|
|
176
|
+
**seeded entity** via the id→entity map `fromState` returns (turned into a `resolve`
|
|
177
|
+
by `Conformance.resolver` — no feature writes `resolve` by hand). Two conventions
|
|
178
|
+
make the wiring vanish: the ECS op takes the entity **under the transition's own arg
|
|
179
|
+
key** (`{ id }`, same-shape args, no reshape), and `fromState` returns the
|
|
180
|
+
`ReadonlyMap<Id, Entity>` id→entity map (or `void` for an index-addressed / singleton
|
|
181
|
+
feature, whose ids then resolve to `Entity.none`). `data-lit-todo` is the reference.
|
|
182
|
+
|
|
183
|
+
## Name-parity — add a same-named op, never a per-item adapter
|
|
184
|
+
|
|
185
|
+
Every app transition is realized by a **same-named** transaction and/or action. When
|
|
186
|
+
the real UI op is richer or renamed — todo's `dragTodo`, space-rock's `newGame` — that
|
|
187
|
+
op is infra, and a thin **same-named** op (todo's `reorderTodo` action, space-rock's
|
|
188
|
+
`createInitial` transaction) gives the transition something to pair with. Do **not**
|
|
189
|
+
reintroduce a per-item adapter to bridge a name mismatch — add the same-named op.
|
|
190
|
+
|
|
191
|
+
## Ordering, tolerance, `ref` — all via `match`
|
|
192
|
+
|
|
193
|
+
`runFeature` threads `match?: MatchOptions` through to every comparison, so
|
|
194
|
+
per-feature tuning is data, not code:
|
|
195
|
+
|
|
196
|
+
- **Ordered by default.** `toState` reads a display-ordered collection in order —
|
|
197
|
+
that is what verifies a reorder — and positional tuples (a `Vec2`) must stay in
|
|
198
|
+
order.
|
|
199
|
+
- **Entity bags compare as multisets.** A collection the ECS materialises with no
|
|
200
|
+
display order (bullets, asteroids) is named in
|
|
201
|
+
`match: { unordered: new Set(["bullets", "asteroids"]) }` so just those fields
|
|
202
|
+
compare order-independently — never blanket-unordered (it would conflate
|
|
203
|
+
`[100,180]` with `[180,100]`).
|
|
204
|
+
- **Float noise** is absorbed by the default `tolerance` (`0.01`); raise it only
|
|
205
|
+
when a case genuinely needs a looser grid.
|
|
206
|
+
- **`Match.ref(label)`** on the expected side asserts id correspondence for a
|
|
207
|
+
referential feature (an id that must line up in two places); `Match.anyNumber` /
|
|
208
|
+
`anyString` leave an id a case doesn't pin fully open.
|
|
209
|
+
|
|
210
|
+
## Recording side effects — built in, no Proxy
|
|
211
|
+
|
|
212
|
+
Effect recording lives in the drivers: they enumerate a plain-object service's own
|
|
213
|
+
methods and closure-wrap each to record `[method, ...args]` calls, then delegate
|
|
214
|
+
(no `Proxy`, per the repo rule). `runSpec` records the case's injected service
|
|
215
|
+
`args`; the action surface records the `db.services` overrides. A case's `effects`
|
|
216
|
+
asserts each **declared** service's calls exactly (`Array` = ordered, `Set` = any
|
|
217
|
+
order); undeclared services (value-returning reads like `generateName`) are
|
|
218
|
+
ignored.
|
|
@@ -119,26 +119,29 @@ export namespace IndexDatabase {
|
|
|
119
119
|
}
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
### The assembled database — `
|
|
122
|
+
### The assembled database — `MainService`
|
|
123
123
|
|
|
124
124
|
*Which* layer is topmost varies with the facets a feature uses (computed /
|
|
125
125
|
service / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/`
|
|
126
126
|
— need the *whole* feature database but must not name that layer, or adding or
|
|
127
|
-
dropping a layer rewrites them all. Alias it once
|
|
127
|
+
dropping a layer rewrites them all. Alias it once from the **folder-eponymous
|
|
128
|
+
`main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in
|
|
129
|
+
its same-named file), as a namespace so consumers reach `MainService.plugin` /
|
|
130
|
+
`MainService.Store`:
|
|
128
131
|
|
|
129
132
|
```ts
|
|
130
|
-
// services/main-service/
|
|
131
|
-
export { SystemDatabase as
|
|
133
|
+
// services/main-service/main-service.ts
|
|
134
|
+
export { SystemDatabase as MainService } from "./system-database/system-database.js";
|
|
132
135
|
```
|
|
133
136
|
|
|
134
|
-
Every consumer references **`
|
|
137
|
+
Every consumer references **`MainService`** (`.plugin`, `.Store`) — never the
|
|
135
138
|
topmost layer. Add or drop a layer → change only this one line.
|
|
136
139
|
|
|
137
140
|
**Cross-feature naming.** Inside a feature the core and assembled databases are
|
|
138
|
-
the bare `CoreDatabase` / `
|
|
141
|
+
the bare `CoreDatabase` / `MainService`. When *another* feature or package
|
|
139
142
|
imports one — a peer built on it, the base `imports` a peer's `core-database`, a
|
|
140
143
|
downstream package — reference it **feature-qualified**: `<Feature>CoreDatabase`
|
|
141
|
-
/ `<Feature>
|
|
144
|
+
/ `<Feature>MainService`, re-exported so from the feature's barrel
|
|
142
145
|
(`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so
|
|
143
146
|
two features' `CoreDatabase`s never collide.
|
|
144
147
|
|
|
@@ -153,9 +156,10 @@ two features' `CoreDatabase`s never collide.
|
|
|
153
156
|
reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
|
|
154
157
|
moment it reads an index.
|
|
155
158
|
- **the whole `Database`** — **computed**, **services**, and **actions** each
|
|
156
|
-
take `
|
|
157
|
-
read/call): computed reads `
|
|
158
|
-
transactions; actions call `
|
|
159
|
+
take `service: <Layer>` (the lowest layer whose database exposes what they
|
|
160
|
+
read/call): computed reads `service.observe.*`; services read observables and
|
|
161
|
+
call transactions; actions call `service.services.*` then
|
|
162
|
+
`service.transactions.*`.
|
|
159
163
|
|
|
160
164
|
Each subfolder has its own rule. Modelling a plugin's authored vs. derived
|
|
161
165
|
surface is covered by `plugin-modelling.md`.
|
|
@@ -5,20 +5,21 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# database/services/ — database-bound service factories
|
|
7
7
|
|
|
8
|
-
One factory per file: `create<Name>Service(
|
|
8
|
+
One factory per file: `create<Name>Service(service, …)` that binds a service
|
|
9
9
|
implementation to a live database — reading its observables and invoking
|
|
10
10
|
its transactions. This is where the async `services/` contracts (or
|
|
11
11
|
framework services like `AgenticService`) are wired to ECS state.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
export const createAgentService = (
|
|
15
|
-
/* … reads
|
|
14
|
+
export const createAgentService = (service: ComputedDatabase, mark: PlayerMark): AgenticService =>
|
|
15
|
+
/* … reads service.observe.*, calls service.transactions.* … */;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Type `
|
|
19
|
-
`service-database` itself — that would be a cycle).
|
|
18
|
+
Type `service` on the lowest layer exposing what the factory reads/calls (never
|
|
19
|
+
the `service-database` itself — that would be a cycle).
|
|
20
20
|
|
|
21
21
|
An `index.ts` barrel re-exports the factories; `service-database.ts`
|
|
22
22
|
registers them under the `services` facet, each keyed by the name
|
|
23
|
-
consumers read it as (`
|
|
24
|
-
contract (not yet bound to a
|
|
23
|
+
consumers read it as (`service.services.agent`). Defining a standalone
|
|
24
|
+
service contract (not yet bound to a database) belongs in the feature
|
|
25
|
+
`services/` layer.
|
|
@@ -30,12 +30,21 @@ export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
|
|
|
30
30
|
than throwing.
|
|
31
31
|
- Decisions come from pure `data/` helpers; the transaction only applies the
|
|
32
32
|
result — read the touched slice, call the `data/` transform, write the diff.
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
- Keep transaction files **single-export** (the `transactions/` barrel is
|
|
34
|
+
`export *`-ed into the plugin facet, so a second export would pollute it).
|
|
35
|
+
- **Conformance is wired once, centrally, and auto-paired** — not per-file. The
|
|
36
|
+
feature's single `conformance/conformance.test.ts` `Conformance.runFeature({...})`
|
|
37
|
+
call conforms transactions: it pulls them off **`plugin.transactions`** (the
|
|
38
|
+
registered facet), pairs each to the **same-named** `data/state` transition, and
|
|
39
|
+
conforms it — seed `fromState(before)`, apply, `Match.assert` `toState ≡ after`
|
|
40
|
+
(state only; service effects are asserted through the action). There is **no**
|
|
41
|
+
`define`/`conforms` adapter and **no** `covers` guard. A transaction taking
|
|
42
|
+
**entity ids** takes them under the transition's own arg key (`{ id }`); the runner
|
|
43
|
+
resolves each `entity(specId)` marker via the id→entity map `fromState` returns. A
|
|
44
|
+
transaction with **no same-named transition** is infrastructure (`setInput`,
|
|
45
|
+
`setBounds`) or the drag UI op (`dragTodo`) or system-dispatched — it is simply
|
|
46
|
+
**skipped**, no guard needed. Don't add a per-item adapter for a renamed op; add a
|
|
47
|
+
thin same-named transaction instead (see `conformance.md`).
|
|
39
48
|
- An `index.ts` barrel feeds the `transactions` plugin facet — so it must
|
|
40
49
|
re-export **only** the mutations. A read/query helper shared by several
|
|
41
50
|
transactions (`readShip`, `readBoard` — a `(t) => value` function) may live
|
|
@@ -38,7 +38,7 @@ A binding element is a thin wire between a service and a presentation. The class
|
|
|
38
38
|
|
|
39
39
|
No other members. No `@state`. No private handler fields. No lifecycle methods (`connectedCallback`, `updated`, `firstUpdated`,
|
|
40
40
|
`disconnectedCallback`) except on app-entrypoint classes where the lifecycle concern is app-boot shaped but cannot be expressed as a hook
|
|
41
|
-
(see
|
|
41
|
+
(see `hooks.md` for hook-first lifecycle).
|
|
42
42
|
|
|
43
43
|
## `@property` allowlist
|
|
44
44
|
|
|
@@ -114,7 +114,7 @@ one-liners** above.
|
|
|
114
114
|
## Lifecycle goes through hooks
|
|
115
115
|
|
|
116
116
|
Any need for mount/unmount/update behavior routes through a hook. **Reuse order, one hook per file, presentation vs binding placement, and
|
|
117
|
-
testing notes:**
|
|
117
|
+
testing notes:** `hooks.md`.
|
|
118
118
|
|
|
119
119
|
Never inline lifecycle logic in the binding. App-entrypoint classes may override lifecycle methods when the concern is app-boot shaped
|
|
120
120
|
(service wiring, ancestor lookup); even then, prefer a hook.
|
|
@@ -158,14 +158,14 @@ expose them by design; reaching for them means a missing computed.
|
|
|
158
158
|
|
|
159
159
|
A binding element imports a sibling `*-presentation.ts` file that exports exactly `render` (required) and optionally `unlocalized`. Nothing
|
|
160
160
|
else — no types, no styles, no helpers. Consumers derive the props type via `Parameters<typeof render>[0]`. Presentation stays hook-free by
|
|
161
|
-
default; element hooks live here or in shared modules per
|
|
161
|
+
default; element hooks live here or in shared modules per `hooks.md` and `features/ui/presentation.md`.
|
|
162
162
|
|
|
163
163
|
**For refactors whose stated scope does not include presentations**, the matching `*-presentation.ts` file must not change. Verify with
|
|
164
164
|
`git diff --name-only | grep presentation`. If a scope-limited refactor requires a presentation signature change, stop and reconsider the
|
|
165
165
|
binding-side helper — the scope fence exists because presentations are the public contract between binding and render output.
|
|
166
166
|
|
|
167
167
|
Callback prop names use **verb** or **verbNoun** form. Never `on*` prefix. The binding-side variable matches the presentation prop name 1:1.
|
|
168
|
-
See
|
|
168
|
+
See `features/ui/presentation.md` for full examples.
|
|
169
169
|
|
|
170
170
|
## Size budget
|
|
171
171
|
|
|
@@ -32,8 +32,10 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
|
|
|
32
32
|
- `<type-name>/<type-name>.ts` is the only public import surface
|
|
33
33
|
- Export a single type alias: `export type <type-name> = <type>` (or `export type * from "./<type-name>-types.js"` when the type lives in a
|
|
34
34
|
separate file)
|
|
35
|
-
-
|
|
36
|
-
`
|
|
35
|
+
- The type alias is **hand-authored** in `<type-name>.ts`. When a schema is needed it lives in `<type-name>-schema.ts` (or
|
|
36
|
+
`schema.ts` inside the folder), named `schema` (not `<type-name>Schema`), and is **written to match the hand-authored type and pinned
|
|
37
|
+
to it** with `Assert<Equal<Schema.ToType<typeof schema>, <TypeName>>>` — never the other way around. `Schema.ToType` appears only in
|
|
38
|
+
that assertion, never as the exported type.
|
|
37
39
|
- Export namespace: `export * as <type-name> from "./public.js"`
|
|
38
40
|
- In public.ts, re-export every public constant file
|
|
39
41
|
- Filenames map deterministically to single export:
|
|
@@ -43,6 +45,26 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
|
|
|
43
45
|
- Function files: name by purpose only (e.g., `add.ts`, `sub.ts`). **Do not prefix** with the type name — the folder provides the namespace
|
|
44
46
|
context.
|
|
45
47
|
|
|
48
|
+
## Constants file
|
|
49
|
+
|
|
50
|
+
Constants are the **one exception** to one-declaration-per-file. A namespace folder MAY have a single `constants.ts` exporting several
|
|
51
|
+
plain `const` literals (no functions, no types), re-exported through `public.ts` so callers reach them as `<TypeName>.<constant>`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// duration/constants.ts
|
|
55
|
+
export const minSeconds = 5;
|
|
56
|
+
export const maxSeconds = 300;
|
|
57
|
+
export const defaultSeconds = 30;
|
|
58
|
+
|
|
59
|
+
// duration/public.ts
|
|
60
|
+
export * from './constants.js';
|
|
61
|
+
|
|
62
|
+
// consumers: Duration.minSeconds, Duration.maxSeconds, Duration.defaultSeconds
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Function declarations remain **one per file** (each in its own purpose-named file). If a value needs logic rather than being a plain
|
|
66
|
+
literal, it is a function and does not belong in `constants.ts`.
|
|
67
|
+
|
|
46
68
|
## Anti-patterns (do not copy)
|
|
47
69
|
|
|
48
70
|
- **`export namespace <TypeName> { ... }`** — Use `export * as <TypeName> from "./public.js"` instead.
|
|
@@ -63,18 +85,24 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
|
|
|
63
85
|
|
|
64
86
|
## Schema pattern example
|
|
65
87
|
|
|
88
|
+
The type is authored first and by hand; the schema (if/when needed) is written
|
|
89
|
+
to match it and pinned so it cannot drift.
|
|
90
|
+
|
|
66
91
|
```ts
|
|
67
|
-
// src/types/player-mark/player-mark
|
|
68
|
-
export
|
|
92
|
+
// src/types/player-mark/player-mark.ts — HAND-AUTHORED, the source of truth
|
|
93
|
+
export type PlayerMark = 'X' | 'O';
|
|
94
|
+
export * as PlayerMark from './public.js';
|
|
95
|
+
|
|
96
|
+
// src/types/player-mark/player-mark-schema.ts — written to MATCH the type
|
|
97
|
+
import { Schema } from '@adobe/data/schema';
|
|
98
|
+
import type { Assert, Equal } from '@adobe/data/types';
|
|
99
|
+
import type { PlayerMark } from './player-mark.js';
|
|
100
|
+
export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema;
|
|
101
|
+
// Compile-time pin: fails to build if schema and type diverge.
|
|
102
|
+
type _Pin = Assert<Equal<Schema.ToType<typeof schema>, PlayerMark>>;
|
|
69
103
|
|
|
70
104
|
// src/types/player-mark/public.ts
|
|
71
105
|
export { schema } from './player-mark-schema.js';
|
|
72
|
-
|
|
73
|
-
// src/types/player-mark/player-mark.ts
|
|
74
|
-
import { Schema } from '@adobe/data/schema';
|
|
75
|
-
import { schema } from './player-mark-schema.js';
|
|
76
|
-
export type PlayerMark = Schema.ToType<typeof schema>;
|
|
77
|
-
export * as PlayerMark from './public.js';
|
|
78
106
|
```
|
|
79
107
|
|
|
80
108
|
## Example: `Point` type refactor
|
|
@@ -76,7 +76,7 @@ use `Parameters<typeof render>[0]`.
|
|
|
76
76
|
|
|
77
77
|
## Action callbacks (not events)
|
|
78
78
|
|
|
79
|
-
Use **verb** or **verbNoun** names — never `on*` prefix. See
|
|
79
|
+
Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples.
|
|
80
80
|
|
|
81
81
|
**Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments:
|
|
82
82
|
|
package/.claude/rules/lit.md
CHANGED
|
@@ -76,7 +76,7 @@ there.
|
|
|
76
76
|
|
|
77
77
|
## Action callbacks (not events)
|
|
78
78
|
|
|
79
|
-
Use **verb** or **verbNoun** names — never `on*` prefix. See
|
|
79
|
+
Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples and rationale.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -91,7 +91,7 @@ view hosts. Multiple instances of the same element need a property to identify w
|
|
|
91
91
|
|
|
92
92
|
## Hooks
|
|
93
93
|
|
|
94
|
-
Canonical rules (reuse order, one hook per file, binding vs presentation):
|
|
94
|
+
Canonical rules (reuse order, one hook per file, binding vs presentation): `hooks.md`.
|
|
95
95
|
|
|
96
96
|
Use hooks from `src/elements/hooks/` and `@adobe/data-lit` instead of `connectedCallback`/`disconnectedCallback`. Use `useEffect` for
|
|
97
97
|
setup/teardown, `useKeyboardEvent` for single key events.
|
|
@@ -107,7 +107,7 @@ When creating or modifying a Lit element:
|
|
|
107
107
|
3. Use a single `useObservableValues` in the binding element. Observe only minimal values; use `Observe.withDefault` for slow-resolving
|
|
108
108
|
values.
|
|
109
109
|
4. Pass observed values and action callbacks to presentation.
|
|
110
|
-
5. Keep presentation pure — no hooks (see
|
|
110
|
+
5. Keep presentation pure — no hooks (see `hooks.md` for exceptions).
|
|
111
111
|
6. Add `@property` only when entity binding requires it (multiple instances).
|
|
112
112
|
7. Presentation exports only `render` (and localization bundles where appropriate). Binding element sets `static styles` from `*.css.ts`.
|
|
113
113
|
8. Add `*-presentation.test.ts` for presentation when appropriate; do not unit test binding elements.
|
package/.claude/rules/observe.md
CHANGED
|
@@ -147,7 +147,7 @@ Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(user
|
|
|
147
147
|
`observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually
|
|
148
148
|
read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate
|
|
149
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
|
|
150
|
+
observe its own entity (see `ecs.md`).
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|