@adobe/data-ai 0.9.90 → 0.9.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude/rules/README.md +11 -6
  2. package/.claude/rules/data-modelling.md +4 -3
  3. package/.claude/rules/ecs.md +1 -1
  4. package/.claude/rules/element.md +1 -1
  5. package/.claude/rules/features/data/index.md +49 -10
  6. package/.claude/rules/features/data/state.md +10 -7
  7. package/.claude/rules/features/index.md +5 -4
  8. package/.claude/rules/features/services/index.md +1 -1
  9. package/.claude/rules/features/services/main-service/actions.md +5 -5
  10. package/.claude/rules/features/services/main-service/computed.md +3 -3
  11. package/.claude/rules/features/services/main-service/conformance.md +4 -3
  12. package/.claude/rules/features/services/main-service/index.md +14 -10
  13. package/.claude/rules/features/services/main-service/services.md +8 -7
  14. package/.claude/rules/features/services/main-service/systems.md +6 -2
  15. package/.claude/rules/features/services/main-service/transactions.md +1 -1
  16. package/.claude/rules/features/ui/binding-element.md +6 -6
  17. package/.claude/rules/{namespace.md → global/namespace.md} +38 -10
  18. package/.claude/rules/{react.md → global/react.md} +1 -1
  19. package/.claude/rules/lit.md +3 -3
  20. package/.claude/rules/observe.md +1 -1
  21. package/.claude-plugin/plugin.json +1 -1
  22. package/README.md +1 -1
  23. package/bin/cli.mjs +51 -3
  24. package/package.json +3 -3
  25. package/skills/build-app-entry/SKILL.md +1 -1
  26. package/skills/build-data/SKILL.md +3 -3
  27. package/skills/build-services/SKILL.md +1 -1
  28. package/skills/build-systems/SKILL.md +4 -1
  29. package/skills/build-transactions/SKILL.md +2 -1
  30. package/skills/meta-build/SKILL.md +1 -0
  31. package/skills/review/SKILL.md +1 -0
  32. /package/.claude/rules/{cohesion.md → global/cohesion.md} +0 -0
  33. /package/.claude/rules/{function-references.md → global/function-references.md} +0 -0
  34. /package/.claude/rules/{type-casts.md → global/type-casts.md} +0 -0
@@ -13,6 +13,8 @@ folder's own `index.md` holds just enough to understand that folder; each
13
13
  child gets its own file (recursing wherever the source tree does):
14
14
 
15
15
  ```
16
+ global/ # always-on conventions — deliberately repo-wide globs
17
+ namespace.md cohesion.md type-casts.md function-references.md react.md
16
18
  features/
17
19
  index.md # the feature layering as a whole (ui → services → data)
18
20
  data/
@@ -28,12 +30,15 @@ features/
28
30
  ui/index.md # UI (points to element / presentation / lazy-element file rules)
29
31
  ```
30
32
 
31
- Alongside `features/`, the rules-root `.md` files hold the **cross-cutting**
32
- patterns the feature rules reference — `namespace.md`, `data-modelling.md`,
33
- `type-casts.md`, `function-references.md`, `cohesion.md`, `archetypes.md` (row
34
- iteration), `plugin-modelling.md`, and the UI file rules (`element.md`,
35
- `lazy-element.md`, `presentation.md`). They live here so the bundle is self-contained; this repo
36
- symlinks them into its own `.claude/rules/` (as it does `features/`).
33
+ The `global/` rules (`namespace.md`, `cohesion.md`, `type-casts.md`,
34
+ `function-references.md`, `react.md`) are **intentionally repo-wide** — their
35
+ `paths:` globs (`**/*.ts` etc.) apply everywhere, by design, so the folder name
36
+ makes that explicit. Alongside `features/`, the remaining rules-root `.md` files
37
+ hold the other **cross-cutting** patterns the feature rules reference —
38
+ `data-modelling.md`, `archetypes.md` (row iteration), `plugin-modelling.md`, and
39
+ the UI file rules (`element.md`, `lazy-element.md`, `presentation.md`). They live
40
+ here so the bundle is self-contained; this repo symlinks them into its own
41
+ `.claude/rules/` (as it does `features/`).
37
42
 
38
43
  Each rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
39
44
  rules apply only once an application opts into the feature-folder pattern
@@ -15,9 +15,10 @@ discriminated unions.
15
15
  ## Example
16
16
 
17
17
  ```ts
18
- // types/http-method/schema.ts
19
- export const schema = { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] }
20
- as const satisfies Schema;
18
+ // types/http-method/http-method.ts — the hand-authored type owns the member identity
19
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
20
+ // (a schema.ts, if/when a runtime boundary needs one, is written to match this
21
+ // type and pinned to it — see global/namespace.md and features/data/index.md)
21
22
 
22
23
  // In an unrelated request-counter plugin:
23
24
 
@@ -143,7 +143,7 @@ Synchronous, deterministic atomic mutations. Receive `store` and a payload.
143
143
 
144
144
  ### actions
145
145
 
146
- UI components that call actions must never consume returned values — see `.claude/rules/binding-element.md` (Actions are fire-and-forget).
146
+ UI components that call actions must never consume returned values — see `features/ui/binding-element.md` (Actions are fire-and-forget).
147
147
 
148
148
  **At most one transaction per action.** Multiple transactions in a single action corrupt the undo/redo stack.
149
149
 
@@ -125,7 +125,7 @@ exists *only* for locator inputs — never for state, never for derived
125
125
  values, never for parent-forwarded flags. The `P` in `DatabaseElement<P>` (and
126
126
  the `get plugin()` it returns) is the surface the element *consumes* — never the
127
127
  varying topmost-layer name (see `features/services/main-service/index.md`): `typeof
