@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.
- package/.claude/rules/README.md +26 -31
- package/.claude/rules/app.md +46 -0
- package/.claude/rules/archetypes.md +5 -3
- package/.claude/rules/data-modelling.md +8 -7
- package/.claude/rules/ecs.md +11 -7
- package/.claude/rules/element.md +6 -5
- package/.claude/rules/features/data/components.md +51 -0
- package/.claude/rules/features/data/entities.md +37 -0
- package/.claude/rules/features/data/index.md +18 -70
- package/.claude/rules/features/data/resources.md +34 -0
- package/.claude/rules/features/data/values.md +36 -0
- package/.claude/rules/features/ecs/actions.md +31 -0
- package/.claude/rules/features/ecs/computed.md +31 -0
- package/.claude/rules/features/ecs/conformance.md +85 -0
- package/.claude/rules/features/ecs/core.md +43 -0
- package/.claude/rules/features/ecs/index.md +72 -0
- package/.claude/rules/features/{services/main-service → ecs}/indexes.md +6 -7
- package/.claude/rules/features/ecs/services.md +36 -0
- package/.claude/rules/features/{services/main-service → ecs}/systems.md +61 -47
- package/.claude/rules/features/ecs/transactions.md +32 -0
- package/.claude/rules/features/index.md +104 -150
- package/.claude/rules/features/services/index.md +52 -84
- package/.claude/rules/features/spec/index.md +161 -0
- package/.claude/rules/features/ui/binding-element.md +5 -5
- package/.claude/rules/features/ui/index.md +8 -3
- package/.claude/rules/features/ui/lazy-wrapper.md +2 -2
- package/.claude/rules/features/ui/presentation-test.md +1 -0
- package/.claude/rules/features/ui/presentation.md +1 -0
- package/.claude/rules/global/function-references.md +2 -2
- package/.claude/rules/global/namespace.md +3 -3
- package/.claude/rules/global/react.md +4 -4
- package/.claude/rules/lit.md +2 -2
- package/.claude/rules/plugin-modelling.md +2 -0
- package/.claude/rules/service.md +2 -0
- package/.claude/rules/versioning.md +8 -6
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -0
- package/bin/cli.mjs +24 -8
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +7 -8
- package/skills/build-app-entry/SKILL.md +12 -8
- package/skills/build-computed/SKILL.md +8 -7
- package/skills/build-core-database/SKILL.md +18 -15
- package/skills/build-data/SKILL.md +14 -13
- package/skills/build-feature/SKILL.md +2 -1
- package/skills/build-game/SKILL.md +6 -4
- package/skills/build-indexes/SKILL.md +6 -7
- package/skills/build-service-database/SKILL.md +8 -8
- package/skills/build-services/SKILL.md +9 -7
- package/skills/build-spec/SKILL.md +23 -0
- package/skills/build-systems/SKILL.md +12 -18
- package/skills/build-transactions/SKILL.md +8 -8
- package/skills/build-ui/SKILL.md +5 -4
- package/skills/review/SKILL.md +3 -3
- package/.claude/rules/features/data/state.md +0 -209
- package/.claude/rules/features/services/main-service/actions.md +0 -70
- package/.claude/rules/features/services/main-service/archetypes.md +0 -49
- package/.claude/rules/features/services/main-service/components.md +0 -41
- package/.claude/rules/features/services/main-service/computed.md +0 -82
- package/.claude/rules/features/services/main-service/conformance.md +0 -95
- package/.claude/rules/features/services/main-service/core-database.md +0 -48
- package/.claude/rules/features/services/main-service/index.md +0 -187
- package/.claude/rules/features/services/main-service/resources.md +0 -28
- package/.claude/rules/features/services/main-service/services.md +0 -25
- package/.claude/rules/features/services/main-service/transactions.md +0 -59
|
@@ -1,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.)
|