From dfa1928602472edd2ebed2a4b982def6746c91ec Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 13:04:19 -0700 Subject: [PATCH 1/6] feat(data-ai): type-first data authoring with pinned schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invert the type<->schema relationship. Types are hand-authored as the readable source of truth (rich types like F32/Vec3 for IntelliSense and semantic meaning); schemas become optional and are written to match the type — often later, by the agent, e.g. when implementing the ECS — pinned to it with `type _Pin = Assert, T>>` so they cannot drift. `Schema.ToType` is used only in that assertion, never as the exported type. Updates features/data/index.md, global/namespace.md, data-modelling.md, and the build-data skill. Bumps @adobe/data-ai to 0.9.92. Co-Authored-By: Claude Opus 4.8 --- packages/data-ai/.claude-plugin/plugin.json | 2 +- .../data-ai/.claude/rules/data-modelling.md | 7 ++- .../.claude/rules/features/data/index.md | 57 ++++++++++++++++--- .../data-ai/.claude/rules/global/namespace.md | 28 +++++---- packages/data-ai/package.json | 2 +- packages/data-ai/skills/build-data/SKILL.md | 4 +- 6 files changed, 74 insertions(+), 26 deletions(-) diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 44738cb8..b20f1ba3 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.9.91", + "version": "0.9.92", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/.claude/rules/data-modelling.md b/packages/data-ai/.claude/rules/data-modelling.md index 55bcc939..ffa97b92 100644 --- a/packages/data-ai/.claude/rules/data-modelling.md +++ b/packages/data-ai/.claude/rules/data-modelling.md @@ -15,9 +15,10 @@ discriminated unions. ## Example ```ts -// types/http-method/schema.ts -export const schema = { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] } - as const satisfies Schema; +// types/http-method/http-method.ts — the hand-authored type owns the member identity +export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; +// (a schema.ts, if/when a runtime boundary needs one, is written to match this +// type and pinned to it — see global/namespace.md and features/data/index.md) // In an unrelated request-counter plugin: diff --git a/packages/data-ai/.claude/rules/features/data/index.md b/packages/data-ai/.claude/rules/features/data/index.md index 3a4d73d6..2a851b52 100644 --- a/packages/data-ai/.claude/rules/features/data/index.md +++ b/packages/data-ai/.claude/rules/features/data/index.md @@ -12,23 +12,62 @@ It depends on nothing but `@adobe/data` and other `data/` declarations, and needs no knowledge of anything built on top of it. Each data type is its own namespace folder (see `global/namespace.md`), holding -its schema, its derived type, and its pure synchronous helpers together: +its hand-authored type, its (optional, matching) schema, and its pure +synchronous helpers together: ``` data/player-mark/ - player-mark.ts # type alias + `export * as PlayerMark from "./public.js"` - schema.ts # `export const schema = { … } as const satisfies Schema` + player-mark.ts # HAND-AUTHORED type + `export * as PlayerMark from "./public.js"` + schema.ts # (optional / later) schema matching the type, pinned via Assert> public.ts # re-exports schema + helpers is.ts values.ts opponent.ts … # one pure helper per file ``` -- The **type is derived from the schema** (`Schema.ToType`), - never hand-written alongside it. -- The **schema is the single source of truth** for the shape; the derived - type and every consumer flow from it. +- The **type is authored by hand** in `.ts` — the readable source of + truth. Write it with the richest types available (`F32`, `Vec3`, unions, + branded aliases) so members carry their semantic meaning and get strong + IntelliSense / hover docs. +- The **schema is derived from the type, not the reverse.** It is written to + match the type and **pinned** to it with a compile-time assertion so the two + cannot drift (see below). A field typed as a branded primitive reuses that + type's schema — `F32` → `F32.schema`, `Vec3` → `Vec3.schema`. +- **Schemas are optional until a runtime boundary needs one.** A pure `data/` + type is complete with `.ts` alone. The schema is added only when + persistence, the wire, or an ECS component/resource requires it — and is + typically written by the agent in a later phase (e.g. when implementing the + ECS), not by the human up front. - Helpers are synchronous and pure; each has a sibling `*.test.ts`. -- Objects with only numeric 32 bit values/sub-structs should use Schema.fromStructProperties - It guarantees a valid struct schema which will be stored in linear memory. +- Objects of only 32-bit numeric values / sub-structs: the schema uses + `Schema.fromStructProperties` (guarantees a valid struct schema, stored in + linear memory). + +## Pinning the schema to the type + +The type is written first and by hand; the schema is written to match it and +is locked to it at compile time, so schema authoring (whenever it happens) +cannot silently drift from the type readers rely on. + +```ts +// player-mark/player-mark.ts — HAND-AUTHORED, the source of truth +export type PlayerMark = 'X' | 'O'; +export * as PlayerMark from './public.js'; + +// player-mark/schema.ts — written to MATCH the type (often later, by the agent) +import { Schema } from '@adobe/data/schema'; +import type { Assert, Equal } from '@adobe/data/types'; +import type { PlayerMark } from './player-mark.js'; + +export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema; + +// Compile-time pin: fails to build if schema and type diverge. +type _Pin = Assert, PlayerMark>>; +``` + +The `Assert>` line is the whole safety net: `Schema.ToType` must be exactly the hand-authored type (including readonly/optional), +or the file does not compile. `Schema.ToType` is used **only** inside the +assertion — never as the exported type. Consumers always import the +hand-authored `PlayerMark`. One folder is special: **`data/state/`** holds the feature's single `State` aggregate and its pure transforms/derivations (its own rule, `state.md`); diff --git a/packages/data-ai/.claude/rules/global/namespace.md b/packages/data-ai/.claude/rules/global/namespace.md index 5953b983..e0c275ab 100644 --- a/packages/data-ai/.claude/rules/global/namespace.md +++ b/packages/data-ai/.claude/rules/global/namespace.md @@ -32,8 +32,10 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform - `/.ts` is the only public import surface - Export a single type alias: `export type = ` (or `export type * from "./-types.js"` when the type lives in a separate file) -- Schema-based types: schema in `-schema.ts`, named `schema` (not `Schema`), derive with - `Schema.ToType` +- The type alias is **hand-authored** in `.ts`. When a schema is needed it lives in `-schema.ts` (or + `schema.ts` inside the folder), named `schema` (not `Schema`), and is **written to match the hand-authored type and pinned + to it** with `Assert, >>` — never the other way around. `Schema.ToType` appears only in + that assertion, never as the exported type. - Export namespace: `export * as from "./public.js"` - In public.ts, re-export every public constant file - Filenames map deterministically to single export: @@ -63,18 +65,24 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform ## Schema pattern example +The type is authored first and by hand; the schema (if/when needed) is written +to match it and pinned so it cannot drift. + ```ts -// src/types/player-mark/player-mark-schema.ts -export const schema = { enum: ['X', 'O'] } as const; +// src/types/player-mark/player-mark.ts — HAND-AUTHORED, the source of truth +export type PlayerMark = 'X' | 'O'; +export * as PlayerMark from './public.js'; + +// src/types/player-mark/player-mark-schema.ts — written to MATCH the type +import { Schema } from '@adobe/data/schema'; +import type { Assert, Equal } from '@adobe/data/types'; +import type { PlayerMark } from './player-mark.js'; +export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema; +// Compile-time pin: fails to build if schema and type diverge. +type _Pin = Assert, PlayerMark>>; // src/types/player-mark/public.ts export { schema } from './player-mark-schema.js'; - -// src/types/player-mark/player-mark.ts -import { Schema } from '@adobe/data/schema'; -import { schema } from './player-mark-schema.js'; -export type PlayerMark = Schema.ToType; -export * as PlayerMark from './public.js'; ``` ## Example: `Point` type refactor diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index d133363a..5a90d235 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.9.91", + "version": "0.9.92", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-ai/skills/build-data/SKILL.md b/packages/data-ai/skills/build-data/SKILL.md index 6e3b23b4..e75c2429 100644 --- a/packages/data-ai/skills/build-data/SKILL.md +++ b/packages/data-ai/skills/build-data/SKILL.md @@ -8,14 +8,14 @@ output: feature Create the feature's `data/` layer (pure spec; depends on nothing but `@adobe/data` and other `data/` declarations): -- one namespace folder per data type — `/{.ts, schema.ts, public.ts, .ts}`; +- one namespace folder per data type — `/{.ts, public.ts, .ts}` (+ `schema.ts` only when a runtime boundary needs it); - `data/state/` — the `State` aggregate plus its pure transforms/derivations. **Always create `data/state/`** — even for a shell feature whose only field is a boolean or enum. A feature with ECS resources/transactions but no `State` is incomplete: the transaction must wrap a pure transform, not invent the logic. In rare circumstances, there may be no real state in a feature in which case the state type = {} -The schema is the single source of truth; the type derives from it (`Schema.ToType`). +The hand-authored type in `.ts` is the source of truth. The schema is optional up front — add `schema.ts` only when a runtime boundary (persistence, wire, or an ECS component/resource) needs one, write it to match the type, and pin it with `type _Pin = Assert, >>` so it cannot drift. `Schema.ToType` is used only in that assertion, never as the exported type. Helpers are pure and unit-tested. Run this first — every other layer imports `data/`. The how is in the auto-loading rules: `features/data/index.md`, `features/data/state.md`, and From 3684cce99d77fc7b4cf847ba5cc02e17eefc0dec Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 14:02:52 -0700 Subject: [PATCH 2/6] docs(data-ai): rename assembled database to MainService; cases exported from test file - The feature's assembled database alias is now `MainService` in the folder-eponymous `main-service.ts` (namespace export: `MainService.plugin` / `MainService.Store`), matching the namespace pattern; cross-feature form is `MainService`. Updates main-service/index.md, element.md, conformance.md, and the build-app-entry / build-systems skills. - State-transform cases are now exported from the `.test.ts` file itself (no separate `.cases.ts`); the conformance runner imports them from `.test.js`. Updates features/data/state.md, main-service/conformance.md, features/index.md. Co-Authored-By: Claude Opus 4.8 --- packages/data-ai/.claude/rules/element.md | 2 +- .../.claude/rules/features/data/state.md | 17 ++++++++++------- .../data-ai/.claude/rules/features/index.md | 2 +- .../services/main-service/conformance.md | 7 ++++--- .../features/services/main-service/index.md | 17 ++++++++++------- .../data-ai/skills/build-app-entry/SKILL.md | 2 +- packages/data-ai/skills/build-systems/SKILL.md | 2 +- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/data-ai/.claude/rules/element.md b/packages/data-ai/.claude/rules/element.md index 73526a73..b5e26642 100644 --- a/packages/data-ai/.claude/rules/element.md +++ b/packages/data-ai/.claude/rules/element.md @@ -125,7 +125,7 @@ exists *only* for locator inputs — never for state, never for derived values, never for parent-forwarded flags. The `P` in `DatabaseElement

` (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 -FeatureDatabase.plugin` for a self-contained feature. **Exception:** an element +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 minimal base layer it consumes (e.g. `ComputedDatabase.plugin`) so every diff --git a/packages/data-ai/.claude/rules/features/data/state.md b/packages/data-ai/.claude/rules/features/data/state.md index f9816673..1b90d8ea 100644 --- a/packages/data-ai/.claude/rules/features/data/state.md +++ b/packages/data-ai/.claude/rules/features/data/state.md @@ -41,13 +41,16 @@ export * as State from "./public.js"; - Guard and **return `state` unchanged** on a no-op / illegal input rather than throwing — this keeps transforms idempotent under repeated application. - Each transform has a sibling `*.test.ts`; performance is irrelevant here, - correctness is everything. Keep its cases in a sibling `.cases.ts` - typed `ConformanceCase[]` (the shared `conformance-case.ts` type) — - 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. + correctness is everything. The test file **exports** its cases — + `export const cases: ConformanceCase[]` (the shared `conformance-case.ts` + type) — right alongside the `describe`/`it` that exercise them. This is + spec-owned truth the matching main-service conformance test imports **from the + `.test.js` file** unchanged (see `services/main-service/conformance.md`). + Keeping the cases in the test file rather than a separate `.cases.ts` + removes a file per transform — less folder clutter, same reuse. 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`. ## Derivations — `(state) => value` diff --git a/packages/data-ai/.claude/rules/features/index.md b/packages/data-ai/.claude/rules/features/index.md index e215720f..b580ac15 100644 --- a/packages/data-ai/.claude/rules/features/index.md +++ b/packages/data-ai/.claude/rules/features/index.md @@ -88,7 +88,7 @@ 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 `services/main-service/conformance/` (see `services/main-service/conformance.md`); the shared `{ before, args, after }` -cases are spec-owned (`data/state/.cases.ts`), so conforming the +cases are spec-owned (exported from `data/state/.test.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. diff --git a/packages/data-ai/.claude/rules/features/services/main-service/conformance.md b/packages/data-ai/.claude/rules/features/services/main-service/conformance.md index 40ef49aa..52e74a33 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/conformance.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/conformance.md @@ -29,8 +29,9 @@ Conformance test-support splits by concern along the layer boundary: `fromState(before)` → `apply(store, args)` → `toState ≡ after`. - **Spec-side, in `data/state/`** (State values, no store — so both the `data/` transform tests and this runner import them without a layer violation): - `conformance-case.ts` (`ConformanceCase`), the `.cases.ts` - cases, and `expect-state-matches.ts` (State equality). + `conformance-case.ts` (`ConformanceCase`), the cases **exported from each + `.test.ts`** (`import { cases } from "…/.test.js"` — no + separate `.cases.ts` file), and `expect-state-matches.ts` (State equality). ## No cast — build on `Store.create`, not a `Database` @@ -39,7 +40,7 @@ A **transaction is `(store, args) => void`**, so transaction conformance needs n `CoreDatabase.Store` — pass the plugin directly, `Store.create` reads its schema facets. Source it from the **lowest layer that declares all the schema** — `IndexDatabase`, or `CoreDatabase` if the feature has no indexes — **not** -`FeatureDatabase`: the store needs only schema, and the behaviour layers +`MainService`: the store needs only schema, and the behaviour layers (transactions / computed / systems) add none. `fromState`/`toState`/`apply` all operate on it, and `apply` calls the **raw transaction function** directly: diff --git a/packages/data-ai/.claude/rules/features/services/main-service/index.md b/packages/data-ai/.claude/rules/features/services/main-service/index.md index 0c858f90..e902995f 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/index.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/index.md @@ -119,26 +119,29 @@ export namespace IndexDatabase { } ``` -### The assembled database — `FeatureDatabase` +### The assembled database — `MainService` *Which* layer is topmost varies with the facets a feature uses (computed / service / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/` — need the *whole* feature database but must not name that layer, or adding or -dropping a layer rewrites them all. Alias it once: +dropping a layer rewrites them all. Alias it once from the **folder-eponymous +`main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in +its same-named file), as a namespace so consumers reach `MainService.plugin` / +`MainService.Store`: ```ts -// services/main-service/feature-database.ts -export { SystemDatabase as FeatureDatabase } from "./system-database/system-database.js"; +// services/main-service/main-service.ts +export { SystemDatabase as MainService } from "./system-database/system-database.js"; ``` -Every consumer references **`FeatureDatabase`** (`.plugin`, `.Store`) — never the +Every consumer references **`MainService`** (`.plugin`, `.Store`) — never the topmost layer. Add or drop a layer → change only this one line. **Cross-feature naming.** Inside a feature the core and assembled databases are -the bare `CoreDatabase` / `FeatureDatabase`. When *another* feature or package +the bare `CoreDatabase` / `MainService`. When *another* feature or package imports one — a peer built on it, the base `imports` a peer's `core-database`, a downstream package — reference it **feature-qualified**: `CoreDatabase` -/ `Database`, re-exported so from the feature's barrel +/ `MainService`, re-exported so from the feature's barrel (`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so two features' `CoreDatabase`s never collide. diff --git a/packages/data-ai/skills/build-app-entry/SKILL.md b/packages/data-ai/skills/build-app-entry/SKILL.md index 8fa11519..63201c99 100644 --- a/packages/data-ai/skills/build-app-entry/SKILL.md +++ b/packages/data-ai/skills/build-app-entry/SKILL.md @@ -9,7 +9,7 @@ output: app plugin, so all schemas coexist, persist, and sync while the base type stays decoupled. - The base reaches a peer only through a lazy element wrapper (dynamic import), so peers load on demand. -- The entry point creates the live database from `FeatureDatabase.plugin` (the base +- The entry point creates the live database from `MainService.plugin` (the base feature's assembled database — see `features/services/main-service/index.md`) and mounts the root UI. See `features/index.md` (one-app-many-features) for the wiring rules. diff --git a/packages/data-ai/skills/build-systems/SKILL.md b/packages/data-ai/skills/build-systems/SKILL.md index 344f24fe..7ea4867c 100644 --- a/packages/data-ai/skills/build-systems/SKILL.md +++ b/packages/data-ai/skills/build-systems/SKILL.md @@ -12,7 +12,7 @@ Create `services/main-service/system-database/system-database.ts`: `Database.Plu Database.Plugin.combine(.plugin, scheduler), systems })` — combine `scheduler` with the feature's **current top** main-service layer (`ActionDatabase` / `ServiceDatabase` / `ComputedDatabase`, whichever it built), never a hardcoded `ComputedDatabase`, so systems *and* any services/actions -compose into the one `FeatureDatabase`. The `systems` +compose into the one `MainService`. The `systems` map is declared **inline** (see `features/services/main-service/systems.md` — inline is required for `db` to be typed and for system-name inference; a `systems/` folder is optional, only for extracted per-frame body helpers). From 84381201104daf7097ebf46d68ace83171903019 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 14:36:57 -0700 Subject: [PATCH 3/6] docs(data-ai): allow a single constants.ts per namespace folder One exception to one-declaration-per-file: a namespace folder may have a single `constants.ts` exporting several plain `const` literals (no functions, no types), re-exported via public.ts so callers reach them as `.`. Function declarations stay one per file. Co-Authored-By: Claude Opus 4.8 --- .../data-ai/.claude/rules/global/namespace.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/data-ai/.claude/rules/global/namespace.md b/packages/data-ai/.claude/rules/global/namespace.md index e0c275ab..68329526 100644 --- a/packages/data-ai/.claude/rules/global/namespace.md +++ b/packages/data-ai/.claude/rules/global/namespace.md @@ -45,6 +45,26 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform - Function files: name by purpose only (e.g., `add.ts`, `sub.ts`). **Do not prefix** with the type name — the folder provides the namespace context. +## Constants file + +Constants are the **one exception** to one-declaration-per-file. A namespace folder MAY have a single `constants.ts` exporting several +plain `const` literals (no functions, no types), re-exported through `public.ts` so callers reach them as `.`: + +```ts +// duration/constants.ts +export const minSeconds = 5; +export const maxSeconds = 300; +export const defaultSeconds = 30; + +// duration/public.ts +export * from './constants.js'; + +// consumers: Duration.minSeconds, Duration.maxSeconds, Duration.defaultSeconds +``` + +Function declarations remain **one per file** (each in its own purpose-named file). If a value needs logic rather than being a plain +literal, it is a function and does not belong in `constants.ts`. + ## Anti-patterns (do not copy) - **`export namespace { ... }`** — Use `export * as from "./public.js"` instead. From 7c3c0ecc97a77bccfb9ed1105f96d9721467b887 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 14:51:56 -0700 Subject: [PATCH 4/6] docs(data-ai): fix stale rule cross-references after the global/ + features/ui/ moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rules referenced the old flat `.claude/rules/.md` paths from before the reorg; repoint them at the real locations: - `.claude/rules/binding-element.md` → `features/ui/binding-element.md` - `.claude/rules/presentation.md` → `features/ui/presentation.md` - `.claude/rules/hooks.md` / `ecs.md` → the rules-root `hooks.md` / `ecs.md` - drop the dangling `.claude/rules/squirrel/ui-data-access.md` (FFP-only, no data-ai equivalent) from observe.md. Touches observe.md, ecs.md, lit.md, features/ui/binding-element.md, global/react.md. Co-Authored-By: Claude Opus 4.8 --- packages/data-ai/.claude/rules/ecs.md | 2 +- .../data-ai/.claude/rules/features/ui/binding-element.md | 8 ++++---- packages/data-ai/.claude/rules/global/react.md | 2 +- packages/data-ai/.claude/rules/lit.md | 6 +++--- packages/data-ai/.claude/rules/observe.md | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/data-ai/.claude/rules/ecs.md b/packages/data-ai/.claude/rules/ecs.md index f4c0e3e6..874396d0 100644 --- a/packages/data-ai/.claude/rules/ecs.md +++ b/packages/data-ai/.claude/rules/ecs.md @@ -143,7 +143,7 @@ Synchronous, deterministic atomic mutations. Receive `store` and a payload. ### actions -UI components that call actions must never consume returned values — see `.claude/rules/binding-element.md` (Actions are fire-and-forget). +UI components that call actions must never consume returned values — see `features/ui/binding-element.md` (Actions are fire-and-forget). **At most one transaction per action.** Multiple transactions in a single action corrupt the undo/redo stack. diff --git a/packages/data-ai/.claude/rules/features/ui/binding-element.md b/packages/data-ai/.claude/rules/features/ui/binding-element.md index 1a535fd4..c7370e1d 100644 --- a/packages/data-ai/.claude/rules/features/ui/binding-element.md +++ b/packages/data-ai/.claude/rules/features/ui/binding-element.md @@ -38,7 +38,7 @@ A binding element is a thin wire between a service and a presentation. The class No other members. No `@state`. No private handler fields. No lifecycle methods (`connectedCallback`, `updated`, `firstUpdated`, `disconnectedCallback`) except on app-entrypoint classes where the lifecycle concern is app-boot shaped but cannot be expressed as a hook -(see `.claude/rules/hooks.md` for hook-first lifecycle). +(see `hooks.md` for hook-first lifecycle). ## `@property` allowlist @@ -114,7 +114,7 @@ one-liners** above. ## Lifecycle goes through hooks Any need for mount/unmount/update behavior routes through a hook. **Reuse order, one hook per file, presentation vs binding placement, and -testing notes:** `.claude/rules/hooks.md`. +testing notes:** `hooks.md`. Never inline lifecycle logic in the binding. App-entrypoint classes may override lifecycle methods when the concern is app-boot shaped (service wiring, ancestor lookup); even then, prefer a hook. @@ -158,14 +158,14 @@ expose them by design; reaching for them means a missing computed. A binding element imports a sibling `*-presentation.ts` file that exports exactly `render` (required) and optionally `unlocalized`. Nothing else — no types, no styles, no helpers. Consumers derive the props type via `Parameters[0]`. Presentation stays hook-free by -default; element hooks live here or in shared modules per `.claude/rules/hooks.md` and `.claude/rules/presentation.md`. +default; element hooks live here or in shared modules per `hooks.md` and `features/ui/presentation.md`. **For refactors whose stated scope does not include presentations**, the matching `*-presentation.ts` file must not change. Verify with `git diff --name-only | grep presentation`. If a scope-limited refactor requires a presentation signature change, stop and reconsider the binding-side helper — the scope fence exists because presentations are the public contract between binding and render output. Callback prop names use **verb** or **verbNoun** form. Never `on*` prefix. The binding-side variable matches the presentation prop name 1:1. -See `.claude/rules/presentation.md` for full examples. +See `features/ui/presentation.md` for full examples. ## Size budget diff --git a/packages/data-ai/.claude/rules/global/react.md b/packages/data-ai/.claude/rules/global/react.md index 3de5b94f..59c380dd 100644 --- a/packages/data-ai/.claude/rules/global/react.md +++ b/packages/data-ai/.claude/rules/global/react.md @@ -76,7 +76,7 @@ use `Parameters[0]`. ## Action callbacks (not events) -Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples. +Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples. **Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments: diff --git a/packages/data-ai/.claude/rules/lit.md b/packages/data-ai/.claude/rules/lit.md index 6bfb551b..b14b8986 100644 --- a/packages/data-ai/.claude/rules/lit.md +++ b/packages/data-ai/.claude/rules/lit.md @@ -76,7 +76,7 @@ there. ## Action callbacks (not events) -Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples and rationale. +Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples and rationale. --- @@ -91,7 +91,7 @@ view hosts. Multiple instances of the same element need a property to identify w ## Hooks -Canonical rules (reuse order, one hook per file, binding vs presentation): `.claude/rules/hooks.md`. +Canonical rules (reuse order, one hook per file, binding vs presentation): `hooks.md`. Use hooks from `src/elements/hooks/` and `@adobe/data-lit` instead of `connectedCallback`/`disconnectedCallback`. Use `useEffect` for setup/teardown, `useKeyboardEvent` for single key events. @@ -107,7 +107,7 @@ When creating or modifying a Lit element: 3. Use a single `useObservableValues` in the binding element. Observe only minimal values; use `Observe.withDefault` for slow-resolving values. 4. Pass observed values and action callbacks to presentation. -5. Keep presentation pure — no hooks (see `.claude/rules/hooks.md` for exceptions). +5. Keep presentation pure — no hooks (see `hooks.md` for exceptions). 6. Add `@property` only when entity binding requires it (multiple instances). 7. Presentation exports only `render` (and localization bundles where appropriate). Binding element sets `static styles` from `*.css.ts`. 8. Add `*-presentation.test.ts` for presentation when appropriate; do not unit test binding elements. diff --git a/packages/data-ai/.claude/rules/observe.md b/packages/data-ai/.claude/rules/observe.md index 11760fbf..4a69e775 100644 --- a/packages/data-ai/.claude/rules/observe.md +++ b/packages/data-ai/.claude/rules/observe.md @@ -147,7 +147,7 @@ Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(user `observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate (`@adobe/data/observe`'s `withDeduplicate` is reference-equality only). `observeSelectDeep` from a UI element is banned — let each element -observe its own entity (see `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`). +observe its own entity (see `ecs.md`). --- From 226a0c3274507fda0b492a366ce38398fa3a6e78 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 15:00:11 -0700 Subject: [PATCH 5/6] chore(release): bump @adobe/data monorepo to 0.9.92 Synchronized version bump across all packages for the data-ai standard changes (type-first schemas, cases-in-test, MainService rename, constants.ts, rule reference fixes). Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- packages/data-gpu-hopper/package.json | 2 +- packages/data-gpu-samples/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit-space-rock-game/package.json | 2 +- packages/data-lit-tictactoe/package.json | 2 +- packages/data-lit-todo/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-p2p-tictactoe/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react-hello/package.json | 2 +- packages/data-react-pixie/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid-dashboard/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data/package.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 09b6b1e9..731ebbc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.91", + "version": "0.9.92", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-gpu-hopper/package.json b/packages/data-gpu-hopper/package.json index 2d0349f6..1db85b3b 100644 --- a/packages/data-gpu-hopper/package.json +++ b/packages/data-gpu-hopper/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-hopper", - "version": "0.9.91", + "version": "0.9.92", "description": "Hopper sample - real-time ECS game rendered as colored cubes via @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu-samples/package.json b/packages/data-gpu-samples/package.json index 338e8e65..00549167 100644 --- a/packages/data-gpu-samples/package.json +++ b/packages/data-gpu-samples/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-samples", - "version": "0.9.91", + "version": "0.9.92", "description": "WebGPU samples built on @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index 8842b2ba..e10089f1 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.9.91", + "version": "0.9.92", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-space-rock-game/package.json b/packages/data-lit-space-rock-game/package.json index 7fc9426c..5cfcd558 100644 --- a/packages/data-lit-space-rock-game/package.json +++ b/packages/data-lit-space-rock-game/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-space-rock-game", - "version": "0.9.91", + "version": "0.9.92", "description": "Space Rock Game sample - real-time ECS game with Lit and @adobe/data", "type": "module", "private": true, diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index 82ecfeab..8c168905 100644 --- a/packages/data-lit-tictactoe/package.json +++ b/packages/data-lit-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-tictactoe", - "version": "0.9.91", + "version": "0.9.92", "description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService", "type": "module", "private": true, diff --git a/packages/data-lit-todo/package.json b/packages/data-lit-todo/package.json index bc06e6b8..d4abc531 100644 --- a/packages/data-lit-todo/package.json +++ b/packages/data-lit-todo/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-todo", - "version": "0.9.91", + "version": "0.9.92", "description": "Todo application - Lit web components with @adobe/data ECS", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index e8c73cbe..6a5f6edf 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.91", + "version": "0.9.92", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-p2p-tictactoe/package.json b/packages/data-p2p-tictactoe/package.json index eee5991d..b5beebdc 100644 --- a/packages/data-p2p-tictactoe/package.json +++ b/packages/data-p2p-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-p2p-tictactoe", - "version": "0.9.91", + "version": "0.9.92", "description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync", "type": "module", "private": true, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index feb4ce4d..112f8de4 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.91", + "version": "0.9.92", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react-hello/package.json b/packages/data-react-hello/package.json index 00fc7df3..867cae7f 100644 --- a/packages/data-react-hello/package.json +++ b/packages/data-react-hello/package.json @@ -1,6 +1,6 @@ { "name": "data-react-hello", - "version": "0.9.91", + "version": "0.9.92", "description": "Hello World sample - click counter using @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react-pixie/package.json b/packages/data-react-pixie/package.json index 93e4b217..d799c8ec 100644 --- a/packages/data-react-pixie/package.json +++ b/packages/data-react-pixie/package.json @@ -1,6 +1,6 @@ { "name": "data-react-pixie", - "version": "0.9.91", + "version": "0.9.92", "description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index de312f2d..346a70f3 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.91", + "version": "0.9.92", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid-dashboard/package.json b/packages/data-solid-dashboard/package.json index ae6b5f59..5d25baa1 100644 --- a/packages/data-solid-dashboard/package.json +++ b/packages/data-solid-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "data-solid-dashboard", - "version": "0.9.91", + "version": "0.9.92", "description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS", "type": "module", "private": true, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 2351c714..35bda073 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.91", + "version": "0.9.92", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index a64d2e22..8481a413 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.91", + "version": "0.9.92", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index 2973248d..edb58236 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.91", + "version": "0.9.92", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, From 79f59eb1d3cf6506901e5d5cf0b770995ac5dc42 Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Thu, 30 Jul 2026 18:14:56 -0700 Subject: [PATCH 6/6] docs(data-ai): rename db -> service in main-service computed/services/actions examples Matches the code-side rename (computed/services/actions parameter renamed to `service`, MainService-slice terminology); transactions keep `t`. --- .../features/services/main-service/actions.md | 10 +++++----- .../features/services/main-service/computed.md | 6 +++--- .../rules/features/services/main-service/index.md | 7 ++++--- .../features/services/main-service/services.md | 15 ++++++++------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/data-ai/.claude/rules/features/services/main-service/actions.md b/packages/data-ai/.claude/rules/features/services/main-service/actions.md index ca34fbe1..8220d1ff 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/actions.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/actions.md @@ -13,19 +13,19 @@ timing — and then commit the result through a transaction. ```ts import type { ServiceDatabase } from "../../service-database/service-database.js"; -export const addRandomTodo = async (db: ServiceDatabase) => { - const name = await db.services.nameGenerator.generateName(); // await a services/ port - db.transactions.createTodo({ name }); // then exactly one commit +export const addRandomTodo = async (service: ServiceDatabase) => { + const name = await service.services.nameGenerator.generateName(); // await a services/ port + service.transactions.createTodo({ name }); // then exactly one commit }; ``` -- Type the `db` parameter on the lowest database layer exposing what the +- Type the `service` parameter on the lowest database layer exposing what the action touches — usually `ServiceDatabase` (services **and** transactions). Never the action layer itself (that would be a cycle). - **Call at most one transaction** per action, so undo/redo stays one step per operation. - **Fire-and-forget.** An action's return value is not consumed; results flow - back through observables. `db.services.*` calls are `void` or + back through observables. `service.services.*` calls are `void` or awaited-internally, never surfaced to the caller. - Do the outside-world work here: await/sequence `services/` calls, and if a slow call needs timing, compute it here around the call. diff --git a/packages/data-ai/.claude/rules/features/services/main-service/computed.md b/packages/data-ai/.claude/rules/features/services/main-service/computed.md index 4a8249ae..8d6e8133 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/computed.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/computed.md @@ -7,7 +7,7 @@ paths: One derived value per file: a `cached` function of a database layer that returns an `Observe` of state projected through pure `data/` helpers. -Derivation logic itself lives in `data/`; a computed only wires a db +Derivation logic itself lives in `data/`; a computed only wires a service observable to it. ```ts @@ -16,8 +16,8 @@ import { Observe } from "@adobe/data/observe"; import { BoardState } from "../../../data/board-state/board-state.js"; import type { IndexDatabase } from "../../index-database/index-database.js"; -export const status = cached((db: IndexDatabase) => - Observe.withFilter(db.observe.resources.board, BoardState.deriveStatus), +export const status = cached((service: IndexDatabase) => + Observe.withFilter(service.observe.resources.board, BoardState.deriveStatus), ); ``` diff --git a/packages/data-ai/.claude/rules/features/services/main-service/index.md b/packages/data-ai/.claude/rules/features/services/main-service/index.md index e902995f..546e340e 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/index.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/index.md @@ -156,9 +156,10 @@ two features' `CoreDatabase`s never collide. reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the moment it reads an index. - **the whole `Database`** — **computed**, **services**, and **actions** each - take `db: ` (the lowest layer whose database exposes what they - read/call): computed reads `db.observe.*`; services read observables and call - transactions; actions call `db.services.*` then `db.transactions.*`. + take `service: ` (the lowest layer whose database exposes what they + read/call): computed reads `service.observe.*`; services read observables and + call transactions; actions call `service.services.*` then + `service.transactions.*`. Each subfolder has its own rule. Modelling a plugin's authored vs. derived surface is covered by `plugin-modelling.md`. diff --git a/packages/data-ai/.claude/rules/features/services/main-service/services.md b/packages/data-ai/.claude/rules/features/services/main-service/services.md index 6fb3ebbd..3c33ae67 100644 --- a/packages/data-ai/.claude/rules/features/services/main-service/services.md +++ b/packages/data-ai/.claude/rules/features/services/main-service/services.md @@ -5,20 +5,21 @@ paths: # database/services/ — database-bound service factories -One factory per file: `createService(db, …)` that binds a service +One factory per file: `createService(service, …)` that binds a service implementation to a live database — reading its observables and invoking its transactions. This is where the async `services/` contracts (or framework services like `AgenticService`) are wired to ECS state. ```ts -export const createAgentService = (db: ComputedDatabase, mark: PlayerMark): AgenticService => - /* … reads db.observe.*, calls db.transactions.* … */; +export const createAgentService = (service: ComputedDatabase, mark: PlayerMark): AgenticService => + /* … reads service.observe.*, calls service.transactions.* … */; ``` -Type `db` on the lowest layer exposing what the factory reads/calls (never the -`service-database` itself — that would be a cycle). +Type `service` on the lowest layer exposing what the factory reads/calls (never +the `service-database` itself — that would be a cycle). An `index.ts` barrel re-exports the factories; `service-database.ts` registers them under the `services` facet, each keyed by the name -consumers read it as (`db.services.agent`). Defining a standalone service -contract (not yet bound to a db) belongs in the feature `services/` layer. +consumers read it as (`service.services.agent`). Defining a standalone +service contract (not yet bound to a database) belongs in the feature +`services/` layer.