128
- FeatureDatabase.plugin` for a self-contained feature. **Exception:** an element
128
+ MainService.plugin` for a self-contained feature. **Exception:** an element
129
129
  meant to be *extended* — injected with various databases that build on this
130
130
  feature (a peer app that adds an agent, a p2p/presence build) — types on the
131
131
  minimal base layer it consumes (e.g. `ComputedDatabase.plugin`) so every
@@ -11,24 +11,63 @@ communication over the wire — no functions, no handles, just plain data.
11
11
  It depends on nothing but `@adobe/data` and other `data/` declarations, and
12
12
  needs no knowledge of anything built on top of it.
13
13
 
14
- Each data type is its own namespace folder (see `namespace.md`), holding
15
- its schema, its derived type, and its pure synchronous helpers together:
14
+ Each data type is its own namespace folder (see `global/namespace.md`), holding
15
+ its hand-authored type, its (optional, matching) schema, and its pure
16
+ synchronous helpers together:
16
17
 
17
18
  ```
18
19
  data/player-mark/
19
- player-mark.ts # type alias + `export * as PlayerMark from "./public.js"`
20
- schema.ts # `export const schema = { … } as const satisfies Schema`
20
+ player-mark.ts # HAND-AUTHORED type + `export * as PlayerMark from "./public.js"`
21
+ schema.ts # (optional / later) schema matching the type, pinned via Assert<Equal<…>>
21
22
  public.ts # re-exports schema + helpers
22
23
  is.ts values.ts opponent.ts … # one pure helper per file
23
24
  ```
24
25
 
25
- - The **type is derived from the schema** (`Schema.ToType<typeof schema>`),
26
- never hand-written alongside it.
27
- - The **schema is the single source of truth** for the shape; the derived
28
- type and every consumer flow from it.
26
+ - The **type is authored by hand** in `<type>.ts` — the readable source of
27
+ truth. Write it with the richest types available (`F32`, `Vec3`, unions,
28
+ branded aliases) so members carry their semantic meaning and get strong
29
+ IntelliSense / hover docs.
30
+ - The **schema is derived from the type, not the reverse.** It is written to
31
+ match the type and **pinned** to it with a compile-time assertion so the two
32
+ cannot drift (see below). A field typed as a branded primitive reuses that
33
+ type's schema — `F32` → `F32.schema`, `Vec3` → `Vec3.schema`.
34
+ - **Schemas are optional until a runtime boundary needs one.** A pure `data/`
35
+ type is complete with `<type>.ts` alone. The schema is added only when
36
+ persistence, the wire, or an ECS component/resource requires it — and is
37
+ typically written by the agent in a later phase (e.g. when implementing the
38
+ ECS), not by the human up front.
29
39
  - Helpers are synchronous and pure; each has a sibling `*.test.ts`.
30
- - Objects with only numeric 32 bit values/sub-structs should use Schema.fromStructProperties
31
- It guarantees a valid struct schema which will be stored in linear memory.
40
+ - Objects of only 32-bit numeric values / sub-structs: the schema uses
41
+ `Schema.fromStructProperties` (guarantees a valid struct schema, stored in
42
+ linear memory).
43
+
44
+ ## Pinning the schema to the type
45
+
46
+ The type is written first and by hand; the schema is written to match it and
47
+ is locked to it at compile time, so schema authoring (whenever it happens)
48
+ cannot silently drift from the type readers rely on.
49
+
50
+ ```ts
51
+ // player-mark/player-mark.ts — HAND-AUTHORED, the source of truth
52
+ export type PlayerMark = 'X' | 'O';
53
+ export * as PlayerMark from './public.js';
54
+
55
+ // player-mark/schema.ts — written to MATCH the type (often later, by the agent)
56
+ import { Schema } from '@adobe/data/schema';
57
+ import type { Assert, Equal } from '@adobe/data/types';
58
+ import type { PlayerMark } from './player-mark.js';
59
+
60
+ export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema;
61
+
62
+ // Compile-time pin: fails to build if schema and type diverge.
63
+ type _Pin = Assert<Equal<Schema.ToType<typeof schema>, PlayerMark>>;
64
+ ```
65
+
66
+ The `Assert<Equal<…>>` line is the whole safety net: `Schema.ToType<typeof
67
+ schema>` must be exactly the hand-authored type (including readonly/optional),
68
+ or the file does not compile. `Schema.ToType` is used **only** inside the
69
+ assertion — never as the exported type. Consumers always import the
70
+ hand-authored `PlayerMark`.
32
71
 
33
72
  One folder is special: **`data/state/`** holds the feature's single `State`
34
73
  aggregate and its pure transforms/derivations (its own rule, `state.md`);
@@ -41,13 +41,16 @@ export * as State from "./public.js";
41
41
  - Guard and **return `state` unchanged** on a no-op / illegal input rather
42
42
  than throwing — this keeps transforms idempotent under repeated application.
43
43
  - Each transform has a sibling `*.test.ts`; performance is irrelevant here,
