@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,95 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/conformance/**/*.ts'
4
- ---
5
-
6
- # services/main-service/conformance/ — keeping the ECS honest against the spec
7
-
8
- **State-based features only** (a feature with a `data/state/` spec). Test-only: imported
9
- only by `*.test.ts`, in no facet barrel. The `data/state` cases (authored per
10
- `../../../data/state.md`, gathered by the `spec.ts` manifest) are the shared truth; the
11
- `@adobe/data-testing` runner replays them against the ECS. This folder holds only the
12
- **feature-specific projection** (`projection.ts`) plus one `conformance.test.ts`.
13
- Reference: `data-lit-tictactoe` (minimal), `data-lit-todo` (entities + `hydrate`).
14
-
15
- **The projection helpers and `Match` are test-only — never a runtime branch.**
16
- `fromState`/`toState` rewrite the whole store out-of-band; runtime code reads through
17
- observables/indexes and writes through transactions.
18
-
19
- ## `projection.ts` — the store ⇄ State mapping (the one per-feature piece)
20
-
21
- A single aggregated export — three test-only helpers, one `export`:
22
-
23
- ```ts
24
- export const projection = { fromState, toState, toData };
25
- ```
26
-
27
- - `fromState(store, state)` seeds a store to a `State` (clear, insert entities, set
28
- resources) and **returns the `id → entity` map** it built (`ReadonlyMap<Id, Entity>`).
29
- The ecs mints its own ids, so the runner turns this map into the resolver that maps a
30
- case's spec-id to the seeded entity — no feature writes id resolution by hand. Return
31
- `void` for an index-addressed / singleton feature (ids then resolve to `Entity.none`).
32
- - `toState(store)` reads the whole store back, built on `toData`.
33
- - `toData(store, entity)` reads one entity as its `data/` value — the single place the
34
- ecs↔data mapping lives. Its **return type** is what tells conformance which derivations
35
- are entity-list computeds. Present only when the feature has entities.
36
-
37
- ## `conformance.test.ts` — one call
38
-
39
- ```ts
40
- import { Conformance } from "@adobe/data-testing";
41
- import { spec } from "../../../data/state/spec.js";
42
- Conformance.checkFeature(spec);
43
- ```
44
-
45
- `checkFeature` replays the manifest's shared cases against the ECS: it pulls the
46
- transactions/actions off `spec.plugin`, the computeds off `spec.computedPlugin`, seeds
47
- each case's `before` over `State.create()`, synthesizes each service's recording double
48
- from `spec.services` + the case's `responses`, round-trips `State.samples` through
49
- `spec.projection`, and hydrates the id-emitting computeds named in `spec.hydrate` through
50
- `toData`. It shares `spec` with `spec.test.ts` — same cases, substituted implementation.
51
-
52
- Everything `checkFeature` needs lives on the manifest (`../../../data/state.md`):
53
- `plugin`, `computedPlugin`, `projection`, `hydrate`, `services`, `state`, `fns`, `cases`.
54
- The manifest's compile-time guards mean the two calls (`checkSpec` + `checkFeature`) are
55
- the whole conformance surface — no per-op wiring, no coverage guard to hand-maintain.
56
-
57
- - **Name-pairing.** Each ecs op conforms to its **same-named** transition/derivation. An
58
- op with no same-named transition (infrastructure like `setInput`, a streaming action,
59
- a richer UI op) is skipped — so when the real op is renamed (`dragTodo`), add a thin
60
- **same-named** op (`reorderTodo`) for the transition to pair with; never a per-item
61
- adapter.
62
- - **Projection round-trip.** When `State.samples` is non-empty the runner adds a
63
- `toState ∘ fromState ≡ identity` test per sample.
64
- - **Comparison.** Ordering is carried by the value's type — `ReadonlyArray` positional,
65
- `ReadonlySet`/`ReadonlyMap` order-independent. Entity ids compare up to an id-bijection
66
- from the schema-marked spec-ids (no matcher). Float noise is absorbed by
67
- `match?: { tolerance }` (default `0.01`). A minted scalar uses `Match.anyNumber` /
68
- `Match.anyString`.
69
-
70
- ## The escape hatch — per-surface `seedContext` / custom concurrency
71
-
72
- A feature that needs **ambient per-case context differing per surface** — a user-scoped
73
- `userId` seeded before the raw transaction *and* independently before the action dispatch
74
- (**p2p presence**) — cannot use `checkFeature` (it has no place to thread two different
75
- `seedContext`s or a custom concurrency). It drives the lower-level runners directly,
76
- feeding them the manifest's cases via `Conformance.adaptCases`:
77
-
78
- ```ts
79
- import { Conformance } from "@adobe/data-testing";
80
- import { spec } from "../../../data/state/spec.js";
81
-
82
- Conformance.runTransactions({
83
- createStore, fromState, toState,
84
- transitions: Conformance.adaptCases(spec.fns, spec.cases, spec.services, false),
85
- transactions,
86
- seedContext: (store, _before, args) => seedUserId(store, (args as { mark: string }).mark),
87
- });
88
- ```
89
-
90
- `adaptCases(fns, cases, services, injectServices)` turns the manifest into the discovery
91
- map the runners consume — `injectServices: false` for the transaction store (a
92
- transaction never receives services), `true` for the action db. `runActions` /
93
- `runComputeds` take the same shape and their own `makeDb` / `seedContext` / concurrency.
94
- This is the **only** sanctioned use of the lower runners; ordinary features use
95
- `checkFeature`.
@@ -1,48 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/**/core-database/core-database.ts'
4
- ---
5
-
6
- # core-database/core-database.ts — the assembled schema + its namespace
7
-
8
- A thin assembler: `Database.Plugin.create({ components?, resources?, archetypes? })`
9
- over the facet files, exposed through the `CoreDatabase` namespace. This is the
10
- single-file namespace form (one value export — `plugin` — everything else a type;
11
- see `global/namespace.md`).
12
-
13
- ```ts
14
- import { Database, type Store } from "@adobe/data/ecs";
15
- import { components } from "./components.js";
16
- import { resources } from "./resources.js";
17
- import { archetypes } from "./archetypes.js";
18
-
19
- const coreDatabasePlugin = Database.Plugin.create({ components, resources, archetypes });
20
-
21
- export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
22
- // Resolved component map, at module scope so the imported `Store` namespace isn't
23
- // shadowed by `CoreDatabase.Store` below.
24
- type CoreComponents = Store.Components<Database.Plugin.ToStore<typeof coreDatabasePlugin>>;
25
-
26
- export namespace CoreDatabase {
27
- export const plugin = coreDatabasePlugin;
28
- /** The store transactions operate on — a store *is* the transaction context. */
29
- export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
30
- /** Index-declaration type bound to this database's components. */
31
- export type Index = Database.Index<CoreComponents>;
32
- /** A component name, and a component-key tuple — an archetype's defining shape, or the key
33
- * list of a `count` / `select` query. */
34
- export type Component = Extract<keyof typeof components, string>;
35
- export type Archetype = readonly Component[];
36
- }
37
- ```
38
-
39
- ## Derive `Component` / `Archetype` from `components`, never the plugin
40
-
41
- Read the keys straight from the leaf `components` module
42
- (`Extract<keyof typeof components, string>`). Do **not** derive them from the
43
- plugin (`Store.Components<ToStore<plugin>>`, i.e. `CoreComponents`): the plugin is
44
- built *from* `archetypes`, so a plugin-derived `Archetype` is **circular** the
45
- moment it is used to `satisfies`-check an archetype tuple — and the cycle
46
- collapses the entire database type to `{}`. The leaf derivation is cycle-free and
47
- usable both in `archetypes.ts` (to check shared bases) and downstream (to check
48
- `count` / `select` query keys).
@@ -1,187 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/main-service.ts'
4
- - '**/features/*/services/main-service/*-database/*-database.ts'
5
- ---
6
-
7
- # services/main-service/ — the feature service (ECS implementation)
8
-
9
- `main-service/` is the feature's single entrypoint service (see `../index.md`),
10
- implemented as a reactive Entity-Component-System — an implementation detail
11
- behind the service. Everything ECS-specific lives here: the schema that stores `data/` types as
12
- entity state (`core-database/`), the derived and mutating logic over it
13
- (`indexes/`, `transactions/`, `computed/`), the database-bound service factories
14
- (`services/`), the async orchestrators (`actions/`), and the layered plugins
15
- that compose it all.
16
-
17
- The distinction from `data/`: a `data/` type is ECS-agnostic (a plain
18
- serializable value); an ECS construct is *how that type is stored and operated
19
- on* inside a database. `mark: PlayerMark.schema` is not a data-model fact — it
20
- is "the ECS keeps a PlayerMark here".
21
-
22
- ## The core schema — `core-database/`
23
-
24
- A feature's whole schema — every component, resource, and archetype — lives in
25
- one `core-database/` folder: `core-database.ts` plus one file per facet the
26
- feature uses (all three facets are optional on `Database.Plugin.create`):
27
-
28
- ```
29
- core-database/
30
- core-database.ts # Database.Plugin.create({ components?, resources?, archetypes? }) → CoreDatabase
31
- components.ts # export const components = Database.components({ … })
32
- resources.ts # export const resources = Database.resources({ … })
33
- archetypes.ts # export const archetypes = Database.archetypes(components, { … })
34
- ```
35
-
36
- `core-database.ts` is a thin assembler; each facet file exports one typed object
37
- built by a `Database.*` helper. No per-item files, no barrels. Create only the
38
- facet files the feature needs — a singleton feature with no entities is just
39
- `resources.ts` + `core-database.ts` (no empty `components({})` boilerplate).
40
-
41
- ### Schema scopes
42
-
43
- State divides on two orthogonal axes — **scope** (shared with peers vs. local to
44
- this client) and **lifetime** (durable across reloads vs. ephemeral) — giving
45
- four scopes, each stamping a flag pair:
46
-
47
- | scope | shared? | durable? | flags |
48
- |-------|:--:|:--:|-------|
49
- | **document** | yes | yes | — |
50
- | **settings** | no | yes | `nonShared` |
51
- | **presence** | yes | no | `nonPersistent` |
52
- | **session** | no | no | `nonPersistent` + `nonShared` |
53
-
54
- `Database.components` / `Database.resources` group declarations by scope and
55
- stamp the flags. Every scope key is optional — declare only the ones you use
56
- (most features are document-only):
57
-
58
- ```ts
59
- // components.ts
60
- export const components = Database.components({
61
- document: { todo: True.schema, name: Name.schema, complete: Boolean.schema },
62
- session: { dragPosition: DragPosition.schema }, // omit settings / presence
63
- });
64
-
65
- // resources.ts — entries must carry a `default` (enforced at the call)
66
- export const resources = Database.resources({
67
- settings: { displayCompleted: Boolean.schema },
68
- });
69
- ```
70
-
71
- A component/resource *value* is a plain `data/`-schema re-export (or a small
72
- inline literal); its scope — and therefore its flags — is stated only by which
73
- group it sits in. Hover a scope key for its meaning. The database does not yet
74
- act on these flags (see the `nonShared` schema note); they model
75
- shared/local × durable/ephemeral now, and the durable-shared subset
76
- (`document`) is the set a bare store could be rebuilt from for reconciliation.
77
-
78
- **Shared columns live in `data/`.** A column two features share (e.g. a `name`
79
- on both todos and users) is a `data/` type both reference by identity
80
- (`Name.schema`), so `combinePlugins` dedupes it. Only the `document` scope
81
- preserves schema identity (it applies no flags) — never share a
82
- settings/presence/session column across features by identity.
83
-
84
- ### Archetypes
85
-
86
- `archetypes.ts` declares the feature's entity shapes with
87
- `Database.archetypes(components, { … })`, which validates every key against the
88
- component map and preserves each archetype's literal tuple — no per-archetype
89
- `as const`. Archetypes are a packing convenience, not serialized data; one may
90
- span scopes (a document entity with an ephemeral `dragPosition` slot). An
91
- archetype mirroring a `data/` row type carries a private compile-time
92
- drift-guard (see `archetypes.md`).
93
-
94
- ## Layered composition (behaviour)
95
-
96
- Above the schema, behaviour is a chain of `Database.Plugin`s, each its own
97
- **namespace folder** — `<layer>-database/` holding the eponymous
98
- `<layer>-database.ts` (the plugin, which `extends` the previous layer) plus the
99
- facet folder it adds. **Name each layer for the facet it adds — never for the
100
- feature or app.** A feature creates only the layers it uses:
101
-
102
- ```
103
- main-service/
104
- core-database/ # the schema (above)
105
- index-database/ # extends core; indexes/
106
- transaction-database/ # extends index; transactions/
107
- service-database/ # extends transaction; services/
108
- computed-database/ # extends service; computed/
109
- action-database/ # extends computed; actions/
110
- ```
111
-
112
- Each exports a namespace mirroring the plugin — the composed object is a
113
- `Database` (the `@adobe/data` term) even though the folder is `services/main-service/`:
114
-
115
- ```ts
116
- export type IndexDatabase = Database.Plugin.ToDatabase<typeof indexDatabasePlugin>;
117
- export namespace IndexDatabase {
118
- export const plugin = indexDatabasePlugin;
119
- export type Store = Database.Plugin.ToStore<typeof indexDatabasePlugin>;
120
- }
121
- ```
122
-
123
- ### The assembled database — `MainService`
124
-
125
- *Which* layer is topmost varies with the facets a feature uses (service /
126
- computed / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/`
127
- — need the *whole* feature database but must not name that layer, or adding or
128
- dropping a layer rewrites them all. Alias it once from the **folder-eponymous
129
- `main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in
130
- its same-named file), as a namespace so consumers reach `MainService.plugin` /
131
- `MainService.Store`:
132
-
133
- ```ts
134
- // services/main-service/main-service.ts
135
- export { SystemDatabase as MainService } from "./system-database/system-database.js";
136
- ```
137
-
138
- Every consumer references **`MainService`** (`.plugin`, `.Store`) — never the
139
- topmost layer. Add or drop a layer → change only this one line.
140
-
141
- **Cross-feature naming.** Inside a feature the core and assembled databases are
142
- the bare `CoreDatabase` / `MainService`. When *another* feature or package
143
- imports one — a peer built on it, the base `imports` a peer's `core-database`, a
144
- downstream package — reference it **feature-qualified**: `<Feature>CoreDatabase`
145
- / `<Feature>MainService`, re-exported so from the feature's barrel
146
- (`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so
147
- two features' `CoreDatabase`s never collide.
148
-
149
- ## What each layer takes as input
150
-
151
- - **`plugin`** — every layer `extends` the previous layer's `plugin`; that is
152
- the one universal thread up the chain.
153
- - **`Store`** (via `ToStore`) — used *only* by **transactions** (`t:
154
- <Layer>.Store`). A store carries the index handles and the initiating
155
- `userId`, so a store *is* the transaction context; there is no separate
156
- transaction-context type. Use **`CoreDatabase.Store`** for a transaction that
157
- reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
158
- moment it reads an index.
159
- - **the whole `Database`** — **services**, **computed**, and **actions** each
160
- take `service: <Layer>` (the lowest layer whose database exposes what they
161
- read/call): services read observables and call transactions; computed reads
162
- `service.observe.*` and, once it needs a service's own reactive surface,
163
- `service.services.*`; actions call `service.services.*` then
164
- `service.transactions.*`.
165
-
166
- Each subfolder has its own rule. Modelling a plugin's authored vs. derived
167
- surface is covered by `plugin-modelling.md`.
168
-
169
- ## Facets are flat until they aren't
170
-
171
- A **behavioural** facet folder (`indexes/`, `transactions/`, `computed/`,
172
- `actions/`) holds one file per item plus an `index.ts` barrel — flat while that
173
- reads well, roughly up to half a dozen items. When a cohesive cluster grows past
174
- that, promoting it into a themed subfolder with its own `index.ts` (re-exported
175
- by the facet barrel) is a standard move — only once the folder actually feels
176
- crowded:
177
-
178
- ```
179
- transactions/
180
- order/ # a cohesive cluster: fractional-ordering helpers
181
- index.ts next-order.ts normalize-order.ts select-ordered-todos.ts
182
- create-todo.ts drag-todo.ts …
183
- ```
184
-
185
- The **schema** facets (components / resources / archetypes) are *single files*
186
- in `core-database/`, not folders. A whole feature should stay small enough that
187
- subfoldering rarely bites — grow by adding features, not by bloating one.
@@ -1,28 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/**/core-database/resources.ts'
4
- ---
5
-
6
- # core-database/resources.ts — the feature's resources
7
-
8
- A resource is a singleton component — one instance held on a single entity. One
9
- file, one export: `resources`, built with `Database.resources(...)`. It is
10
- identical to `Database.components` except **every entry must carry a `default`**
11
- — a missing `default` is a compile error here, at the declaration site.
12
-
13
- ```ts
14
- import { Database } from "@adobe/data/ecs";
15
- import { Boolean } from "@adobe/data/schema";
16
-
17
- export const resources = Database.resources({
18
- settings: { displayCompleted: Boolean.schema }, // per-device view toggle
19
- });
20
- ```
21
-
22
- - Give each resource a `default` — spread the data-type's schema and add one, or
23
- bind a `data/` / primitive schema that already carries a sensible default.
24
- - **Scope = which group it sits in** (see `services/main-service/index.md`). A per-device
25
- preference is `settings` (durable, `nonShared`); a synced-but-unsaved value is
26
- `presence`; a saved shared value is `document`.
27
- - Because there is only ever one instance, linear-memory packing buys nothing —
28
- never choose a struct schema for efficiency here.
@@ -1,25 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/service-database/services/**/*.ts'
4
- ---
5
-
6
- # database/services/ — database-bound service factories
7
-
8
- One factory per file: `create<Name>Service(service, …)` that binds a service
9
- implementation to a live database — reading its observables and invoking
10
- its transactions. This is where the async `services/` contracts (or
11
- framework services like `AgenticService`) are wired to ECS state.
12
-
13
- ```ts
14
- export const createAgentService = (service: TransactionDatabase, mark: PlayerMark): AgenticService =>
15
- /* … reads service.observe.*, calls service.transactions.* … */;
16
- ```
17
-
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
-
21
- An `index.ts` barrel re-exports the factories; `service-database.ts`
22
- registers them under the `services` facet, each keyed by the name
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.
@@ -1,59 +0,0 @@
1
- ---
2
- paths:
3
- - '**/features/*/services/main-service/transaction-database/transactions/**/*.ts'
4
- ---
5
-
6
- # database/transactions/ — atomic mutations
7
-
8
- One mutation per file: a function taking the transaction store as its
9
- first argument and pure `data/` args, returning `void`. It reads and
10
- writes `t.resources` / entities; all its writes commit or roll back
11
- together.
12
-
13
- ```ts
14
- import { PlayMoveArgs } from "../../../data/play-move-args/play-move-args.js";
15
- import type { CoreDatabase } from "../../core-database/core-database.js";
16
-
17
- export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
18
- // guard, then mutate t.resources / entities / archetypes
19
- };
20
- ```
21
-
22
- - Type the store parameter as `<Layer>.Store`: **`CoreDatabase.Store`** for a
23
- transaction that reads/writes entities, resources, or archetypes (the common
24
- case — all schema lives in `core-database`); **`IndexDatabase.Store`** the
25
- moment it reads an index. A store *is* the transaction context: it carries the
26
- index handles and the initiating `t.userId`. There is no separate
27
- transaction-context type.
28
- - Keep transactions idempotent under replay where sync/P2P applies —
29
- validate and silently return on illegal or out-of-turn input rather
30
- than throwing.
31
- - Decisions come from pure `data/` helpers; the transaction only applies the
32
- result — read the touched slice, call the `data/` transform, write the diff.
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
- - **ECS-based feature (no `data/state/`):** there are no conformance cases, so
36
- **every transaction carries its own unit test** — `Store.create(plugin)`, run the
37
- transaction, assert the resulting resources/entities/archetypes. That test is the
38
- direct substitute for the conformance oracle (see `../../index.md`, Two modes). The
39
- bullet below applies only to **state-based** features.
40
- - **Conformance is wired once, centrally, and auto-paired** — not per-file. The
41
- feature's single `conformance/conformance.test.ts` `Conformance.checkFeature(spec)`
42
- call conforms transactions: it pulls them off **`plugin.transactions`** (the
43
- registered facet), pairs each to the **same-named** `data/state` transition, and
44
- conforms it — seed `fromState(before)`, apply, `Match.assert` `toState ≡ after`
45
- (state only; service effects are asserted through the action). There is **no**
46
- `define`/`conforms` adapter and **no** `covers` guard. A transaction taking
47
- **entity ids** takes them under the transition's own arg key (`{ id }`); the runner
48
- resolves each id the case's `args` schema marks via the id→entity map `fromState` returns. A
49
- transaction with **no same-named transition** is infrastructure (`setInput`,
50
- `setBounds`) or the drag UI op (`dragTodo`) or system-dispatched — it is simply
51
- **skipped**, no guard needed. Don't add a per-item adapter for a renamed op; add a
52
- thin same-named transaction instead (see `conformance.md`).
53
- - An `index.ts` barrel feeds the `transactions` plugin facet — so it must
54
- re-export **only** the mutations. A read/query helper shared by several
55
- transactions (`readShip`, `readBoard` — a `(t) => value` function) may live
56
- beside them in `transactions/`, but keep it **out of the barrel**, or it gets
57
- registered as a dispatchable transaction. (These are related helpers, so they
58
- belong here, not in a separate folder — `global/cohesion.md`'s peer-level caution
59
- doesn't apply.)