Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.9.89",
"version": "0.9.90",
"private": true,
"engines": {
"node": ">=24"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adobe-data-ai",
"version": "0.9.89",
"version": "0.9.90",
"description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
"author": {
"name": "Adobe"
Expand Down
21 changes: 11 additions & 10 deletions packages/data-ai/.claude/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ child gets its own file (recursing wherever the source tree does):

```
features/
index.md # the feature layering as a whole (ui → ecs → services → data)
index.md # the feature layering as a whole (ui → services → data)
data/
index.md # data-type namespaces (foundation)
state.md # the State aggregate: pure transforms & derivations
services/index.md # service interfaces + service types
ecs/
index.md # the ECS layer + layered database plugins
components.md resources.md archetypes.md
computed.md indexes.md transactions.md services.md actions.md
systems.md conformance.md
services/
index.md # the services layer: main-service + capability contracts
main-service/ # the feature service (ECS implementation)
index.md # the service + its layered database plugins
components.md resources.md archetypes.md
computed.md indexes.md transactions.md services.md actions.md
systems.md conformance.md
ui/index.md # UI (points to element / presentation / lazy-element file rules)
```

Expand All @@ -37,6 +38,6 @@ symlinks them into its own `.claude/rules/` (as it does `features/`).
Each rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
rules apply only once an application opts into the feature-folder pattern
(source organized under `features/<name>/`). Editing
`features/<name>/ecs/<layer>-database/components/foo.ts` pulls in both
`ecs/index.md` (the layer overview) and `ecs/components.md` (the specifics) —
at creation and at every later edit.
`features/<name>/services/main-service/core-database/components.ts` pulls in both
`main-service/index.md` (the layer overview) and `main-service/components.md`
(the specifics) — at creation and at every later edit.
2 changes: 1 addition & 1 deletion packages/data-ai/.claude/rules/element.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Lit-only: `static styles = styles` from a sibling `*.css.ts`. `@property`
exists *only* for locator inputs — never for state, never for derived
values, never for parent-forwarded flags. The `P` in `DatabaseElement<P>` (and
the `get plugin()` it returns) is the surface the element *consumes* — never the
varying topmost-layer name (see `features/ecs/index.md`): `typeof
varying topmost-layer name (see `features/services/main-service/index.md`): `typeof
FeatureDatabase.plugin` for a self-contained feature. **Exception:** an element
meant to be *extended* — injected with various databases that build on this
feature (a peer app that adds an agent, a p2p/presence build) — types on the
Expand Down
4 changes: 2 additions & 2 deletions packages/data-ai/.claude/rules/features/data/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export * as State from "./public.js";
- Each transform has a sibling `*.test.ts`; performance is irrelevant here,
correctness is everything. Keep its cases in a sibling `<transform>.cases.ts`
typed `ConformanceCase<Args>[]` (the shared `conformance-case.ts` type) —
spec-owned truth the matching ecs conformance test imports unchanged (see
`ecs/conformance.md`). Author `before`/`after` as full `State`
spec-owned truth the matching main-service conformance test imports unchanged (see
`services/main-service/conformance.md`). Author `before`/`after` as full `State`
(`{ ...State.create(), …overrides }`); the generic-slice signature lets them
flow through. Tolerant full-`State` equality is the shared
`expect-state-matches.ts`, alongside the cases here.
Expand Down
94 changes: 55 additions & 39 deletions packages/data-ai/.claude/rules/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
paths:
- '**/features/*/data/**/*.ts'
- '**/features/*/services/**/*.ts'
- '**/features/*/ecs/**/*.ts'
- '**/features/*/ui/**/*.ts'
---

Expand All @@ -14,34 +13,39 @@ trust, and an **efficient implementation** proven equivalent to it.
- **`data/` is the specification.** The whole feature modelled as one immutable
`State` with pure transformations over it — correct by construction, fully
unit-tested, performance irrelevant. This is the source of truth.
- **`ecs/` is the implementation.** The same model physically arranged for
mutation efficiency (a reactive Entity-Component-System). Its reads and writes
wrap the `data/` functions where practical; where they must be hand-optimized,
unit tests verify they still agree with the `data/` precedent.
- **`services/main-service/` is the implementation.** The same model physically
arranged for mutation efficiency — a reactive Entity-Component-System, an
implementation detail behind the service. Its reads and writes wrap the
`data/` functions where practical; where they must be hand-optimized, unit
tests verify they still agree with the `data/` precedent.

Because the optimized `ecs/` layer is largely mechanical given the `data/` spec,
it can be generated and kept honest by AI rules, with the conformance tests as
the safety net. Net result: write a slow-but-verifiable app, then derive a fast
one that provably behaves the same.
Because the optimized `main-service` is largely mechanical given the `data/`
spec, it can be generated and kept honest by AI rules, with the conformance
tests as the safety net. Net result: write a slow-but-verifiable app, then
derive a fast one that provably behaves the same.

## The four layers
## The layers

`ui → ecs → services → data` — higher imports lower, never the reverse. `data/`
`ui → services → data` — higher imports lower, never the reverse. `data/`
depends on nothing but `@adobe/data` and other `data/` declarations. A feature
creates only the layers it uses.

| Layer | Role |
|-------|------|
| `data/` | The spec: `State`, pure transforms & derivations, entity sub-types. Pure, tested. |
| `services/` | Async capability contracts (ports to the outside world). Optional. |
| `ecs/` | The implementation: the ECS materialisation + reactive reads/writes over it. |
| `services/main-service/` | The implementation: the ECS materialisation + reactive reads/writes. The **sole entrypoint** — the only service the `ui/` binds to. |
| `services/<name>-service/` | Async capability contracts (ports to the outside world). Reached only *through* `main-service` (its actions/services wire them in), never by the UI. Optional. |
| `ui/` | Presentation. |

`services/` holds two kinds of service: the one `main-service/` (ECS-backed
state, its own subtree of rules) and any async capability contracts. The UI, and
every external consumer, sees only `main-service`.

## One app, many features

An application is a set of **features**, each its own `features/<name>/` folder
with the same four layers. One base feature (`features/main/`) is the host; the
rest are peers that load lazily.
with the same layers. One base feature (`features/main/`) is the host; the rest
are peers that load lazily.

**Keep each feature small; grow by adding features, not by bloating one.** A
feature is meant to fit in the head — a handful of files per layer. When a
Expand All @@ -53,16 +57,17 @@ its own peer feature rather than let one feature's folders balloon.
base must not depend on its children — with one sanctioned exception below.
- **The base `imports` every peer's *schema* plugin** —
`Database.Plugin.create({ imports })`, not `extends`. `imports` merges the
peer's `core-database` (components / resources / archetypes) into the shared
store at runtime **without** pulling its types or behavior into the base's type
or bundle (`extends` would do both, and cost quadratically). So one store knows
every feature's schema — data coexists, persists, and syncs — while the base
stays decoupled. Import the peer's `core-database.ts` plugin (schema only) —
under its feature-qualified name `<Peer>CoreDatabase` (see the cross-feature
naming rule in `ecs/index.md`): its indexes, transactions, computed, services,
and UI stay out until the feature loads. A column two features share (e.g.
`name`) lives in `data/` and is referenced by identity, so `combinePlugins`
dedupes it.
peer's `services/main-service/core-database` (components / resources /
archetypes) into the shared store at runtime **without** pulling its types or
behavior into the base's type or bundle (`extends` would do both, and cost
quadratically). So one store knows every feature's schema — data coexists,
persists, and syncs — while the base stays decoupled. Import the peer's
`services/main-service/core-database.ts` plugin (schema only) — under its
feature-qualified name `<Peer>CoreDatabase` (see the cross-feature naming rule
in `services/main-service/index.md`): its indexes, transactions, computed,
services, and UI stay out until the feature loads. A column two features share
(e.g. `name`) lives in `data/` and is referenced by identity, so
`combinePlugins` dedupes it.
- **Peers load lazily by being used.** `DatabaseElement`, on connect, walks up to
the nearest ancestor database and `extend`s it with its own plugin — so the
first time a feature element renders, its full plugin (indexes, transactions,
Expand All @@ -71,25 +76,36 @@ its own peer feature rather than let one feature's folders balloon.
load is genuinely on-demand.
- **The base reaches a child only through a lazy element wrapper** — a tiny
`Foo()` that `void import()`s the child element. That dynamic import is the one
allowed core→child seam; the heavy element and its service database stay in the
allowed core→child seam; the heavy element and its main-service stay in the
child's own chunk.

## Spec and implementation, kept honest

The tie between `data/` (spec) and `ecs/` (implementation) is **conformance**,
one property — `toState(apply(fromState(before), args)) ≡ transform(before, args)`:
each ecs mutation, seeded and read back through a test-only `ecs ↔ State`
The tie between `data/` (spec) and `main-service` (implementation) is
**conformance**, one property —
`toState(apply(fromState(before), args)) ≡ transform(before, args)`: each
main-service mutation, seeded and read back through a test-only store↔`State`
projection, equals the pure `data/` transform it stands for. The projection and
its runner live in `ecs/conformance/` (see `ecs/conformance.md`); the shared
`{ before, args, after }` cases are spec-owned (`data/state/<transform>.cases.ts`),
so conforming the ecs path is "substitute the implementation, reuse the
expectations." This lets the ecs layer be largely mechanical and agent-generated,
with the spec as oracle. *How* to author each layer lives in the per-folder rules
below.
its runner live in `services/main-service/conformance/` (see
`services/main-service/conformance.md`); the shared `{ before, args, after }`
cases are spec-owned (`data/state/<transform>.cases.ts`), so conforming the
implementation is "substitute the implementation, reuse the expectations." This
lets `main-service` be largely mechanical and agent-generated, with the spec as
oracle. *How* to author each layer lives in the per-folder rules below.

## Reference implementations

Working samples ship inside `@adobe/data` at
`node_modules/@adobe/data/references/<sample>/src/` — read them for concrete,
current examples of this structure. `data-lit-todo` is the most complete
(multi-feature, capability services, indexes, conformance tests);
`data-lit-tictactoe` is the minimal turn-based reference.

## Per-layer detail

See the rules under each folder: `data/`, `services/`, `ecs/` (components,
resources, archetypes, computed, indexes, transactions), `ui/`. Cross-cutting
patterns live at the rules root — `namespace.md`, `data-modelling.md`,
`archetypes.md` (row iteration), `type-casts.md`, `cohesion.md`.
See the rules under each folder: `data/`, `services/` — the `main-service/`
subtree (components, resources, archetypes, computed, indexes, transactions,
systems, conformance) and capability-contract services — and `ui/`.
Cross-cutting patterns live at the rules root — `namespace.md`,
`data-modelling.md`, `archetypes.md` (row iteration), `type-casts.md`,
`cohesion.md`.
49 changes: 31 additions & 18 deletions packages/data-ai/.claude/rules/features/services/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,45 @@ paths:
- '**/features/*/services/**/*.ts'
---

# services/ — async capability contracts
# services/ — the feature's services

Async ports: processing, persistence, observation, generation. A service
is the boundary between pure feature code and the outside world, which is
why its members are async — enabling cross-process portability and lazy
loading (`AsyncDataService.createLazy`).
Everything a feature exposes as a service lives here. Two kinds:

This layer holds **service interfaces**, one per `<name>-service/` folder;
the export and folder both carry the `-service` suffix. Each is a
namespace folder (`namespace.md`).
- **`main-service/`** — the one entrypoint. The ECS-backed reactive state
service (materialisation + reads/writes over it); the only service the `ui/`
and external consumers bind to. It has its own subtree of rules
(`main-service/index.md` and the layer files under it). Every feature with
state has exactly one.
- **`<name>-service/`** — async **capability contracts**: ports to the outside
world (processing, persistence, observation, generation). Reached only
*through* `main-service` (its service/action layers wire them in), never by the
UI. A feature has zero or more.

- The contract is an `interface` — the only place `interface` is used in
the codebase — validated immediately with
The rest of this rule governs the capability contracts; `main-service` follows
its own subtree.

## Capability contracts

Each is a namespace folder (`namespace.md`); the export and folder both carry the
`-service` suffix. A service is the boundary between pure feature code and the
outside world, which is why its members are async — enabling cross-process
portability and lazy loading (`AsyncDataService.createLazy`).

- The contract is an `interface` — the only place `interface` is used in the
codebase — validated immediately with
`Assert<AsyncDataService.IsValid<typeof MyService>>`.
- Members are async only: `void | Promise<T> | AsyncGenerator<T> | Observe<T>`.
- Provide `create*` factories.

## Where the I/O types live

Most input/output types belong to a single service — declare them **on
that service's namespace** (`MyService.SomeInput`) and expose them only
when something external actually references them; not everything does.
Most input/output types belong to a single service — declare them **on that
service's namespace** (`MyService.SomeInput`) and expose them only when something
external actually references them; not everything does.

A **service type** may be non-serializable (callbacks, function
signatures) — that's what distinguishes it from a `data/` type. If an I/O
value is a plain serializable value, prefer a `data/` type instead.
A **service type** may be non-serializable (callbacks, function signatures) —
that's what distinguishes it from a `data/` type. If an I/O value is a plain
serializable value, prefer a `data/` type instead.

A non-service type sits *directly* in `services/` **only** when it is
genuinely shared across more than one service — rare, but it happens.
A non-service type sits *directly* in `services/` **only** when it is genuinely
shared across more than one service — rare, but it happens.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
paths:
- '**/features/*/ecs/**/actions/**/*.ts'
- '**/features/*/services/main-service/**/actions/**/*.ts'
---

# ecs/actions/ — async orchestration
# services/main-service/actions/ — async orchestration

One action per file: a function taking the **whole database** as its first
argument and pure `data/` args. Actions orchestrate anything *outside* a
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
paths:
- '**/features/*/ecs/**/core-database/archetypes.ts'
- '**/features/*/services/main-service/**/core-database/archetypes.ts'
---

# core-database/archetypes.ts — the feature's archetypes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
paths:
- '**/features/*/ecs/**/core-database/components.ts'
- '**/features/*/services/main-service/**/core-database/components.ts'
---

# core-database/components.ts — the feature's components
Expand Down Expand Up @@ -28,7 +28,7 @@ export const components = Database.components({
- A component name may differ from the type it stores (`position` of a `Vec3`,
`mark` of a `PlayerMark`) — the field name describes the role, the schema the
shape.
- **Scope = which group it sits in** (see `ecs/index.md` for the four scopes and
- **Scope = which group it sits in** (see `services/main-service/index.md` for the four scopes and
their flags). `document` is the common case; hover a scope key for its
meaning. Never write `nonShared` / `nonPersistent` by hand — the group applies
them.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
paths:
- '**/features/*/ecs/**/computed/**/*.ts'
- '**/features/*/services/main-service/**/computed/**/*.ts'
---

# database/computed/ — derived observable values
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
paths:
- '**/features/*/ecs/**/conformance/**/*.ts'
- '**/features/*/services/main-service/**/conformance/**/*.ts'
---

# ecs/conformance/ — spec↔implementation projection (test-only)
# services/main-service/conformance/ — spec↔implementation projection (test-only)

The bridge that keeps the ecs implementation honest against the `data/` spec.
**Test-only**: imported solely by `*.test.ts` and in no facet barrel, so it never
Expand All @@ -19,7 +19,7 @@ The property, per `{ before, args, after }` case:

Conformance test-support splits by concern along the layer boundary:

- **Store-side, here in `ecs/conformance/`:**
- **Store-side, here in `services/main-service/conformance/`:**
- `from-state.ts` → `fromState(store: CoreDatabase.Store, state: State): void`
seeds the store (clear tail→head → insert entities → set resources) to a `State`.
- `to-state.ts` → `toState(store): State` reads the store back — each kind via
Expand Down
Loading