44
- correctness is everything. Keep its cases in a sibling `<transform>.cases.ts`
45
- typed `ConformanceCase<Args>[]` (the shared `conformance-case.ts` type) —
46
- spec-owned truth the matching main-service conformance test imports unchanged (see
47
- `services/main-service/conformance.md`). Author `before`/`after` as full `State`
48
- (`{ ...State.create(), …overrides }`); the generic-slice signature lets them
49
- flow through. Tolerant full-`State` equality is the shared
50
- `expect-state-matches.ts`, alongside the cases here.
44
+ correctness is everything. The test file **exports** its cases —
45
+ `export const cases: ConformanceCase<Args>[]` (the shared `conformance-case.ts`
46
+ type) — right alongside the `describe`/`it` that exercise them. This is
47
+ spec-owned truth the matching main-service conformance test imports **from the
48
+ `<transform>.test.js` file** unchanged (see `services/main-service/conformance.md`).
49
+ Keeping the cases in the test file rather than a separate `<transform>.cases.ts`
50
+ removes a file per transform — less folder clutter, same reuse. Author
51
+ `before`/`after` as full `State` (`{ ...State.create(), …overrides }`); the
52
+ generic-slice signature lets them flow through. Tolerant full-`State` equality is
53
+ the shared `expect-state-matches.ts`.
51
54
 
52
55
  ## Derivations — `(state) => value`
53
56
 
@@ -88,7 +88,7 @@ main-service mutation, seeded and read back through a test-only store↔`State`
88
88
  projection, equals the pure `data/` transform it stands for. The projection and
89
89
  its runner live in `services/main-service/conformance/` (see
90
90
  `services/main-service/conformance.md`); the shared `{ before, args, after }`
91
- cases are spec-owned (`data/state/<transform>.cases.ts`), so conforming the
91
+ cases are spec-owned (exported from `data/state/<transform>.test.ts`), so conforming the
92
92
  implementation is "substitute the implementation, reuse the expectations." This
93
93
  lets `main-service` be largely mechanical and agent-generated, with the spec as
94
94
  oracle. *How* to author each layer lives in the per-folder rules below.
@@ -106,6 +106,7 @@ current examples of this structure. `data-lit-todo` is the most complete
106
106
  See the rules under each folder: `data/`, `services/` — the `main-service/`
107
107
  subtree (components, resources, archetypes, computed, indexes, transactions,
108
108
  systems, conformance) and capability-contract services — and `ui/`.
109
- Cross-cutting patterns live at the rules root — `namespace.md`,
110
- `data-modelling.md`, `archetypes.md` (row iteration), `type-casts.md`,
111
- `cohesion.md`.
109
+ Always-on conventions live in `global/` (`namespace.md`, `cohesion.md`,
110
+ `type-casts.md`, `function-references.md`, `react.md` — deliberately repo-wide);
111
+ other cross-cutting patterns at the rules root — `data-modelling.md`,
112
+ `archetypes.md` (row iteration).
@@ -22,7 +22,7 @@ its own subtree.
22
22
 
23
23
  ## Capability contracts
24
24
 
25
- Each is a namespace folder (`namespace.md`); the export and folder both carry the
25
+ Each is a namespace folder (`global/namespace.md`); the export and folder both carry the
26
26
  `-service` suffix. A service is the boundary between pure feature code and the
27
27
  outside world, which is why its members are async — enabling cross-process
28
28
  portability and lazy loading (`AsyncDataService.createLazy`).
@@ -13,19 +13,19 @@ timing — and then commit the result through a transaction.
13
13
  ```ts
14
14
  import type { ServiceDatabase } from "../../service-database/service-database.js";
15
15
 
16
- export const addRandomTodo = async (db: ServiceDatabase) => {
17
- const name = await db.services.nameGenerator.generateName(); // await a services/ port
18
- db.transactions.createTodo({ name }); // then exactly one commit
16
+ export const addRandomTodo = async (service: ServiceDatabase) => {
17
+ const name = await service.services.nameGenerator.generateName(); // await a services/ port
18
+ service.transactions.createTodo({ name }); // then exactly one commit
19
19
  };
20
20
  ```
21
21
 
22
- - Type the `db` parameter on the lowest database layer exposing what the
22
+ - Type the `service` parameter on the lowest database layer exposing what the
23
23
  action touches — usually `ServiceDatabase` (services **and**
24
24
  transactions). Never the action layer itself (that would be a cycle).
25
25
  - **Call at most one transaction** per action, so undo/redo stays one step
26
26
  per operation.
27
27
  - **Fire-and-forget.** An action's return value is not consumed; results flow
28
- back through observables. `db.services.*` calls are `void` or
28
+ back through observables. `service.services.*` calls are `void` or
29
29
  awaited-internally, never surfaced to the caller.
30
30
  - Do the outside-world work here: await/sequence `services/` calls, and if a
31
31
  slow call needs timing, compute it here around the call.
@@ -7,7 +7,7 @@ paths:
7
7
 
8
8
  One derived value per file: a `cached` function of a database layer that
9
9
  returns an `Observe` of state projected through pure `data/` helpers.
10
- Derivation logic itself lives in `data/`; a computed only wires a db
10
+ Derivation logic itself lives in `data/`; a computed only wires a service
11
11
  observable to it.
12
12
 
13
13
  ```ts
@@ -16,8 +16,8 @@ import { Observe } from "@adobe/data/observe";
16
16
  import { BoardState } from "../../../data/board-state/board-state.js";
17
17
  import type { IndexDatabase } from "../../index-database/index-database.js";
18
18
 
19
- export const status = cached((db: IndexDatabase) =>
20
- Observe.withFilter(db.observe.resources.board, BoardState.deriveStatus),
19
+ export const status = cached((service: IndexDatabase) =>
20
+ Observe.withFilter(service.observe.resources.board, BoardState.deriveStatus),
21
21
  );
22
22
  ```
23
23
 
@@ -29,8 +29,9 @@ Conformance test-support splits by concern along the layer boundary:
29
29
  `fromState(before)` → `apply(store, args)` → `toState ≡ after`.
