@adobe/data-ai 0.9.82 → 0.9.84

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 (59) hide show
  1. package/.claude/rules/README.md +42 -0
  2. package/.claude/rules/archetypes.md +60 -0
  3. package/.claude/rules/cohesion.md +66 -0
  4. package/.claude/rules/data-modelling.md +71 -0
  5. package/.claude/rules/element.md +130 -0
  6. package/.claude/rules/features/data/index.md +35 -0
  7. package/.claude/rules/features/data/state.md +54 -0
  8. package/.claude/rules/features/ecs/actions.md +32 -0
  9. package/.claude/rules/features/ecs/archetypes.md +46 -0
  10. package/.claude/rules/features/ecs/components.md +41 -0
  11. package/.claude/rules/features/ecs/computed.md +26 -0
  12. package/.claude/rules/features/ecs/conformance.md +69 -0
  13. package/.claude/rules/features/ecs/index.md +179 -0
  14. package/.claude/rules/features/ecs/indexes.md +63 -0
  15. package/.claude/rules/features/ecs/resources.md +28 -0
  16. package/.claude/rules/features/ecs/services.md +24 -0
  17. package/.claude/rules/features/ecs/systems.md +133 -0
  18. package/.claude/rules/features/ecs/transactions.md +45 -0
  19. package/.claude/rules/features/index.md +95 -0
  20. package/.claude/rules/features/services/index.md +34 -0
  21. package/.claude/rules/features/ui/index.md +31 -0
  22. package/.claude/rules/function-references.md +35 -0
  23. package/.claude/rules/lazy-element.md +127 -0
  24. package/.claude/rules/namespace.md +179 -0
  25. package/.claude/rules/plugin-modelling.md +170 -0
  26. package/.claude/rules/presentation.md +83 -0
  27. package/.claude/rules/type-casts.md +94 -0
  28. package/.claude-plugin/plugin.json +1 -1
  29. package/README.md +39 -81
  30. package/bin/cli.mjs +81 -26
  31. package/package.json +9 -2
  32. package/skills/build-actions/SKILL.md +14 -0
  33. package/skills/build-app-entry/SKILL.md +15 -0
  34. package/skills/build-application/SKILL.md +10 -0
  35. package/skills/build-computed/SKILL.md +14 -0
  36. package/skills/build-core-database/SKILL.md +24 -0
  37. package/skills/build-data/SKILL.md +18 -0
  38. package/skills/build-feature/SKILL.md +21 -0
  39. package/skills/build-game/SKILL.md +19 -0
  40. package/skills/build-indexes/SKILL.md +14 -0
  41. package/skills/build-service-database/SKILL.md +16 -0
  42. package/skills/build-services/SKILL.md +14 -0
  43. package/skills/build-systems/SKILL.md +24 -0
  44. package/skills/build-transactions/SKILL.md +15 -0
  45. package/skills/build-ui/SKILL.md +13 -0
  46. package/skills/meta-build/SKILL.md +43 -0
  47. package/skills/review/SKILL.md +25 -0
  48. package/skills/structure/SKILL.md +7 -0
  49. package/skills/x-alpha/SKILL.md +7 -0
  50. package/skills/x-beta/SKILL.md +7 -0
  51. package/skills/x-double/SKILL.md +7 -0
  52. package/skills/x-execute/SKILL.md +16 -0
  53. package/skills/x-log/SKILL.md +12 -0
  54. package/skills/x-pow/SKILL.md +9 -0
  55. package/skills/x-subagent/SKILL.md +8 -0
  56. package/skills/x-trace/SKILL.md +11 -0
  57. package/skills/x-validate/SKILL.md +67 -0
  58. package/skills/bar/SKILL.md +0 -8
  59. package/skills/foo/SKILL.md +0 -8
