@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.
@@ -3,69 +3,216 @@ paths:
3
3
  - '**/features/*/services/main-service/**/conformance/**/*.ts'
4
4
  ---
5
5
 
6
- # services/main-service/conformance/ — spec↔implementation projection (test-only)
7
-
8
- The bridge that keeps the ecs implementation honest against the `data/` spec.
9
- **Test-only**: imported solely by `*.test.ts` and in no facet barrel, so it never
10
- enters the runtime bundle (test-support may import the test framework — it is not
11
- a runtime declaration). **Never call `fromState` / `toState` (or a shared
12
- clear-all + reinsert helper used only for that projection) from systems,
13
- transactions, or UI** — that is a full-store rewrite, not an O(1) ECS update.
14
- **Feature-level**: one projection per feature, reused by every transaction test
15
- and the system tick-loop test — don't nest it under `transaction-database/`.
16
-
17
- The property, per `{ before, args, after }` case:
18
- `toState(apply(fromState(before), args)) ≡ spec(before, args)`.
19
-
20
- Conformance test-support splits by concern along the layer boundary:
21
-
22
- - **Store-side, here in `services/main-service/conformance/`:**
23
- - `from-state.ts` → `fromState(store: CoreDatabase.Store, state: State): void`
24
- seeds the store (clear tail→head → insert entities → set resources) to a `State`.
25
- - `to-state.ts` → `toState(store): State` reads the store back — each kind via
26
- its full named-archetype component set, so the shapes never alias.
27
- - `expect-conforms.ts` → **one export, `expectConforms({ cases, spec, apply })`**.
28
- Per case it asserts `spec(before, args) ≡ after` (keeps the case honest), then
29
- `fromState(before)` → `apply(store, args)` → `toState ≡ after`.
30
- - **Spec-side, in `data/state/`** (State values, no store — so both the `data/`
31
- transform tests and this runner import them without a layer violation):
32
- `conformance-case.ts` (`ConformanceCase<Args>`), the `<transform>.cases.ts`
33
- cases, and `expect-state-matches.ts` (State equality).
34
-
35
- ## No cast — build on `Store.create`, not a `Database`
36
-
37
- A **transaction is `(store, args) => void`**, so transaction conformance needs no
38
- `Database`: `Store.create(<schema-layer>.plugin)` returns a cast-free writable
39
- `CoreDatabase.Store` — pass the plugin directly, `Store.create` reads its schema
40
- facets. Source it from the **lowest layer that declares all the schema** —
41
- `IndexDatabase`, or `CoreDatabase` if the feature has no indexes — **not**
42
- `FeatureDatabase`: the store needs only schema, and the behaviour layers
43
- (transactions / computed / systems) add none.
44
- `fromState`/`toState`/`apply` all operate on it, and `apply` calls the **raw
45
- transaction function** directly:
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
- expectConforms({ cases, spec: State.hitTarget, apply: (store, args) => hitTarget(store, args) });
147
+ import { State } from "./state.js";
148
+ import { transitions } from "./transitions.js";
149
+
150
+ Conformance.runSpec({ state: State, transitions });
49
151
  ```
50
152
 
51
- A mutation addressed by **entity id** resolves its entities from the seeded store
52
- inside `apply` (the shared cases stay spec-shaped). Reads never need a cast —
53
- `Database extends ReadonlyStore`.
54
-
55
- **Systems** are the one exception: they run via `db.system.functions` and reach
56
- the store through the db. Get that writable view with the library lens —
57
- `Database.toSystemDatabase(Database.create(plugin))` (the widening twin of
58
- `UIService.restrict`, cast-free) — then `fromState(db.store, before)` → drive one
59
- frame → `toState(db.store)`. Test **selection/detection** logic (which entities
60
- interact) separately, with seeded edge-case geometries.
61
-
62
- ## State equality — ordering vs precision, kept separate
63
-
64
- `expect-state-matches.ts` compares with **`equalsUnordered`** from `@adobe/data`
65
- (arrays as multisets — archetype hole-fill reorders rows — objects key-order
66
- independent). Absorb float noise — F32↔f64 storage rounding **and** trig epsilon
67
- (a quadrant `cos`/`sin` yields ~3e-15 where a case authors `0`) — by quantizing
68
- every number on both sides onto a small grid (`Math.round(Math.fround(n)*k)/k`)
69
- before comparing. Ordering and precision stay separate concerns. **Guard the
70
- projection itself** with one `fromState → toState` identity test on
71
- representative states.
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 — `FeatureDatabase`
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/feature-database.ts
131
- export { SystemDatabase as FeatureDatabase } from "./system-database/system-database.js";
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 **`FeatureDatabase`** (`.plugin`, `.Store`) — never the
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` / `FeatureDatabase`. When *another* feature or package
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>Database`, re-exported so from the feature's barrel
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 `db: <Layer>` (the lowest layer whose database exposes what they
157
- read/call): computed reads `db.observe.*`; services read observables and call
158
- transactions; actions call `db.services.*` then `db.transactions.*`.
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(db, …)` that binds a 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 = (db: ComputedDatabase, mark: PlayerMark): AgenticService =>
15
- /* … reads db.observe.*, calls db.transactions.* … */;
14
+ export const createAgentService = (service: ComputedDatabase, mark: PlayerMark): AgenticService =>
15
+ /* … reads service.observe.*, calls service.transactions.* … */;
16
16
  ```
17
17
 
18
- Type `db` on the lowest layer exposing what the factory reads/calls (never the
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 (`db.services.agent`). Defining a standalone service
24
- contract (not yet bound to a db) belongs in the feature `services/` layer.
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
- - **Conform every transaction to its `data/` transform** in its sibling
34
- `*.test.ts`, via `expectConforms` over the spec's shared cases (see
35
- `services/main-service/conformance.md`): seed `fromState(before)`, dispatch, assert `toState ≡
36
- after`, covering every branch and edge case. A transaction taking **entity
37
- ids** resolves them from the seeded store in the `apply` closure; one with no
38
- `data/` analogue (`setBounds`, `setInput`) gets a direct resource assertion.
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 `.claude/rules/hooks.md` for hook-first lifecycle).
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:** `.claude/rules/hooks.md`.
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 `.claude/rules/hooks.md` and `.claude/rules/presentation.md`.
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 `.claude/rules/presentation.md` for full examples.
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
- - Schema-based types: schema in `<type-name>-schema.ts`, named `schema` (not `<type-name>Schema`), derive with
36
- `Schema.ToType<typeof schema>`
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-schema.ts
68
- export const schema = { enum: ['X', 'O'] } as const;
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 `.claude/rules/presentation.md` for full examples.
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
 
@@ -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 `.claude/rules/presentation.md` for full examples and rationale.
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): `.claude/rules/hooks.md`.
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 `.claude/rules/hooks.md` for exceptions).
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.
@@ -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 `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`).
150
+ observe its own entity (see `ecs.md`).
151
151
 
152
152
  ---
153
153
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adobe-data-ai",
3
- "version": "0.9.91",
3
+ "version": "0.9.93",
4
4
  "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
5
5
  "author": {
6
6
  "name": "Adobe"