30
30
  - **Spec-side, in `data/state/`** (State values, no store — so both the `data/`
31
31
  transform tests and this runner import them without a layer violation):
32
- `conformance-case.ts` (`ConformanceCase<Args>`), the `<transform>.cases.ts`
33
- cases, and `expect-state-matches.ts` (State equality).
32
+ `conformance-case.ts` (`ConformanceCase<Args>`), the cases **exported from each
33
+ `<transform>.test.ts`** (`import { cases } from "…/<transform>.test.js"` — no
34
+ separate `.cases.ts` file), and `expect-state-matches.ts` (State equality).
34
35
 
35
36
  ## No cast — build on `Store.create`, not a `Database`
36
37
 
@@ -39,7 +40,7 @@ A **transaction is `(store, args) => void`**, so transaction conformance needs n
39
40
  `CoreDatabase.Store` — pass the plugin directly, `Store.create` reads its schema
40
41
  facets. Source it from the **lowest layer that declares all the schema** —
41
42
  `IndexDatabase`, or `CoreDatabase` if the feature has no indexes — **not**
42
- `FeatureDatabase`: the store needs only schema, and the behaviour layers
43
+ `MainService`: the store needs only schema, and the behaviour layers
43
44
  (transactions / computed / systems) add none.
44
45
  `fromState`/`toState`/`apply` all operate on it, and `apply` calls the **raw
45
46
  transaction function** directly:
@@ -119,26 +119,29 @@ export namespace IndexDatabase {
119
119
  }
120
120
  ```
121
121
 
122
- ### The assembled database — `FeatureDatabase`
122
+ ### The assembled database — `MainService`
123
123
 
124
124
  *Which* layer is topmost varies with the facets a feature uses (computed /
125
125
  service / action / system). Consumers — `ui/`, the app entry, `services/main-service/conformance/`
126
126
  — need the *whole* feature database but must not name that layer, or adding or
127
- dropping a layer rewrites them all. Alias it once:
127
+ dropping a layer rewrites them all. Alias it once from the **folder-eponymous
128
+ `main-service.ts`** (per `global/namespace.md`, a folder's primary export lives in
129
+ its same-named file), as a namespace so consumers reach `MainService.plugin` /
130
+ `MainService.Store`:
128
131
 
129
132
  ```ts
130
- // services/main-service/feature-database.ts
131
- export { SystemDatabase as FeatureDatabase } from "./system-database/system-database.js";
133
+ // services/main-service/main-service.ts
134
+ export { SystemDatabase as MainService } from "./system-database/system-database.js";
132
135
  ```
133
136
 
134
- Every consumer references **`FeatureDatabase`** (`.plugin`, `.Store`) — never the
137
+ Every consumer references **`MainService`** (`.plugin`, `.Store`) — never the
135
138
  topmost layer. Add or drop a layer → change only this one line.
136
139
 
137
140
  **Cross-feature naming.** Inside a feature the core and assembled databases are
138
- the bare `CoreDatabase` / `FeatureDatabase`. When *another* feature or package
141
+ the bare `CoreDatabase` / `MainService`. When *another* feature or package
139
142
  imports one — a peer built on it, the base `imports` a peer's `core-database`, a
140
143
  downstream package — reference it **feature-qualified**: `<Feature>CoreDatabase`
141
- / `<Feature>Database`, re-exported so from the feature's barrel
144
+ / `<Feature>MainService`, re-exported so from the feature's barrel
142
145
  (`export { CoreDatabase as TodoCoreDatabase } from "…/core-database.js"`) — so
143
146
  two features' `CoreDatabase`s never collide.
144
147
 
@@ -153,9 +156,10 @@ two features' `CoreDatabase`s never collide.
153
156
  reads/writes entities, resources, or archetypes; **`IndexDatabase.Store`** the
154
157
  moment it reads an index.
155
158
  - **the whole `Database`** — **computed**, **services**, and **actions** each
156
- take `db: <Layer>` (the lowest layer whose database exposes what they
157
- read/call): computed reads `db.observe.*`; services read observables and call
158
- transactions; actions call `db.services.*` then `db.transactions.*`.
159
+ take `service: <Layer>` (the lowest layer whose database exposes what they
160
+ read/call): computed reads `service.observe.*`; services read observables and
161
+ call transactions; actions call `service.services.*` then
162
+ `service.transactions.*`.
159
163
 
160
164
  Each subfolder has its own rule. Modelling a plugin's authored vs. derived
161
165
  surface is covered by `plugin-modelling.md`.
@@ -5,20 +5,21 @@ paths:
5
5
 
6
6
  # database/services/ — database-bound service factories
7
7
 
8
- One factory per file: `create<Name>Service(db, …)` that binds a service
8
+ One factory per file: `create<Name>Service(service, …)` that binds a service
9
9
  implementation to a live database — reading its observables and invoking
10
10
  its transactions. This is where the async `services/` contracts (or
11
11
  framework services like `AgenticService`) are wired to ECS state.
12
12
 
13
13
  ```ts
