Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1994443
feat(conformance): split manifest into Conformance.spec + Conformance…
krisnye Sep 30, 2026
17a0684
refactor(data-lit-todo): port to the layered feature layout
krisnye Sep 30, 2026
8199935
fix(data-lit-todo): reloaded todos vanished — don't pack session drag…
krisnye Sep 30, 2026
c33a5a3
refactor(data-solid-dashboard): port to the layered feature layout
krisnye Sep 30, 2026
a8afc63
refactor(data-react-pixie): port to the layered feature layout
krisnye Sep 30, 2026
b7cbd50
refactor(data-lit-tictactoe): port to the layered feature layout
krisnye Sep 30, 2026
b74243d
refactor(data-p2p-tictactoe): port to the layered feature layout
krisnye Sep 30, 2026
c06c3cf
feat(conformance): frame option conforms a per-frame spec op by drivi…
krisnye Sep 30, 2026
65f69e1
refactor(data-gpu-hopper): port to the layered feature layout
krisnye Sep 30, 2026
e976c06
refactor(data-react-pixie): declare the init-only system inline
krisnye Sep 30, 2026
0d614d3
refactor(data-lit-tictactoe): values helper takes mark fields structu…
krisnye Sep 30, 2026
44810ee
refactor(data-lit-space-rock-game): port to the layered feature layout
krisnye Sep 30, 2026
f4514d1
refactor(conformance): drop transactionOps — frame covers system-driv…
krisnye Sep 30, 2026
4d68c00
fix(data-lit-todo): id-addressed todo ops ignore other features' enti…
krisnye Sep 30, 2026
39e23fc
docs(data-ai): rewrite feature guidance for the layered layout
krisnye Sep 30, 2026
9534615
docs(data-ai): reconcile guidance with the ported samples
krisnye Sep 30, 2026
2230a17
refactor: services sit below computed in the ecs layer chain
krisnye Sep 30, 2026
57e8a5d
feat(conformance): per-system conformance, frames composed from sched…
krisnye Sep 30, 2026
58ed9a4
feat: per-system conformance in hopper; fresh doubles per frame run; …
krisnye Sep 30, 2026
d25139b
feat(data-lit-space-rock-game): per-system conformance
krisnye Sep 30, 2026
ae29ac0
feat(conformance): strict doubles, frame cases on the implementation,…
krisnye Sep 30, 2026
5e7427b
refactor(data-gpu-hopper): frame cases on the implementation, one noO…
krisnye Sep 30, 2026
de20357
refactor(data-lit-space-rock-game): frame cases on the implementation…
krisnye Sep 30, 2026
dc3e956
chore: bump versions to 0.10.22
krisnye Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .claude/rules/app.md
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.10.21",
"version": "0.10.22",
"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.10.21",
"version": "0.10.22",
"description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
"author": {
"name": "Adobe"
Expand Down
57 changes: 26 additions & 31 deletions packages/data-ai/.claude/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,38 @@ frontmatter glob, injects them only when a matching file is in context.

## Layout mirrors the feature-folder structure

The `features/` subtree mirrors the four feature layers one-to-one, so the
guidance for a folder lives at the same path shape as the code it governs. A
folder's own `index.md` holds just enough to understand that folder; each
child gets its own file (recursing wherever the source tree does):
The `features/` subtree mirrors a feature's layers one-to-one, so the guidance for a
folder lives at the same path shape as the code it governs. A folder's `index.md`
holds just enough to understand that folder; each child gets its own file:

```
global/ # always-on conventions — deliberately repo-wide globs
namespace.md cohesion.md type-casts.md function-references.md react.md
features/
index.md # the feature layering as a whole (ui → services → data)
index.md # the layering as a whole: data → services → spec | ecs → ui → app
data/
index.md # data-type namespaces (foundation)
state.md # the State aggregate: pure transforms & derivations
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)
index.md # pure Data declarations
values.md components.md resources.md entities.md
services/index.md # service namespaces: interface, create, createFake
spec/index.md # the State spec: actions, derivations, cases (test tier)
ecs/
index.md # the layered database plugins + MainService
core.md indexes.md transactions.md computed.md services.md actions.md
systems.md conformance.md
ui/ # element / presentation / lazy-wrapper rules
app.md # src/app/: the composition root
```

The `global/` rules (`namespace.md`, `cohesion.md`, `type-casts.md`,
`function-references.md`, `react.md`) are **intentionally repo-wide** — their
`paths:` globs (`**/*.ts` etc.) apply everywhere, by design, so the folder name
makes that explicit. Alongside `features/`, the remaining rules-root `.md` files
hold the other **cross-cutting** patterns the feature rules reference —
`data-modelling.md`, `archetypes.md` (row iteration), `plugin-modelling.md`,
`versioning.md` (schema history & upgrade-on-load), and
the UI file rules (`element.md`, `lazy-element.md`, `presentation.md`). They live
here so the bundle is self-contained; this repo symlinks them into its own
`.claude/rules/` (as it does `features/`).
The `global/` rules are **intentionally repo-wide** — their `paths:` globs (`**/*.ts`
etc.) apply everywhere, by design. Alongside `features/`, the remaining rules-root
`.md` files hold **cross-cutting** patterns the feature rules reference —
`app.md`, `data-modelling.md`, `archetypes.md` (the query and iteration model),
`plugin-modelling.md`, `versioning.md`, and the UI rules `element.md` and
`lazy-element.md` (`features/ui/` holds `presentation.md` and the binding-element and
lazy-wrapper rules). They live here so the bundle is
self-contained; this repo symlinks them into its own `.claude/rules/`.

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>/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.
Each feature rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
rules apply once an application organizes its source under `features/<name>/`.
Editing `features/<name>/data/components/name.ts` pulls in both `data/index.md` and
`data/components.md` — at creation and at every later edit.
46 changes: 46 additions & 0 deletions packages/data-ai/.claude/rules/app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
paths:
- '**/src/app/**/*.ts'
- '**/src/app/**/*.tsx'
---

# src/app/ — the composition root

The app root composes the features. It is the only place that knows every feature,
and no feature imports it.

```
src/app/
schema.ts the app's composed plugin (when there are several features)
versioning/ versions.ts + versions.test.ts (versioning.md)
persistence.test.ts a save/load round-trip, when the app persists
*.test.ts cross-feature tests (e.g. a todo op ignores a user id)
main.ts the entry: create the database, inject services, render
```

```ts
// schema.ts — main's full behavior, plus the schema of features that load lazily
const appSchemaPlugin = Database.Plugin.create({
imports: AssignCoreDatabase.plugin,
extends: MainService.plugin,
});
export namespace AppSchema {
export const plugin = appSchemaPlugin;
}

// main.ts
const service = Database.create(AppSchema.plugin, {
versioning: createVersionUpgrader(versions),
services: { game }, // a service whose `create` throws until the app injects it
});
```

- **`imports`, not `extends`, for a lazily loaded feature**: the store knows its
columns up front (its data persists and loads), while its behavior, types and code
stay out until one of its elements connects and extends the database.
- **Inject every service whose `create` throws** at `Database.create`. Services can't
be injected by a later `extend`.
- **Versioning** folds the whole persisted schema, so it lives here, built from
`AppSchema.plugin`.
- A single-feature app needs no `schema.ts`; `main.ts` uses the feature's
`MainService.plugin`.
8 changes: 5 additions & 3 deletions packages/data-ai/.claude/rules/archetypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ paths:
- '**/*-plugin/**/*.ts'
- '**/*-database.ts'
- '**/*-database/**/*.ts'
- '**/features/*/data/entities/**/*.ts'
- '**/features/*/ecs/core/**/*.ts'
---

# Archetypes — the ECS query & iteration model

General to all `@adobe/data` ECS use, feature-organized or not. The
`core-database/archetypes.ts` *file* conventions live in
`features/services/main-service/archetypes.md`.
General to all `@adobe/data` ECS use, feature-organized or not. The feature file
conventions live in `features/data/entities.md` (entity tuples) and
`features/ecs/core.md` (archetype packing).

An archetype is a named set of component keys — a kind of entity — and also a
**query**: `queryArchetypes(include, { exclude })` (and the `select` / `count`
Expand Down
15 changes: 8 additions & 7 deletions packages/data-ai/.claude/rules/data-modelling.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
paths:
- '**/data/*.ts'
- '**/features/*/data/**/*.ts'
- '**/features/*/spec/**/*.ts'
---

# Data modelling — locality of knowledge
Expand Down Expand Up @@ -54,15 +55,15 @@ single source of truth, not a downstream comparison flag:
may be a plain `number` or the ECS `Entity` type (a branded `number`) — reusing
`Entity`, and reference bundles like `CoeditingRelations.OrderedChild`, is allowed:
they are plain value types, not ECS machinery, and keep the spec aligned with the ECS
(see `features/data/state.md`). Also used for deterministic-key lookups (an enum, a
name, a stable string).
(see `features/spec/index.md`). In a `State`, every `ReadonlyMap` field is an entity
collection; a lookup keyed by an enum or name belongs inside a value type.

**Entities are keyed, never id-bearing values.** An entity's identity is its map
key, so entity value types (`Todo`, `Bullet`) have no `id` field, and there is no
`ReadonlySet<T>`/`ReadonlyArray<T>` *of entity values* — the single
`ReadonlyMap<number, …>` store is the only home for entities, and queries return
their **ids** (`ReadonlySet<number>` unordered, `ReadonlyArray<number>` ordered).
See `features/data/state.md` for the full `State` shape.
`ReadonlySet<T>`/`ReadonlyArray<T>` *of entity values* in `State` — a
`ReadonlyMap<number, …>` per entity kind is the only home for entities. A derivation
may return entity values for display (the ECS computed then emits ids, hydrated by
conformance). See `features/spec/index.md` for the full `State` shape.

These are first-class `Data` (see `features/data/index.md`) — serialize a
Set/Map-bearing value with `Data.stringify` / `Data.parse` (plain `JSON.stringify`
Expand Down
18 changes: 11 additions & 7 deletions packages/data-ai/.claude/rules/ecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ paths:

# Database.Plugin authoring

> Inside a feature (`features/<name>/ecs/`), the `features/ecs/` rules govern file layout and naming: layer files are `<layer>-database.ts` exporting an `XDatabase` namespace, and systems are camelCase. This file covers general plugin mechanics.

Plugins are created with `Database.Plugin.create()` from `@adobe/data/ecs`.

## Property order (enforced at runtime)
Expand All @@ -19,7 +21,7 @@ Properties **must** appear in this exact order. All are optional.
| 2 | `extends` | `Plugin` | Base plugin to extend (types re-exported) |
| 3 | `services` | `(db) => ServiceInstance` | Singleton service factories |
| 4 | `components` | schema object | ECS component schemas |
| 5 | `resources` | `{ default: value as Type }` | Global resource schemas |
| 5 | `resources` | schema with a `default` | Global resource schemas |
| 6 | `archetypes` | `['comp1', 'comp2']` | Standard ECS archetypes |
| 7 | `indexes` | `{ key, order?, unique?, archetype? }` | Sorted/filtered entity indexes |
| 8 | `computed` | `(db) => Observe<T>` | Computed observables |
Expand Down Expand Up @@ -99,7 +101,7 @@ const particleDataPlugin = Database.Plugin.create({
```

- **Components**: per-entity data. Use schema imports (`Vec3`, `Vec4`, `F32` from `@adobe/data/math`) or type namespaces for custom shapes.
- **Resources**: global state. Use **only** `{ default: value as Type }`.
- **Resources**: global state. Every resource carries a `default` (`{ ...Boolean.schema }`, or `{ default: 'dark' as ThemeColor }` to widen a literal).
- **Archetypes**: one per entity kind. List all components that kind requires.

---
Expand All @@ -108,23 +110,25 @@ const particleDataPlugin = Database.Plugin.create({

### components

Non-persistable values (e.g. HTML elements, DOM refs) must use `ephemeral: true` on the schema.
Non-persistable values (e.g. HTML elements, DOM refs) are session scope — spread
`Scope.session` (`nonPersistent` + `nonShared`) into the schema.

```ts
components: {
layout: Layout.schema,
layoutElement: { default: null as unknown as HTMLElement, ephemeral: true },
layoutElement: { default: null as HTMLElement | null, ...Scope.session },
},
```

### resources

Use `as Type` to provide the compile-time type. Use `null as unknown as Type` for resources initialized later in a system initializer.
Use `as Type` to widen a literal default to its type. A resource initialized later
(in a system initializer) defaults to `null` widened to `Type | null`.

```ts
resources: {
themeColor: { default: 'dark' as ThemeColor },
connection: { default: null as unknown as WebSocket },
connection: { default: null as WebSocket | null, ...Scope.session },
},
```

Expand Down Expand Up @@ -156,7 +160,7 @@ UI components that call actions must never consume returned values — see `feat

---

## Naming conventions
## Naming conventions (standalone plugins)

| Item | Convention | Example |
| ----------- | ----------------------------------------------------- | ---------------------------- |
Expand Down
11 changes: 6 additions & 5 deletions packages/data-ai/.claude/rules/element.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ paths:
- '**/*-element.ts'
- '**/elements/**/*.ts'
- '**/ui/**/*.ts'
- '**/ui/**/*.tsx'
---

# Container element authoring
Expand Down Expand Up @@ -84,9 +85,9 @@ function **by reference**, don't wrap it in an identity arrow:

```ts
// ❌ identity wrapper — adds nothing
setOperation: (op) => this.service.transactions.setOperation(op),
setOperation: (op) => this.service.actions.setOperation(op),
// ✅ same signature, pass it through
setOperation: this.service.transactions.setOperation,
setOperation: this.service.actions.setOperation,
```

Only write an arrow when you genuinely adapt the call (drop an event
Expand Down Expand Up @@ -117,14 +118,14 @@ transaction, or hook.
| Framework | Container is | Subscription hook | Locator inputs | Render delegation |
| --------- | ------------------------ | --------------------- | -------------------- | ------------------------------------------------ |
| Lit | `DatabaseElement<P>` | `useObservableValues` | `@property` only | `presentation.render({ ...values, ...callbacks })` |
| React | function component | `useObservableValues` | function arguments | `<Presentation {...values} {...callbacks} />` |
| Solid | component function | framework-specific | function arguments | `<Presentation {...values} {...callbacks} />` |
| React | function component | `useObservableValues` | function arguments | `presentation.render({ ...values, ...callbacks })` |
| Solid | component function | framework-specific | function arguments | `presentation.render({ ...values, ...callbacks })` |

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/services/main-service/index.md`): `typeof
varying topmost-layer name (see `features/ecs/index.md`): `typeof
MainService.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
51 changes: 51 additions & 0 deletions packages/data-ai/.claude/rules/features/data/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
paths:
- '**/features/*/data/components/**/*.ts'
---

# data/components/ — one component schema per file

A component is a named column of entity state. Each lives in its own file, with a
single export: its schema. The file owns that schema object, and every other feature
reuses it by importing the object, since the ECS compares schemas by identity.

```ts
// components/name.ts — document scope: a value type's schema, re-exported by identity
export const name = Name.schema;

// components/drag-position.ts — session scope: flags spread in the file
export const dragPosition = { ...DragPosition.schema, ...Scope.session } satisfies Schema;

// components/todo.ts — a tag
export const todo = True.schema;

// components/index.ts — the feature's vocabulary
export const components = { todo, name, complete, order, dragPosition };
```

- **Scope** is declared per file with `Scope` from `@adobe/data/ecs`:

| scope | shared | durable | spread |
|---|:-:|:-:|---|
| document | yes | yes | nothing (the default) |
| settings | no | yes | `...Scope.settings` |
| presence | yes | no | `...Scope.presence` |
| session | no | no | `...Scope.session` |

On a component, the flags scope the **column**. An entity's own scope comes from
its marker components (`entities.md`).
- **Every column lives here**, including implementation-only ones (a tag, a drag
offset). `ecs/core/` only chooses how they are packed.
- **A tag** is a component with `True.schema`. Add one only when an entity's
component set would otherwise be contained in another's (`entities.md`).
- **The name describes the role**, the schema the shape (`position` of a `Vec3`).
- **Reserved names:** `id`, `nonPersistent`, `nonShared` (built into the ECS), and
`index` (it would collide with the `index.ts` barrel).
- **A feature built on another** spreads the lower barrel first and pins that its own
names don't shadow it (a spread would overwrite silently):
```ts
export const components = { ...mainComponents, user, assignees };
type _NoShadow = Assert<Equal<Extract<"user" | "assignees", keyof typeof mainComponents>, never>>;
```
- The barrel never includes the built-in `nonPersistent` / `nonShared` markers; the
ECS reserves those names and throws.
37 changes: 37 additions & 0 deletions packages/data-ai/.claude/rules/features/data/entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
paths:
- '**/features/*/data/entities/**/*.ts'
---

# data/entities/ — entities as component tuples

An entity is declared by the components it has. Its value type is the aggregate of
those components' types, derived rather than written:

```ts
// entities/todo.ts
export const Todo = ["name", "complete", "order"] as const satisfies Database.EntityComponents<typeof components>;
export type Todo = Database.EntityType<typeof components, typeof Todo>;
// → { readonly name: string; readonly complete: boolean; readonly order: number }

// entities/index.ts
export const entities = { Todo };
```

The same-named `const` and `type` pair counts as one export.

- **Declare the spec-visible entity.** Implementation-only columns (a tag, a drag
offset) are added when `ecs/core/` packs the archetype.
- **An entity's kind is its component set.** Queries match every entity that has *at
least* the listed components. So when one entity's set is contained in another's
(`User = [name]` inside `Todo = [name, complete, order]`), give one of them a tag
(`User = ["user", "name"]`) or query with `exclude`.
- **Entity scope** comes from the built-in marker components in the tuple:
`["cursor", "position", "nonPersistent", "nonShared"]` is a session entity. Markers
are omitted from the value type; tags are kept.
- **A feature built on another** may declare an entity over the lower feature's
components, e.g. `AssignedTodo = ["name", "complete", "order", "assignees"]`.
- **The entity takes the name.** When a value type has the same shape, don't declare
both: helpers on its fields go on the field types' `values/` namespaces.
- The archetype model (supersets, base archetypes, row iteration) is in the
rules-root `archetypes.md`.
Loading
Loading