@@ -0,0 +1,42 @@
1
+ # Rules
2
+
3
+ Distributable Claude project rules for data-oriented architecture. Copy or
4
+ symlink this directory into your project's `.claude/rules/` layout; Claude
5
+ discovers `.md` files recursively and, for rules carrying a `paths:`
6
+ frontmatter glob, injects them only when a matching file is in context.
7
+
8
+ ## Layout mirrors the feature-folder structure
9
+
10
+ The `features/` subtree mirrors the four feature layers one-to-one, so the
11
+ guidance for a folder lives at the same path shape as the code it governs. A
12
+ folder's own `index.md` holds just enough to understand that folder; each
13
+ child gets its own file (recursing wherever the source tree does):
14
+
15
+ ```
16
+ features/
17
+ index.md # the feature layering as a whole (ui → ecs → services → data)
18
+ data/
19
+ index.md # data-type namespaces (foundation)
20
+ state.md # the State aggregate: pure transforms & derivations
21
+ services/index.md # service interfaces + service types
22
+ ecs/
23
+ index.md # the ECS layer + layered database plugins
24
+ components.md resources.md archetypes.md
25
+ computed.md indexes.md transactions.md services.md actions.md
26
+ systems.md conformance.md
27
+ ui/index.md # UI (points to element / presentation / lazy-element file rules)
28
+ ```
29
+
30
+ Alongside `features/`, the rules-root `.md` files hold the **cross-cutting**
31
+ patterns the feature rules reference — `namespace.md`, `data-modelling.md`,
32
+ `type-casts.md`, `function-references.md`, `cohesion.md`, `archetypes.md` (row
33
+ iteration), `plugin-modelling.md`, and the UI file rules (`element.md`,
34
+ `lazy-element.md`, `presentation.md`). They live here so the bundle is self-contained; this repo
35
+ symlinks them into its own `.claude/rules/` (as it does `features/`).
36
+
37
+ Each rule's `paths:` glob is scoped to `**/features/*/<layer>/…`, so these
38
+ rules apply only once an application opts into the feature-folder pattern
39
+ (source organized under `features/<name>/`). Editing
40
+ `features/<name>/ecs/<layer>-database/components/foo.ts` pulls in both
41
+ `ecs/index.md` (the layer overview) and `ecs/components.md` (the specifics) —
42
+ at creation and at every later edit.
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: Authoring rules for ECS systems that iterate archetype rows.
3
+ globs: "**/*.ts"
4
+ ---
5
+
6
+ # Archetypes — iteration rules
7
+
8
+ ## Express selection in the query, not the loop
9
+
10
+ `queryArchetypes(include, { exclude })` accepts both required and excluded
11
+ component lists. Use them. Don't query a wider set and then skip rows or
12
+ archetypes with an `if`.
13
+
14
+ ```ts
15
+ // ❌ post-filter
16
+ for (const arch of db.store.queryArchetypes(["position", "rotation"])) {
17
+ if (arch.components.has("_worldMatrix")) continue;
18
+ ...
19
+ }
20
+
21
+ // ✅ declarative
22
+ for (const arch of db.store.queryArchetypes(
23
+ ["position", "rotation"],
24
+ { exclude: ["_worldMatrix"] },
25
+ )) {
26
+ ...
27
+ }
28
+ ```
29
+
30
+ ## When every row migrates out, iterate tail → head
31
+
32
+ Archetypes are densely packed. Removing or migrating a row that isn't the
33
+ last one triggers a hole-fill: the tail row is moved into the gap.
34
+ Iterating `0 → rowCount-1` while migrating every row pays this cost on
35
+ every iteration. Iterating `rowCount-1 → 0` means each removal is from
36
+ the tail — no shift, indices ahead of the cursor stay valid.
37
+
38
+ ```ts
39
+ // ❌ shifts the tail into every hole, and the snapshot allocation is
40
+ // only there to survive the shifts.
41
+ const ids = [...];
42
+ for (let i = 0; i < arch.rowCount; i++) ids[i] = arch.columns.id.get(i);
43
+ for (const id of ids) db.store.update(id, { _worldMatrix: Mat4x4.identity });
44
+
45
+ // ✅ no shifts, no allocation
46
+ for (let i = arch.rowCount - 1; i >= 0; i--) {
47
+ db.store.update(arch.columns.id.get(i), { _worldMatrix: Mat4x4.identity });
48
+ }
49
+ ```
50
+
51
+ If only *some* rows migrate (filter inside the loop), snapshot the ids
52
+ you'll touch — forward iteration is fine because rows you don't touch
53
+ stay put.
54
+
55
+ ## Don't snapshot what the query already filters
56
+
57
+ A snapshot of "all entity ids in this archetype right now" is only
58
+ needed when forward iteration would invalidate. Reverse iteration
59
+ removes the need; an `exclude` clause removes the need to look at rows
60
+ that don't qualify in the first place.
@@ -0,0 +1,66 @@
1
+ ---
2
+ paths:
3
+ - 'packages/**/*.ts'
4
+ - 'packages/**/*.tsx'
5
+ ---
6
+
7
+ # Cohesion — make related code share a name
8
+
9
+ Code that serves one purpose should be reachable through one name.
10
+ Cohesion that exists only in the reader's head is a smell — make it
11
+ visible by extracting a function, a hook, or a folder.
12
+
13
+ ## Within a file (statement cohesion)
14
+
15
+ When adjacent statements share a single purpose — same dependencies,
16
+ same lifetime, same conceptual job — collapse them behind one named
17
+ local helper. The surrounding scope should read at one level of
18
+ abstraction; the lower-level mechanics live inside the helper.
19
+
20
+ This is *Composed Method* / *SLAP*. The fix is local: a private
21
+ function or local hook in the same file, no public API, no claim of
22
+ reusability. Naming makes the cohesion structural instead of leaving
23
+ it for the reader to assemble.
24
+
25
+ ```ts
26
+ // Smell: two statements that are one concept ("owned controller")
27
+ const controller = useMemo(() => createController(svc, cfg), [svc, cfg]);
28
+ useEffect(() => () => controller.dispose(), [controller]);
29
+
30
+ // Fix: cohesion has a name
31
+ const controller = useDisposableController(svc, cfg);
32
+ ```
33
+
34
+ ## Across files (folder cohesion)
35
+
36
+ One public export per file. Private declarations inside the file are
37
+ fine — they are implementation detail and are tested indirectly
38
+ through the public export. Only the public export gets a sibling
39
+ `*.test.ts`.
40
+
41
+ **Exception: barrels.** An `index.ts` (and a namespace's `public.ts`)
42
+ exists precisely to re-export a folder's public surface, so it carries
43
+ many exports. That is the barrel's whole job — it declares nothing of its
44
+ own, only `export … from "./…"`. The one-export rule governs files that
45
+ *declare*, not files that *re-export*.
46
+
47
+ When a public file wants *helper sibling files*, promote the cluster
48
+ into a folder named after the concept. Helpers never sit at peer
49
+ level with unrelated public files; the folder is the new boundary.
50
+
51
+ ```
52
+ ok smell fix
53
+ foo.ts foo.ts foo/
54
+ foo.test.ts foo-helper.ts foo.ts
55
+ bar.ts foo.test.ts foo-helper.ts
56
+ bar.test.ts bar.ts foo.test.ts
57
+ bar.test.ts bar.ts
58
+ bar.test.ts
59
+ ```
60
+
61
+ ## Heuristic
62
+
63
+ For any pair of symbols — two statements, two files — ask: "is their
64
+ relationship visible in the structure, or only in the reader's
65
+ head?" If the latter, give the cohesion a name: a function, a hook,
66
+ or a folder.
@@ -0,0 +1,71 @@
1
+ ---
2
+ ---
3
+
4
+ # Data modelling — locality of knowledge
5
+
6
+ A type's identity (its members) is named only inside its own folder.
7
+ Every other module either accepts members as parameters, narrows broader
8
+ input with the type's `is` guard, or iterates `Type.values`.
9
+
10
+ This applies to any closed set: enums, role tags, status codes,
11
+ discriminated unions.
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ // types/http-method/schema.ts
17
+ export const schema = { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] }
18
+ as const satisfies Schema;
19
+
20
+ // In an unrelated request-counter plugin:
21
+
22
+ // ❌ Members spelled outside the type folder
23
+ const counts = { GET: 0, POST: 0, PUT: 0, DELETE: 0 };
24
+ function record(m: string) {
25
+ if (m === "GET" || m === "POST" || m === "PUT" || m === "DELETE") counts[m]++;
26
+ }
27
+
28
+ // ✅ HttpMethod owns its identity; this file owns the counter
29
+ const counts: Partial<Record<HttpMethod, number>> = {};
30
+ function record(m: unknown) {
31
+ if (!HttpMethod.is(m)) return;
32
+ counts[m] = (counts[m] ?? 0) + 1;
33
+ }
34
+ ```
35
+
36
+ ## Shape of keyed collections
37
+
38
+ - `Record<EnumKey, T>` — every key required at all times. Default lists
39
+ every member. Use only when state is genuinely dense.
40
+ - `Partial<Record<EnumKey, T>>` — keys appear and disappear over the
41
+ lifecycle. Default `{}`. Use for sparse / per-actor state.
42
+
43
+ If the value can be meaningfully absent for a present member (a peer
44
+ exists but hasn't moved yet, an entry hasn't loaded), use `Partial`.
45
+ Reserve `Record` for descriptors where every member must have a value.
46
+
47
+ Sibling fields (`{ countGET, countPOST, countPUT, countDELETE }`) duplicate
48
+ the type's identity into field names — always replace with a keyed
49
+ collection.
50
+
51
+ ## Per-member variation lives with the type
52
+
53
+ When something *genuinely differs* per member (colours, labels, icons),
54
+ the descriptor lives in the type's folder as a `Record<EnumKey, V>`,
55
+ named for its purpose:
56
+
57
+ ```ts
58
+ // types/http-method/method-color.ts
59
+ export const methodColor: Record<HttpMethod, string> = {
60
+ GET: "#06f", POST: "#0a0", PUT: "#fa0", DELETE: "#f00",
61
+ };
62
+ ```
63
+
64
+ A per-member string built at the call site (`"req-${method.toLowerCase()}"`,
65
+ `<HttpMethod>` BEM modifiers, SVG ids) is the same leak in derivation form:
66
+ the *mapping rule* belongs in the type's folder, not at the call site.
67
+
68
+ A static stylesheet that lists `.row--get { … } .row--post { … } …` is
69
+ the same leak in CSS form. Drive per-member visuals from the descriptor
70
+ (inline `style`, a CSS custom property, or a generated stylesheet) so
71
+ the enum is not re-encoded at the rendering layer.
@@ -0,0 +1,130 @@
1
+ ---
2
+ paths:
3
+ - 'packages/**/*-element.ts'
4
+ ---
5
+
6
+ # Container element authoring
7
+
8
+ A *container element* is a UI element whose only job is to bridge an
9
+ ECS / data service to a pure presentation function: subscribe to state,
10
+ hand it down, wire actions back up. It owns no business logic.
11
+
12
+ This rule is framework-agnostic. The framework-specific bindings are in
13
+ the table at the bottom.
14
+
15
+ > **Out of scope:** sibling `*presentation.ts`, `*.css.ts`, `*.test.ts`,
16
+ > and `*.stories.ts` files in the same folder are governed by their own
17
+ > rules. Pure UI primitives that take all of their inputs through props
18
+ > (no service subscription) are not container elements either.
19
+
20
+ ## The discipline
21
+
22
+ A container element fits on screen. Its body contains:
23
+
24
+ 1. Optional styles assignment.
25
+ 2. Zero or more locator-shaped inputs (`entityId`, `userId`, `route`)
26
+ that identify *where* to load this instance's data.
27
+ 3. One subscription read of the bound data service.
28
+ 4. Action callbacks — one-line invocations of service transactions or
29
+ actions, never logic.
30
+ 5. One delegation to the matching presentation function.
31
+
32
+ That is the entire surface. No internal state, no event handlers, no
33
+ lifecycle overrides, no derivation math, no timers, no event emitters,
34
+ no broadcast channels, no synchronous data reads.
35
+
36
+ ### Bootstrap exception
37
+
38
+ A *bootstrap container* — one that constructs its service rather than
39
+ consuming a pre-existing one (e.g. an element that owns a WebRTC
40
+ handshake before mounting a child app) — may also accept dependency-
41
+ injection props that configure that construction (the plugin to use,
42
+ a child tag name, mapping functions). Those props feed a sibling
43
+ controller invoked from a `useMemo` / `useEffect` hook; they never
44
+ appear inside render branching. Use sparingly.
45
+
46
+ ### Imperative / canvas containers
47
+
48
+ A container whose "presentation" is an imperative surface (a `<canvas>` /
49
+ WebGL context) can't render reactively — a real-time feature mutates
50
+ component columns in place each frame, which fires no observers. Such a
51
+ container may:
52
+
53
+ - run a `requestAnimationFrame` draw loop inside a `useEffect` (cancelled in
54
+ its cleanup) that reads the live store synchronously and calls a pure
55
+ `draw(ctx, scene)`;
56
+ - read the **full** database for those reads via the base class's protected
57
+ accessor (`this.database` on a Lit `DatabaseElement`) — never cast the
58
+ restricted `service` back to the full surface;
59
+ - hold ephemeral input-hardware state (a held-key `useRef`, pointer state)
60
+ gathered by `useWindowEvent` and forwarded to a transaction.
61
+
62
+ The discipline still holds otherwise: no game/derivation logic (it lives in
63
+ `data/` and systems), the draw function is pure, and the reactive HUD around
64
+ the canvas is a normal subscribe-and-delegate presentation.
65
+
66
+ ## Subscription reads must be raw
67
+
68
+ Each value in the subscription block is a direct read of a service
69
+ observable or computed — no `.map`, `.filter`, no inline composition.
70
+ If derivation is needed, it lives as a service computed; if used only
71
+ by this element, define the computed in a sibling file and import it.
72
+
73
+ ## Action callbacks are one-liners
74
+
75
+ Each callback assembles arguments and invokes one service action or
76
+ transaction. No clamping, normalisation, branching, or shape-building.
77
+ Logic belongs in the action / transaction layer.
78
+
79
+ Transactions and actions are pure, already-bound functions — when a
80
+ callback would just forward the same arguments to one of them, pass the
81
+ function **by reference**, don't wrap it in an identity arrow:
82
+
83
+ ```ts
84
+ // ❌ identity wrapper — adds nothing
85
+ setOperation: (op) => this.service.transactions.setOperation(op),
86
+ // ✅ same signature, pass it through
87
+ setOperation: this.service.transactions.setOperation,
88
+ ```
89
+
90
+ Only write an arrow when you genuinely adapt the call (drop an event
91
+ arg, reorder, supply a captured locator).
92
+
93
+ ## Lifecycle goes through hooks
94
+
95
+ Mount / unmount / update behaviour routes through a hook abstraction
96
+ (`useEffect`, `useFocus`, etc.) called inside the render body — never
97
+ through a framework lifecycle method (`connectedCallback`, `componentDidMount`,
98
+ `onMount`).
99
+
100
+ ## Action returns are not consumed
101
+
102
+ Actions are fire-and-forget. If a caller needs a result, that's a
103
+ transaction the binding invokes directly, *or* a new observable to
104
+ subscribe to — never a return value awaited in render.
105
+
106
+ ## Deletion test
107
+
108
+ Strip the file to its essentials. What remains should be: optional
109
+ styles, locator props, one subscription call, callbacks, and one render
110
+ delegation. Anything else is a violation — find the missing computed,
111
+ transaction, or hook.
112
+
113
+ ## Framework bindings
114
+
115
+ | Framework | Container is | Subscription hook | Locator inputs | Render delegation |
116
+ | --------- | ------------------------ | --------------------- | -------------------- | ------------------------------------------------ |
117
+ | Lit | `DatabaseElement<P>` | `useObservableValues` | `@property` only | `presentation.render({ ...values, ...callbacks })` |
118
+ | React | function component | `useObservableValues` | function arguments | `<Presentation {...values} {...callbacks} />` |
119
+ | Solid | component function | framework-specific | function arguments | `<Presentation {...values} {...callbacks} />` |
120
+
121
+ Lit-only: `static styles = styles` from a sibling `*.css.ts`. `@property`
122
+ exists *only* for locator inputs — never for state, never for derived
123
+ values, never for parent-forwarded flags. The `P` in `DatabaseElement<P>` (and
124
+ the `get plugin()` it returns) is the surface the element *consumes* — never the
125
+ varying topmost-layer name (see `features/ecs/index.md`): `typeof
126
+ FeatureDatabase.plugin` for a self-contained feature. **Exception:** an element
127
+ meant to be *extended* — injected with various databases that build on this
128
+ feature (a peer app that adds an agent, a p2p/presence build) — types on the
129
+ minimal base layer it consumes (e.g. `ComputedDatabase.plugin`) so every
130
+ extending database is assignable.
@@ -0,0 +1,35 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/data/**/*.ts'
4
+ ---
5
+
6
+ # data/ — the data model
7
+
8
+ The foundation layer: the feature's **data types**. A data type is a
9
+ readonly, JSON-serializable value suitable for persistence and for
10
+ communication over the wire — no functions, no handles, just plain data.
11
+ It depends on nothing but `@adobe/data` and other `data/` declarations, and
12
+ needs no knowledge of anything built on top of it.
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:
16
+
17
+ ```
18
+ 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`
21
+ public.ts # re-exports schema + helpers
22
+ is.ts values.ts opponent.ts … # one pure helper per file
23
+ ```
24
+
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.
29
+ - 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.
32
+
33
+ One folder is special: **`data/state/`** holds the feature's single `State`
34
+ aggregate and its pure transforms/derivations (its own rule, `state.md`);
35
+ the individual type folders here are its building blocks.
@@ -0,0 +1,54 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/data/state/**/*.ts'
4
+ ---
5
+
6
+ # data/state/ — the State specification
7
+
8
+ `State` is the whole feature modelled as **one immutable object** — the
9
+ pure, fully-tested source of truth for the feature. Modelling everything as
10
+ a single value keeps each transform a small, trivially-testable function of
11
+ state: collections of entity sub-types plus scalar fields.
12
+
13
+ ```ts
14
+ // state.ts — the aggregate, plus the namespace of transforms/derivations.
15
+ export type State = {
16
+ readonly todos: readonly Todo[]; // a collection of entity sub-types
17
+ readonly displayCompleted: boolean; // a scalar field
18
+ };
19
+ export * as State from "./public.js";
20
+ ```
21
+
22
+ ## Transforms — one per file, `(state, …args) => state`
23
+
24
+ - **Pure.** No I/O, no framework, no mutation — return a new value.
25
+ - **Narrow in, same shape out.** Write on the smallest slice the transform
26
+ needs and keep the input generic over that slice so it lifts to
27
+ full-state-in / full-state-out:
28
+
29
+ ```ts
30
+ export const playMove = <T extends Pick<State, "board" | "firstPlayer">>(
31
+ state: T,
32
+ input: PlayMoveArgs,
33
+ ): T => { /* … return { ...state, board: … } */ };
34
+ ```
35
+
36
+ A whole-`State` transform (`restartGame(state: State): State`) is fine when
37
+ it genuinely touches everything.
38
+ - Args may be **narrowed or omitted** (`toggleDisplayCompleted(state)`).
39
+ - Guard and **return `state` unchanged** on a no-op / illegal input rather
40
+ than throwing — this keeps transforms idempotent under repeated application.
41
+ - Each transform has a sibling `*.test.ts`; performance is irrelevant here,
42
+ correctness is everything. Keep its cases in a sibling `<transform>.cases.ts`
43
+ typed `ConformanceCase<Args>[]` (the shared `conformance-case.ts` type) —
44
+ spec-owned truth the matching ecs conformance test imports unchanged (see
45
+ `ecs/conformance.md`). Author `before`/`after` as full `State`
46
+ (`{ ...State.create(), …overrides }`); the generic-slice signature lets them
47
+ flow through. Tolerant full-`State` equality is the shared
48
+ `expect-state-matches.ts`, alongside the cases here.
49
+
50
+ ## Derivations — `(state) => value`
51
+
52
+ Pure selectors (`visibleTodos(state)`). Sub-type math (a winner, a status)
53
+ lives on the relevant `data/<type>` namespace; `state/` only composes over
54
+ the whole aggregate.
@@ -0,0 +1,32 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/actions/**/*.ts'
4
+ ---
5
+
6
+ # ecs/actions/ — async orchestration
7
+
8
+ One action per file: a function taking the **whole database** as its first
9
+ argument and pure `data/` args. Actions orchestrate anything *outside* a
10
+ single transaction — awaiting a `services/` port, sequencing calls, deriving
11
+ timing — and then commit the result through a transaction.
12
+
13
+ ```ts
14
+ import type { ServiceDatabase } from "../../service-database/service-database.js";
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
19
+ };
20
+ ```
21
+
22
+ - Type the `db` parameter on the lowest database layer exposing what the
23
+ action touches — usually `ServiceDatabase` (services **and**
24
+ transactions). Never the action layer itself (that would be a cycle).
25
+ - **Call at most one transaction** per action, so undo/redo stays one step
26
+ per operation.
27
+ - **Fire-and-forget.** An action's return value is not consumed; results flow
28
+ back through observables. `db.services.*` calls are `void` or
29
+ awaited-internally, never surfaced to the caller.
30
+ - Do the outside-world work here: await/sequence `services/` calls, and if a
31
+ slow call needs timing, compute it here around the call.
32
+ - An `index.ts` barrel feeds the `actions` plugin facet.
@@ -0,0 +1,46 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/core-database/archetypes.ts'
4
+ ---
5
+
6
+ # core-database/archetypes.ts — the feature's archetypes
7
+
8
+ An archetype is a named, ordered set of component keys — a kind of entity. One
9
+ file, one export: `archetypes`, built with `Database.archetypes(components, …)`,
10
+ which validates every key against the component map and preserves each
11
+ archetype's literal tuple (so no per-archetype `as const satisfies`):
12
+
13
+ ```ts
14
+ import { Database } from "@adobe/data/ecs";
15
+ import { components } from "./components.js";
16
+
17
+ export const archetypes = Database.archetypes(components, {
18
+ Todo: ["todo", "name", "complete", "order", "dragPosition", "assignees"],
19
+ });
20
+ ```
21
+
22
+ - `UpperCase` archetype names — an archetype names a *shape*, the way a class
23
+ name does.
24
+ - Archetypes may **span scopes**: a document entity can include an ephemeral
25
+ `session` column (e.g. a live `dragPosition` slot) — the `components` map
26
+ already merges every scope, so keys from any scope validate.
27
+ - Archetypes are a packing / iteration convenience, **not** part of the
28
+ serialized data model.
29
+
30
+ ## Drift guard against the data-type
31
+
32
+ An archetype that mirrors a `data/` row type carries a **non-exported**
33
+ compile-time check — infer the row shape from the archetype and assert it equals
34
+ that data-type — so the two can't drift apart:
35
+
36
+ ```ts
37
+ import { Schema } from "@adobe/data/schema";
38
+ import type { Assert, Equal } from "@adobe/data/types";
39
+ import type { PlacedMark } from "../../data/placed-mark/placed-mark.js";
40
+
41
+ const _schema = Schema.fromArchetype(components, archetypes.PlacedMark);
42
+ type _Check = Assert<Equal<Schema.ToType<typeof _schema>, PlacedMark>>;
43
+ ```
44
+
45
+ Iterating archetype **rows** at runtime is a separate concern — see the
46
+ rules-root `archetypes.md`.
@@ -0,0 +1,41 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/core-database/components.ts'
4
+ ---
5
+
6
+ # core-database/components.ts — the feature's components
7
+
8
+ One file, one export: `components`, built with `Database.components(...)`. A
9
+ component binds a `data/` type into ECS column storage, grouped by schema scope.
10
+ The scope groups are optional — declare only the ones you use.
11
+
12
+ Bind each schema straight from its `data/` type (or a primitive schema); don't
13
+ author shapes here. Component entries are any `Schema` — no `default` required
14
+ (that's resources):
15
+
16
+ ```ts
17
+ import { Database } from "@adobe/data/ecs";
18
+ import { Boolean, True } from "@adobe/data/schema";
19
+ import { PlayerMark } from "../../data/player-mark/player-mark.js";
20
+ import { DragPosition } from "../../data/drag-position/drag-position.js";
21
+
22
+ export const components = Database.components({
23
+ document: { mark: PlayerMark.schema, complete: Boolean.schema },
24
+ session: { dragPosition: DragPosition.schema },
25
+ });
26
+ ```
27
+
28
+ - A component name may differ from the type it stores (`position` of a `Vec3`,
29
+ `mark` of a `PlayerMark`) — the field name describes the role, the schema the
30
+ shape.
31
+ - **Scope = which group it sits in** (see `ecs/index.md` for the four scopes and
32
+ their flags). `document` is the common case; hover a scope key for its
33
+ meaning. Never write `nonShared` / `nonPersistent` by hand — the group applies
34
+ them.
35
+ - **Shared cross-feature columns live in `data/`** and go in `document` (which
36
+ preserves schema identity) so `combinePlugins` dedupes them.
37
+ - Struct vs. object storage and schema selection are properties of the `data/`
38
+ type, decided there.
39
+ - Don't infer entity type based upon component membership.
40
+ Consider using a const: true "tag" component or a type/kind enumerated value if needed.
41
+ Note that *many* good ecs designs don't care the type of the entity and only act on components.
@@ -0,0 +1,26 @@
1
+ ---
2
+ paths:
3
+ - '**/features/*/ecs/**/computed/**/*.ts'
4
+ ---
5
+
6
+ # database/computed/ — derived observable values
7
+
8
+ One derived value per file: a `cached` function of a database layer that
9
+ returns an `Observe` of state projected through pure `data/` helpers.
10
+ Derivation logic itself lives in `data/`; a computed only wires a db
11
+ observable to it.
12
+
13
+ ```ts
14
+ import { cached } from "@adobe/data/cache";
15
+ import { Observe } from "@adobe/data/observe";
16
+ import { BoardState } from "../../../data/board-state/board-state.js";
17
+ import type { IndexDatabase } from "../../index-database/index-database.js";
18
+
19
+ export const status = cached((db: IndexDatabase) =>
20
+ Observe.withFilter(db.observe.resources.board, BoardState.deriveStatus),
21
+ );
22
+ ```
23
+
24
+ Type the parameter on the lowest database layer that exposes what it
25
+ reads. An `index.ts` barrel re-exports every computed; `computed-database.ts`
26
+ registers it under the `computed` facet.