14
- export const createAgentService = (db: ComputedDatabase, mark: PlayerMark): AgenticService =>
15
- /* … reads db.observe.*, calls db.transactions.* … */;
14
+ export const createAgentService = (service: ComputedDatabase, mark: PlayerMark): AgenticService =>
15
+ /* … reads service.observe.*, calls service.transactions.* … */;
16
16
  ```
17
17
 
18
- Type `db` on the lowest layer exposing what the factory reads/calls (never the
19
- `service-database` itself — that would be a cycle).
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
20
 
21
21
  An `index.ts` barrel re-exports the factories; `service-database.ts`
22
22
  registers them under the `services` facet, each keyed by the name
23
- consumers read it as (`db.services.agent`). Defining a standalone service
24
- contract (not yet bound to a db) belongs in the feature `services/` layer.
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.
@@ -32,6 +32,8 @@ import { Database, scheduler } from "@adobe/data/ecs";
32
32
  import { Motion } from "../../data/motion/motion.js";
33
33
 
34
34
  const plugin = Database.Plugin.create({
35
+ // combine scheduler with the CURRENT TOP layer — ComputedDatabase here, but
36
+ // ActionDatabase / ServiceDatabase if the feature built those (systems come last).
35
37
  extends: Database.Plugin.combine(ComputedDatabase.plugin, scheduler),
36
38
  systems: {
37
39
  control: { create: (db) => () => { /* input → ship; db.transactions.fireBullet() */ } },
@@ -119,8 +121,10 @@ with no rAF and no rendering attached.
119
121
 
120
122
  ## Layer
121
123
 
122
- `system-database.ts` extends the previous main-service layer (usually `ComputedDatabase`),
123
- combined with `scheduler`, and declares the `systems` map **inline** (see above —
124
+ `system-database.ts` extends the feature's **current top** main-service layer (`ActionDatabase` /
125
+ `ServiceDatabase` / `ComputedDatabase` — whichever it built; systems come last in the pipeline, so
126
+ they must sit atop any service/action layers, not a hardcoded `ComputedDatabase`), combined with
127
+ `scheduler`, and declares the `systems` map **inline** (see above —
124
128
  this is the one facet not split one-per-file, because `create`'s `db` is only typed
125
129
  inline and standalone declarations break name inference). A `systems/` folder is
126
130
  optional: it holds extracted per-frame body helpers (`(db) => () => void`) only when
@@ -41,5 +41,5 @@ export const playMove = (t: CoreDatabase.Store, { index }: PlayMoveArgs) => {
41
41
  transactions (`readShip`, `readBoard` — a `(t) => value` function) may live
42
42
  beside them in `transactions/`, but keep it **out of the barrel**, or it gets
43
43
  registered as a dispatchable transaction. (These are related helpers, so they
44
- belong here, not in a separate folder — `cohesion.md`'s peer-level caution
44
+ belong here, not in a separate folder — `global/cohesion.md`'s peer-level caution
45
45
  doesn't apply.)
@@ -32,15 +32,15 @@ Presentation files (`*-presentation.ts`), CSS files (`*.css.ts`), tests (`*.test
32
32
  A binding element is a thin wire between a service and a presentation. The class body contains:
33
33
 
34
34
  1. Optional static `styles` property definition
35
- 2. Zero or more `@property` declarations (whitelisted below).
35
+ 2. Zero or more `@property` declarations (allowed below).
36
36
  3. One `render()` method whose body is: one `useObservableValues(...)` call, another lifecycle hook call (in rare cases), a null-guard, and
37
37
  `return presentation.render({ ...values, ...callbacks })`.
38
38
 
39
39
  No other members. No `@state`. No private handler fields. No lifecycle methods (`connectedCallback`, `updated`, `firstUpdated`,
40
40
  `disconnectedCallback`) except on app-entrypoint classes where the lifecycle concern is app-boot shaped but cannot be expressed as a hook
41
- (see `.claude/rules/hooks.md` for hook-first lifecycle).
41
+ (see `hooks.md` for hook-first lifecycle).
42
42
 
43
- ## `@property` whitelist
43
+ ## `@property` allowlist
44
44
 
45
45
  The allowed set depends on **class role**:
46
46
 
@@ -114,7 +114,7 @@ one-liners** above.
114
114
  ## Lifecycle goes through hooks
115
115
 
116
116
  Any need for mount/unmount/update behavior routes through a hook. **Reuse order, one hook per file, presentation vs binding placement, and
117
- testing notes:** `.claude/rules/hooks.md`.
117
+ testing notes:** `hooks.md`.
118
118
 
119
119
  Never inline lifecycle logic in the binding. App-entrypoint classes may override lifecycle methods when the concern is app-boot shaped
120
120
  (service wiring, ancestor lookup); even then, prefer a hook.
@@ -158,14 +158,14 @@ expose them by design; reaching for them means a missing computed.
158
158
 
159
159
  A binding element imports a sibling `*-presentation.ts` file that exports exactly `render` (required) and optionally `unlocalized`. Nothing
160
160
  else — no types, no styles, no helpers. Consumers derive the props type via `Parameters<typeof render>[0]`. Presentation stays hook-free by
161
- default; element hooks live here or in shared modules per `.claude/rules/hooks.md` and `.claude/rules/presentation.md`.
161
+ default; element hooks live here or in shared modules per `hooks.md` and `features/ui/presentation.md`.
162
162
 
163
163
  **For refactors whose stated scope does not include presentations**, the matching `*-presentation.ts` file must not change. Verify with
164
164
  `git diff --name-only | grep presentation`. If a scope-limited refactor requires a presentation signature change, stop and reconsider the
165
165
  binding-side helper — the scope fence exists because presentations are the public contract between binding and render output.
166
166
 
167
167
  Callback prop names use **verb** or **verbNoun** form. Never `on*` prefix. The binding-side variable matches the presentation prop name 1:1.
168
- See `.claude/rules/presentation.md` for full examples.
168
+ See `features/ui/presentation.md` for full examples.
169
169
 
170
170
  ## Size budget
171
171
 
@@ -32,8 +32,10 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
32
32
  - `<type-name>/<type-name>.ts` is the only public import surface
