@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.
- package/.claude/rules/README.md +42 -0
- package/.claude/rules/archetypes.md +60 -0
- package/.claude/rules/cohesion.md +66 -0
- package/.claude/rules/data-modelling.md +71 -0
- package/.claude/rules/element.md +130 -0
- package/.claude/rules/features/data/index.md +35 -0
- package/.claude/rules/features/data/state.md +54 -0
- package/.claude/rules/features/ecs/actions.md +32 -0
- package/.claude/rules/features/ecs/archetypes.md +46 -0
- package/.claude/rules/features/ecs/components.md +41 -0
- package/.claude/rules/features/ecs/computed.md +26 -0
- package/.claude/rules/features/ecs/conformance.md +69 -0
- package/.claude/rules/features/ecs/index.md +179 -0
- package/.claude/rules/features/ecs/indexes.md +63 -0
- package/.claude/rules/features/ecs/resources.md +28 -0
- package/.claude/rules/features/ecs/services.md +24 -0
- package/.claude/rules/features/ecs/systems.md +133 -0
- package/.claude/rules/features/ecs/transactions.md +45 -0
- package/.claude/rules/features/index.md +95 -0
- package/.claude/rules/features/services/index.md +34 -0
- package/.claude/rules/features/ui/index.md +31 -0
- package/.claude/rules/function-references.md +35 -0
- package/.claude/rules/lazy-element.md +127 -0
- package/.claude/rules/namespace.md +179 -0
- package/.claude/rules/plugin-modelling.md +170 -0
- package/.claude/rules/presentation.md +83 -0
- package/.claude/rules/type-casts.md +94 -0
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +39 -81
- package/bin/cli.mjs +81 -26
- package/package.json +9 -2
- package/skills/build-actions/SKILL.md +14 -0
- package/skills/build-app-entry/SKILL.md +15 -0
- package/skills/build-application/SKILL.md +10 -0
- package/skills/build-computed/SKILL.md +14 -0
- package/skills/build-core-database/SKILL.md +24 -0
- package/skills/build-data/SKILL.md +18 -0
- package/skills/build-feature/SKILL.md +21 -0
- package/skills/build-game/SKILL.md +19 -0
- package/skills/build-indexes/SKILL.md +14 -0
- package/skills/build-service-database/SKILL.md +16 -0
- package/skills/build-services/SKILL.md +14 -0
- package/skills/build-systems/SKILL.md +24 -0
- package/skills/build-transactions/SKILL.md +15 -0
- package/skills/build-ui/SKILL.md +13 -0
- package/skills/meta-build/SKILL.md +43 -0
- package/skills/review/SKILL.md +25 -0
- package/skills/structure/SKILL.md +7 -0
- package/skills/x-alpha/SKILL.md +7 -0
- package/skills/x-beta/SKILL.md +7 -0
- package/skills/x-double/SKILL.md +7 -0
- package/skills/x-execute/SKILL.md +16 -0
- package/skills/x-log/SKILL.md +12 -0
- package/skills/x-pow/SKILL.md +9 -0
- package/skills/x-subagent/SKILL.md +8 -0
- package/skills/x-trace/SKILL.md +11 -0
- package/skills/x-validate/SKILL.md +67 -0
- package/skills/bar/SKILL.md +0 -8
- 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.
|