33
33
  - Export a single type alias: `export type <type-name> = <type>` (or `export type * from "./<type-name>-types.js"` when the type lives in a
34
34
  separate file)
35
- - Schema-based types: schema in `<type-name>-schema.ts`, named `schema` (not `<type-name>Schema`), derive with
36
- `Schema.ToType<typeof schema>`
35
+ - The type alias is **hand-authored** in `<type-name>.ts`. When a schema is needed it lives in `<type-name>-schema.ts` (or
36
+ `schema.ts` inside the folder), named `schema` (not `<type-name>Schema`), and is **written to match the hand-authored type and pinned
37
+ to it** with `Assert<Equal<Schema.ToType<typeof schema>, <TypeName>>>` — never the other way around. `Schema.ToType` appears only in
38
+ that assertion, never as the exported type.
37
39
  - Export namespace: `export * as <type-name> from "./public.js"`
38
40
  - In public.ts, re-export every public constant file
39
41
  - Filenames map deterministically to single export:
@@ -43,6 +45,26 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
43
45
  - Function files: name by purpose only (e.g., `add.ts`, `sub.ts`). **Do not prefix** with the type name — the folder provides the namespace
44
46
  context.
45
47
 
48
+ ## Constants file
49
+
50
+ Constants are the **one exception** to one-declaration-per-file. A namespace folder MAY have a single `constants.ts` exporting several
51
+ plain `const` literals (no functions, no types), re-exported through `public.ts` so callers reach them as `<TypeName>.<constant>`:
52
+
53
+ ```ts
54
+ // duration/constants.ts
55
+ export const minSeconds = 5;
56
+ export const maxSeconds = 300;
57
+ export const defaultSeconds = 30;
58
+
59
+ // duration/public.ts
60
+ export * from './constants.js';
61
+
62
+ // consumers: Duration.minSeconds, Duration.maxSeconds, Duration.defaultSeconds
63
+ ```
64
+
65
+ Function declarations remain **one per file** (each in its own purpose-named file). If a value needs logic rather than being a plain
66
+ literal, it is a function and does not belong in `constants.ts`.
67
+
46
68
  ## Anti-patterns (do not copy)
47
69
 
48
70
  - **`export namespace <TypeName> { ... }`** — Use `export * as <TypeName> from "./public.js"` instead.
@@ -63,18 +85,24 @@ patterns (e.g., inline `export namespace` in shared files) that do not conform
63
85
 
64
86
  ## Schema pattern example
65
87
 
88
+ The type is authored first and by hand; the schema (if/when needed) is written
89
+ to match it and pinned so it cannot drift.
90
+
66
91
  ```ts
67
- // src/types/player-mark/player-mark-schema.ts
68
- export const schema = { enum: ['X', 'O'] } as const;
92
+ // src/types/player-mark/player-mark.ts — HAND-AUTHORED, the source of truth
93
+ export type PlayerMark = 'X' | 'O';
94
+ export * as PlayerMark from './public.js';
95
+
96
+ // src/types/player-mark/player-mark-schema.ts — written to MATCH the type
97
+ import { Schema } from '@adobe/data/schema';
98
+ import type { Assert, Equal } from '@adobe/data/types';
99
+ import type { PlayerMark } from './player-mark.js';
100
+ export const schema = { type: 'string', enum: ['X', 'O'] } as const satisfies Schema;
101
+ // Compile-time pin: fails to build if schema and type diverge.
102
+ type _Pin = Assert<Equal<Schema.ToType<typeof schema>, PlayerMark>>;
69
103
 
70
104
  // src/types/player-mark/public.ts
71
105
  export { schema } from './player-mark-schema.js';
72
-
73
- // src/types/player-mark/player-mark.ts
74
- import { Schema } from '@adobe/data/schema';
75
- import { schema } from './player-mark-schema.js';
76
- export type PlayerMark = Schema.ToType<typeof schema>;
77
- export * as PlayerMark from './public.js';
78
106
  ```
79
107
 
80
108
  ## Example: `Point` type refactor
@@ -76,7 +76,7 @@ use `Parameters<typeof render>[0]`.
76
76
 
77
77
  ## Action callbacks (not events)
78
78
 
79
- Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples.
79
+ Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples.
80
80
 
81
81
  **Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments:
82
82
 
@@ -76,7 +76,7 @@ there.
76
76
 
77
77
  ## Action callbacks (not events)
78
78
 
79
- Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples and rationale.
79
+ Use **verb** or **verbNoun** names — never `on*` prefix. See `features/ui/presentation.md` for full examples and rationale.
80
80
 
81
81
  ---
82
82
 
@@ -91,7 +91,7 @@ view hosts. Multiple instances of the same element need a property to identify w
91
91
 
92
92
  ## Hooks
93
93
 
94
- Canonical rules (reuse order, one hook per file, binding vs presentation): `.claude/rules/hooks.md`.
94
+ Canonical rules (reuse order, one hook per file, binding vs presentation): `hooks.md`.
95
95
 
96
96
  Use hooks from `src/elements/hooks/` and `@adobe/data-lit` instead of `connectedCallback`/`disconnectedCallback`. Use `useEffect` for
97
97
  setup/teardown, `useKeyboardEvent` for single key events.
@@ -107,7 +107,7 @@ When creating or modifying a Lit element:
107
107
  3. Use a single `useObservableValues` in the binding element. Observe only minimal values; use `Observe.withDefault` for slow-resolving
108
108
  values.
109
109
  4. Pass observed values and action callbacks to presentation.
110
- 5. Keep presentation pure — no hooks (see `.claude/rules/hooks.md` for exceptions).
110
+ 5. Keep presentation pure — no hooks (see `hooks.md` for exceptions).
111
111
  6. Add `@property` only when entity binding requires it (multiple instances).
112
112
  7. Presentation exports only `render` (and localization bundles where appropriate). Binding element sets `static styles` from `*.css.ts`.
113
113
  8. Add `*-presentation.test.ts` for presentation when appropriate; do not unit test binding elements.
@@ -147,7 +147,7 @@ Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(user
147
147
  `observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually
148
148
  read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate
149
149
  (`@adobe/data/observe`'s `withDeduplicate` is reference-equality only). `observeSelectDeep` from a UI element is banned — let each element
150
- observe its own entity (see `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`).
150
+ observe its own entity (see `ecs.md`).
151
151
 
152
152
  ---
153
153
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adobe-data-ai",
3
- "version": "0.9.90",
3
+ "version": "0.9.92",
4
4
  "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
5
5
  "author": {
6
6
  "name": "Adobe"
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  AI architecture skills + rules for building applications with
4
4
  [`@adobe/data`](https://www.npmjs.com/package/@adobe/data) — a layered,
5
- spec-first feature structure (`data → ecs → ui`) kept honest by conformance
5
+ spec-first feature structure (`data → services → ui`) kept honest by conformance
6
6
  tests. Versioned in lockstep with the library: `@adobe/data-ai@x.y.z` targets
7
7
  `@adobe/data@x.y.z`.
8
8
 
package/bin/cli.mjs CHANGED
@@ -50,8 +50,8 @@ function notice(extra) {
50
50
 
51
51
  The files in this folder are installed and managed by the \`${PKG_NAME}\`
52
52
  package. This entire directory is **deleted and rebuilt** on every
53
- \`npx ${PKG_NAME} install\`, so any local edit here is silently discarded on the
54
- next reinstall or version upgrade.
53
+ \`npx ${PKG_NAME}@${VERSION} install\`, so any local edit here is silently discarded on
54
+ the next reinstall or version upgrade.
55
55
 
56
56
  To change them, edit upstream in the \`${PKG_NAME}\` package and publish a new
57
57
  version, then reinstall — do not modify these files in place.
@@ -66,10 +66,21 @@ function writeMeta(bundleDir, noticeBody, extraMeta) {
66
66
  );
67
67
  }
68
68
 
69
+ // Skills that author/evolve the bundle itself (the self-improvement loop and its
70
+ // rule review). They mutate or critique the managed files, so they belong only to
71
+ // the source repo — never a consumer install, where those files are read-only
72
+ // managed copies. Both also carry `disable-model-invocation: true`.
73
+ const MAINTAINER_ONLY = new Set(["meta-build", "review"]);
74
+
69
75
  function discoverSkills() {
70
76
  if (!existsSync(skillsSrc)) return [];
71
77
  return readdirSync(skillsSrc, { withFileTypes: true })
72
- .filter((d) => d.isDirectory() && existsSync(join(skillsSrc, d.name, "SKILL.md")))
78
+ .filter(
79
+ (d) =>
80
+ d.isDirectory() &&
81
+ !MAINTAINER_ONLY.has(d.name) &&
82
+ existsSync(join(skillsSrc, d.name, "SKILL.md")),
83
+ )
73
84
  .map((d) => d.name)
74
85
  .sort();
75
86
  }
@@ -111,6 +122,41 @@ function installRules(base) {
111
122
  return bundleDir;
112
123
  }
113
124
 
125
+ // A single managed rule (namespaced by its `adobe-data-ai-` filename prefix) telling
126
+ // an agent to (re)install the bundle when the skills are absent — pinned to THIS
127
+ // reviewed version, never `@latest`, so a drifted or compromised release cannot execute
128
+ // during install. Rewritten with the matching version on every install. It sits beside
129
+ // (not inside) the wiped bundle dir, so it survives and is simply overwritten.
130
+ function installBootstrap(base) {
131
+ const file = join(base, ".claude", "rules", `${BUNDLE}-bootstrap.md`);
132
+ mkdirSync(dirname(file), { recursive: true });
133
+ writeFileSync(
134
+ file,
135
+ `---
136
+ paths:
137
+ - '**/features/*/data/**/*.ts'
138
+ - '**/features/*/services/**/*.ts'
139
+ - '**/features/*/ui/**/*.ts'
140
+ ---
141
+
142
+ # ${PKG_NAME} skills — ensure installed
143
+
144
+ Before using an \`@adobe/data\` build workflow, confirm the skills are present: look for
145
+ a skill named \`build-feature\` (or \`build-application\`). If it is missing, install the
146
+ pinned, reviewed bundle, then re-check:
147
+
148
+ \`\`\`sh
149
+ npx ${PKG_NAME}@${VERSION} install
150
+ \`\`\`
151
+
152
+ Pin the version to the reviewed bundle (never \`@latest\`) so a compromised or drifted
153
+ release cannot execute during install. This file is regenerated with the matching
154
+ version on every install. Package: <https://www.npmjs.com/package/${PKG_NAME}>
155
+ `,
156
+ );
157
+ return file;
158
+ }
159
+
114
160
  function parseArgs(argv) {
115
161
  const positional = [];
116
162
  const flags = new Set();
@@ -176,11 +222,13 @@ function main() {
176
222
  const base = flags.has("global") ? homedir() : dir ? resolve(dir) : process.cwd();
177
223
  const skillsDir = installSkills(base, skills);
178
224
  const rulesDir = installRules(base);
225
+ const bootstrapFile = installBootstrap(base);
179
226
  const ruleCount = countRules(rulesSrc);
180
227
 
181
228
  process.stdout.write(`Installed ${PKG_NAME} v${VERSION}\n`);
182
229
  process.stdout.write(` ${skills.length} skills → ${skillsDir}\n`);
183
230
  process.stdout.write(` ${ruleCount} rules → ${rulesDir}\n`);
231
+ process.stdout.write(` bootstrap → ${bootstrapFile}\n`);
184
232
  }
185
233
 
186
234
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/data-ai",
3
- "version": "0.9.90",
3
+ "version": "0.9.92",
4
4
  "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,12 +29,12 @@
29
29
  "skills"
30
30
  ],
31
31
  "peerDependencies": {
32
- "@adobe/data": "0.9.90"
32
+ "@adobe/data": "0.9.92"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^5.8.3",
36
36
  "vitest": "^1.6.0",
37
- "@adobe/data": "0.9.90"
37
+ "@adobe/data": "0.9.92"
38
38
  },
39
39
  "license": "MIT",
40
40
  "scripts": {
@@ -9,7 +9,7 @@ output: app
9
9
  plugin, so all schemas coexist, persist, and sync while the base type stays decoupled.
10
10
  - The base reaches a peer only through a lazy element wrapper (dynamic import), so peers
11
11
  load on demand.
12
- - The entry point creates the live database from `FeatureDatabase.plugin` (the base
12
+ - The entry point creates the live database from `MainService.plugin` (the base
13
13
  feature's assembled database — see `features/services/main-service/index.md`) and mounts the root UI.
14
14
 
15
15
  See `features/index.md` (one-app-many-features) for the wiring rules.
@@ -8,15 +8,15 @@ output: feature
8
8
  Create the feature's `data/` layer (pure spec; depends on nothing but `@adobe/data`
9
9
  and other `data/` declarations):
10
10
 
11
- - one namespace folder per data type — `<type>/{<type>.ts, schema.ts, public.ts, <helper>.ts}`;
11
+ - one namespace folder per data type — `<type>/{<type>.ts, public.ts, <helper>.ts}` (+ `schema.ts` only when a runtime boundary needs it);
12
12
  - `data/state/` — the `State` aggregate plus its pure transforms/derivations.
13
13
 
14
14
  **Always create `data/state/`** — even for a shell feature whose only field is a
15
15
  boolean or enum. A feature with ECS resources/transactions but no `State` is
16
16
  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 = {}
17
17
 
18
- The schema is the single source of truth; the type derives from it (`Schema.ToType`).
18
+ The hand-authored type in `<type>.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<Equal<Schema.ToType<typeof schema>, <Type>>>` so it cannot drift. `Schema.ToType` is used only in that assertion, never as the exported type.
19
19
  Helpers are pure and unit-tested. Run this first — every other layer imports `data/`.
20
20
 
21
21
  The how is in the auto-loading rules: `features/data/index.md`, `features/data/state.md`, and
22
- `namespace.md`.
22
+ `global/namespace.md`.
@@ -13,4 +13,4 @@ and `create*` factories.
13
13
 
14
14
  Only if the feature talks to the outside world. Comes after `data/`, before `services/main-service/`.
15
15
 
16
- The how is in the auto-loading `features/services/index.md` rule (and `namespace.md`).
16
+ The how is in the auto-loading `features/services/index.md` rule (and `global/namespace.md`).
@@ -9,7 +9,10 @@ Skip if this feature doesn't contain or require systems.
9
9
  Most games that use a canvas will use systems but most applications will not.
10
10
 
11
11
  Create `services/main-service/system-database/system-database.ts`: `Database.Plugin.create({ extends:
12
- Database.Plugin.combine(ComputedDatabase.plugin, scheduler), systems })` where the `systems`
12
+ Database.Plugin.combine(<currentTop>.plugin, scheduler), systems })` — combine `scheduler` with the
13
+ feature's **current top** main-service layer (`ActionDatabase` / `ServiceDatabase` / `ComputedDatabase`,
14
+ whichever it built), never a hardcoded `ComputedDatabase`, so systems *and* any services/actions
15
+ compose into the one `MainService`. The `systems`
13
16
  map is declared **inline** (see `features/services/main-service/systems.md` — inline is required for `db` to be
14
17
  typed and for system-name inference; a `systems/` folder is optional, only for extracted
15
18
  per-frame body helpers).
@@ -5,7 +5,8 @@ input: feature
5
5
  output: feature
6
6
  ---
7
7
 
8
- Create `services/main-service/transaction-database/`: `transaction-database.ts` (extends `IndexDatabase`, adds
8
+ Create `services/main-service/transaction-database/`: `transaction-database.ts` (extends the
9
+ preceding layer — `IndexDatabase` if the feature built indexes, else `CoreDatabase` — adds
9
10
  `transactions` from `./transactions/index.js`) plus a `transactions/` folder — one mutation per
10
11
  file + barrel. Type the store param `CoreDatabase.Store` (entities/resources/archetypes) or
11
12
  `IndexDatabase.Store` (reads an index).
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: meta-build
3
+ disable-model-invocation: true
3
4
  description: Iteratively build an application or game phase-by-phase in clean subagents, reviewing each phase and refining the build-* skills / rules until each is optimal, then summarizing.
4
5
  input: a prompt describing the application or game to build
5
6
  options:
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: review
3
+ disable-model-invocation: true
3
4
  description: Review built feature/application output against the @adobe/data feature rules; report whether it is optimal and where any fix belongs. Read-only.
4
5
  input: a path to built output (a layer, a feature, or a whole app)
5
6
  output: a review — OPTIMAL, or a list of issues each tagged code | skill | rule