@adobe/data-ai 0.9.83 → 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,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/services/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# services/ — async capability contracts
|
|
7
|
+
|
|
8
|
+
Async ports: processing, persistence, observation, generation. A service
|
|
9
|
+
is the boundary between pure feature code and the outside world, which is
|
|
10
|
+
why its members are async — enabling cross-process portability and lazy
|
|
11
|
+
loading (`AsyncDataService.createLazy`).
|
|
12
|
+
|
|
13
|
+
This layer holds **service interfaces**, one per `<name>-service/` folder;
|
|
14
|
+
the export and folder both carry the `-service` suffix. Each is a
|
|
15
|
+
namespace folder (`namespace.md`).
|
|
16
|
+
|
|
17
|
+
- The contract is an `interface` — the only place `interface` is used in
|
|
18
|
+
the codebase — validated immediately with
|
|
19
|
+
`Assert<AsyncDataService.IsValid<typeof MyService>>`.
|
|
20
|
+
- Members are async only: `void | Promise<T> | AsyncGenerator<T> | Observe<T>`.
|
|
21
|
+
- Provide `create*` factories.
|
|
22
|
+
|
|
23
|
+
## Where the I/O types live
|
|
24
|
+
|
|
25
|
+
Most input/output types belong to a single service — declare them **on
|
|
26
|
+
that service's namespace** (`MyService.SomeInput`) and expose them only
|
|
27
|
+
when something external actually references them; not everything does.
|
|
28
|
+
|
|
29
|
+
A **service type** may be non-serializable (callbacks, function
|
|
30
|
+
signatures) — that's what distinguishes it from a `data/` type. If an I/O
|
|
31
|
+
value is a plain serializable value, prefer a `data/` type instead.
|
|
32
|
+
|
|
33
|
+
A non-service type sits *directly* in `services/` **only** when it is
|
|
34
|
+
genuinely shared across more than one service — rare, but it happens.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/features/*/ui/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ui/ — user interface
|
|
7
|
+
|
|
8
|
+
The top layer: UI that binds the `ecs/` database to what the user sees. It
|
|
9
|
+
owns no business logic — it subscribes to state, hands it to a pure
|
|
10
|
+
presentation, and wires actions back to transactions.
|
|
11
|
+
|
|
12
|
+
The folder name is framework-neutral; its contents are framework-specific
|
|
13
|
+
(Lit elements, React components, …). Each UI unit is its own folder, named
|
|
14
|
+
for the concept, exposed through a single lazy wrapper so consumers pay no
|
|
15
|
+
bundle cost for what they never render:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
<name>/
|
|
19
|
+
<name>.ts # PUBLIC: lazy wrapper — the only external import
|
|
20
|
+
<name>-element.ts # PRIVATE: the container (subscribe + delegate)
|
|
21
|
+
<name>-presentation.ts # PRIVATE: pure render(props) function
|
|
22
|
+
<name>.css.ts # PRIVATE: styles
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The detailed rules for each file kind (Lit bindings shown; React/Solid
|
|
26
|
+
tables inside each):
|
|
27
|
+
|
|
28
|
+
- `lazy-element.md` — the folder layout and lazy-wrapper mechanics.
|
|
29
|
+
- `element.md` — the container discipline (subscribe, callbacks, one
|
|
30
|
+
delegation; no logic).
|
|
31
|
+
- `presentation.md` — the pure `render` function.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- 'packages/**/*.ts'
|
|
4
|
+
- 'packages/**/*.tsx'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Pass functions by reference — don't wrap them
|
|
8
|
+
|
|
9
|
+
When a value is already a function with the signature the callee wants, pass it
|
|
10
|
+
**by reference**. Never re-wrap it in an arrow that only forwards its arguments:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// ❌ identity wrappers — the arrow adds nothing
|
|
14
|
+
expectConforms({ spec: (s, dt) => State.resolveBulletHits(s, dt), apply: (store, a) => hitAsteroid(store, a) });
|
|
15
|
+
onClick={() => save()}
|
|
16
|
+
todos.map((t) => render(t))
|
|
17
|
+
|
|
18
|
+
// ✅ pass the function itself
|
|
19
|
+
expectConforms({ spec: State.resolveBulletHits, apply: hitAsteroid });
|
|
20
|
+
onClick={save}
|
|
21
|
+
todos.map(render)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Fewer parameters than the callee supplies is fine — extra args are ignored — so a
|
|
25
|
+
`(store) => void` transaction passes directly where `(store, args) => void` is
|
|
26
|
+
expected. Wrap **only** when the call genuinely *adapts*: drops or reorders args
|
|
27
|
+
(`(_state, bounds) => createInitial(bounds)`), reaches into one (`(s, a) =>
|
|
28
|
+
del(s, a.id)`), or supplies a captured value.
|
|
29
|
+
|
|
30
|
+
**The lone real exception — unbound methods.** A method pulled off a class type or
|
|
31
|
+
an unbound instance loses its `this` when passed bare, so it needs a wrapper (or
|
|
32
|
+
`.bind`). This is *exceptionally rare here*: everything in this codebase is pure
|
|
33
|
+
functions and closures — every database's `transactions` / `actions` / `computed`
|
|
34
|
+
and every `data/` helper is an already-bound standalone function. So before
|
|
35
|
+
reaching for a wrapper, confirm you're not just forwarding args.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- 'packages/**/ui/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lazy Lit element pattern
|
|
7
|
+
|
|
8
|
+
Each Lit custom element lives in its own folder and is exposed to
|
|
9
|
+
external code through a single lazy wrapper function. The element class
|
|
10
|
+
itself is loaded only on first invocation, so consumers pay no bundle
|
|
11
|
+
cost for elements they never render.
|
|
12
|
+
|
|
13
|
+
## Folder layout
|
|
14
|
+
|
|
15
|
+
For an element conceptually named `foo`:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
foo/
|
|
19
|
+
foo.ts # PUBLIC: lazy wrapper Foo()
|
|
20
|
+
foo-element.ts # PRIVATE: the LitElement class
|
|
21
|
+
foo-presentation.ts # PRIVATE: pure render(props) function
|
|
22
|
+
foo.css.ts # PRIVATE: styles
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
External code only ever imports `foo.ts`. The other three are
|
|
26
|
+
implementation detail of the folder.
|
|
27
|
+
|
|
28
|
+
## The lazy wrapper (`foo.ts`)
|
|
29
|
+
|
|
30
|
+
A PascalCase function that returns an `html` template fragment. It
|
|
31
|
+
fires a dynamic `import()` of the element file on every call (the
|
|
32
|
+
import promise is intentionally unawaited — the browser deduplicates
|
|
33
|
+
and Lit upgrades the unknown element once the class registers).
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { html } from "lit";
|
|
37
|
+
import type { TemplateResult } from "lit";
|
|
38
|
+
|
|
39
|
+
export const Foo = (args: { count: number }): TemplateResult => {
|
|
40
|
+
void import("./foo-element.js");
|
|
41
|
+
return html`<my-foo .count=${args.count}></my-foo>`;
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rules:
|
|
46
|
+
|
|
47
|
+
- Exactly one public export: the wrapper function.
|
|
48
|
+
- Argument shape is a single typed object (or omitted entirely).
|
|
49
|
+
- The custom-element tag is hardcoded as a literal — no shared `tagName`
|
|
50
|
+
symbol crosses the wrapper / element boundary.
|
|
51
|
+
- The wrapper passes typed args via `.prop=${args.prop}` bindings, never
|
|
52
|
+
attribute strings.
|
|
53
|
+
- **Injecting a database:** a `DatabaseElement` receives its live database
|
|
54
|
+
through the `.service` setter — that is the one DI property it exposes
|
|
55
|
+
(`set service(db)`; there is no *public* `.database` binding target — the
|
|
56
|
+
`protected get database()` is a subclass's own read path, not a DI property).
|
|
57
|
+
A wrapper over a `DatabaseElement` binds `.service=${args.service}`. Binding
|
|
58
|
+
any other name (`.database=`) is silently inert: the setter never runs, so the element
|
|
59
|
+
falls back to `connectedCallback` self-creating from `this.plugin` and the
|
|
60
|
+
injected database is discarded.
|
|
61
|
+
|
|
62
|
+
## The element file (`foo-element.ts`)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { customElement } from "lit/decorators.js";
|
|
66
|
+
import * as presentation from "./foo-presentation.js";
|
|
67
|
+
import { styles } from "./foo.css.js";
|
|
68
|
+
|
|
69
|
+
const tagName = "my-foo";
|
|
70
|
+
|
|
71
|
+
declare global {
|
|
72
|
+
interface HTMLElementTagNameMap {
|
|
73
|
+
[tagName]: FooElement;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@customElement(tagName)
|
|
78
|
+
export class FooElement extends LitElement {
|
|
79
|
+
static styles = styles;
|
|
80
|
+
render() { return presentation.render({ /* … */ }); }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `tagName` is a private const; the global `HTMLElementTagNameMap`
|
|
85
|
+
declaration uses it so the wrapper's `html\`<my-foo></my-foo>\``
|
|
86
|
+
template is type-checked.
|
|
87
|
+
- One public export: the class.
|
|
88
|
+
- Side-effect imports of *other* element files are forbidden — invoke
|
|
89
|
+
their lazy wrappers from inside the presentation instead, so children
|
|
90
|
+
are tree-shaken too.
|
|
91
|
+
|
|
92
|
+
## Composition: render children via wrappers
|
|
93
|
+
|
|
94
|
+
Inside a presentation, render child elements by calling their lazy
|
|
95
|
+
wrappers, not by writing their tag literals or by importing their
|
|
96
|
+
classes. This is what keeps the lazy graph honest end-to-end.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { html } from "lit";
|
|
100
|
+
import { Bar } from "../bar/bar.js";
|
|
101
|
+
|
|
102
|
+
export const render = (props: { … }) => html`
|
|
103
|
+
<section>${Bar({ kind: props.kind })}</section>
|
|
104
|
+
`;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Dynamic-tag rendering
|
|
108
|
+
|
|
109
|
+
Do not use `lit/static-html` / `unsafeStatic` to switch between
|
|
110
|
+
elements. Take a render-callback prop instead:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
@property({ attribute: false })
|
|
114
|
+
renderChild!: (args: { service: SomeDb }) => TemplateResult;
|
|
115
|
+
|
|
116
|
+
// caller passes:
|
|
117
|
+
.renderChild=${({ service }) => Bar({ service })}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This keeps the child's lazy wrapper as the single point of contact and
|
|
121
|
+
preserves type checking across the boundary.
|
|
122
|
+
|
|
123
|
+
## Entry-point mounting
|
|
124
|
+
|
|
125
|
+
Even the top-level element is mounted through its lazy wrapper. A
|
|
126
|
+
`main.ts` calls `render(Foo(args), document.getElementById("app")!)` —
|
|
127
|
+
no side-effect import, no `document.createElement`.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- 'packages/**/src/**/*.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Type namespace pattern
|
|
7
|
+
|
|
8
|
+
Each type lives in its own folder. The folder's eponymous file is the only
|
|
9
|
+
public import surface; a `<Type>` namespace re-exports every helper.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/types/<type-name>/
|
|
13
|
+
<type-name>.ts # type alias + `export * as <Type> from "./public.js"`
|
|
14
|
+
schema.ts # `export const schema = { ... } as const satisfies Schema`
|
|
15
|
+
public.ts # re-exports every public helper
|
|
16
|
+
<helper>.ts # one declaration per file, camelCase
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// types/log-level/schema.ts
|
|
23
|
+
export const schema = { type: "string", enum: ["debug", "info", "warn", "error"] }
|
|
24
|
+
as const satisfies Schema;
|
|
25
|
+
|
|
26
|
+
// types/log-level/log-level.ts
|
|
27
|
+
import { Schema } from "@adobe/data/schema";
|
|
28
|
+
import { schema } from "./schema.js";
|
|
29
|
+
export type LogLevel = Schema.ToType<typeof schema>;
|
|
30
|
+
export * as LogLevel from "./public.js";
|
|
31
|
+
|
|
32
|
+
// types/log-level/public.ts
|
|
33
|
+
export { schema } from "./schema.js";
|
|
34
|
+
export { is } from "./is.js"; // when external code narrows broader input
|
|
35
|
+
export { values } from "./values.js"; // when external code iterates the set
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Consumers import only `LogLevel` from `log-level/log-level.ts` and use it
|
|
39
|
+
as both type and namespace (`LogLevel.is(x)`, `LogLevel.values`).
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
- Never import `schema.ts` or `public.ts` from outside the folder.
|
|
44
|
+
- Consumers use one import: `import { LogLevel } from "./log-level.js"`.
|
|
45
|
+
The same identifier serves as type *and* namespace — a separate
|
|
46
|
+
`import type` is unnecessary and produces a duplicate-identifier error.
|
|
47
|
+
- Helper files use `import type` from the sibling `<type-name>.ts` to
|
|
48
|
+
avoid cycling through `public.ts`.
|
|
49
|
+
- Add `is` / `values` / per-member descriptors only when a consumer
|
|
50
|
+
actually needs them — not preemptively. A consumer in a not-yet-built
|
|
51
|
+
higher layer counts: a presentational descriptor the `ui/` will need
|
|
52
|
+
(per-member glyphs, a keypad `values` list) may be authored with the
|
|
53
|
+
data type, but give it a one-line doc naming the intended consumer (as
|
|
54
|
+
`player-mark/mark-color.ts` does) so the need is recorded, not guessed.
|
|
55
|
+
|
|
56
|
+
## Strict single public export per file
|
|
57
|
+
|
|
58
|
+
**One public export per file** — this is strict, not a preference. A file
|
|
59
|
+
that *declares* exports exactly one public symbol (private declarations
|
|
60
|
+
inside it are fine and are tested through the public export). The reason is
|
|
61
|
+
cohesion: when several declarations share one file, each pulls its own
|
|
62
|
+
external imports to the top and the file becomes a jumble that belongs to no
|
|
63
|
+
single concept. One declaration → one file → imports that are about that one
|
|
64
|
+
thing.
|
|
65
|
+
|
|
66
|
+
More than one public export in a file is allowed **only when explicitly
|
|
67
|
+
approved on a per-file or per-file-type basis** — the deliberate, named
|
|
68
|
+
patterns below, nothing else. There is no "shared imports" or "cohesive set"
|
|
69
|
+
escape hatch: a folder of sibling pure functions over one type (e.g. the
|
|
70
|
+
`vec3`/`vec4`/`mat4x4` math) is one function per file, collected by the
|
|
71
|
+
folder's `public.ts` barrel — dozens of one-line files is the intended shape,
|
|
72
|
+
not a reason to lump.
|
|
73
|
+
|
|
74
|
+
- **Barrels** — `index.ts` and a namespace's `public.ts` *only* re-export
|
|
75
|
+
(`export … from "./…"`); they never declare. Many re-exports is their job.
|
|
76
|
+
- **The eponymous namespace file** (`<type>.ts`) — the type alias plus
|
|
77
|
+
`export * as <Type> from "./public.js"` (a type and its namespace value).
|
|
78
|
+
- **The single-file pattern** (below) — one value export plus type-only
|
|
79
|
+
exports, for a trivial namespace not worth a folder.
|
|
80
|
+
- **`<format>-schema.ts`** — a borrowed data-format projection (plain
|
|
81
|
+
`interface`s), next to its parser/emitter.
|
|
82
|
+
- **Explicit authoring-cohesion files** — where two exports are genuinely one
|
|
83
|
+
unit to author, e.g. a presentation's `render` plus a co-located fixture
|
|
84
|
+
bundle (`unlocalized`). Rare; the pair must truly belong together.
|
|
85
|
+
|
|
86
|
+
Everything else is one-per-file. In particular, **ECS facet folders
|
|
87
|
+
(`components/`, `resources/`, `archetypes/`, `indexes/`, …) are not exempt**:
|
|
88
|
+
one component / resource / archetype / index per file, collected by the
|
|
89
|
+
folder's `index.ts` barrel. Collapsing several into one `index.ts` is exactly
|
|
90
|
+
the anti-pattern this rule exists to prevent.
|
|
91
|
+
|
|
92
|
+
Never `*-types.ts`. "Types" is a meta-word the `.ts` extension already
|
|
93
|
+
implies; the name has to predict the contents. If you can't beat
|
|
94
|
+
`-types`, the file isn't a real lump — split per type, or inline at use
|
|
95
|
+
site.
|
|
96
|
+
|
|
97
|
+
## Casing: `PascalCase` for types, `camelCase` for everything else
|
|
98
|
+
|
|
99
|
+
Capitalize a name **only** when it is a type, or a value that *represents a
|
|
100
|
+
type*. Everything else — functions, values, methods — is `camelCase`.
|
|
101
|
+
|
|
102
|
+
A `Schema` is a runtime type, so this extends to schema helpers:
|
|
103
|
+
|
|
104
|
+
- A **schema value that is a type** is `PascalCase`: the namespace schemas
|
|
105
|
+
`F32`, `Boolean`, `Guid`, `True` (each *is* a type's schema).
|
|
106
|
+
- A **higher-order schema function that returns a new type-representing
|
|
107
|
+
schema** is a runtime **type constructor** — the value-level analogue of
|
|
108
|
+
`Readonly<T>` / `Partial<T>` — and is `PascalCase`: `Nullable(schema)`,
|
|
109
|
+
`Tuple(item, n)`, `SuccessResultSchema(v)`, `ProgressiveResultSchema(v, e)`.
|
|
110
|
+
- Everything else stays `camelCase`, even when it touches schemas: functions
|
|
111
|
+
that **annotate the same type** (`withValidation`), **build from a spec**
|
|
112
|
+
(`fromArchetype`, `fromObjectProperties`, `fromStructProperties`),
|
|
113
|
+
**resolve/transform** (`getDynamicSchema`, `toVertexBufferLayout`), or hang
|
|
114
|
+
off a namespace (`Guid.create`, `FractionalIndex.between`).
|
|
115
|
+
|
|
116
|
+
The test: *does calling it yield a new runtime type?* Yes → `PascalCase`
|
|
117
|
+
constructor. No → `camelCase`.
|
|
118
|
+
|
|
119
|
+
## Domain namespaces (for types you don't own)
|
|
120
|
+
|
|
121
|
+
When utility functions operate on a platform or third-party type (e.g.
|
|
122
|
+
`GPUDevice`, `GPUTexture`), use a **domain namespace** instead: a folder
|
|
123
|
+
named after the concept, with a namespace export but *no type re-export*.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
src/gpu/
|
|
127
|
+
gpu.ts # `export * as GPU from "./public.js"` — no type alias
|
|
128
|
+
public.ts # re-exports every public helper
|
|
129
|
+
<helper>.ts # one declaration per file
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// gpu/gpu.ts
|
|
134
|
+
export * as GPU from "./public.js";
|
|
135
|
+
|
|
136
|
+
// gpu/public.ts
|
|
137
|
+
export { createCubemap } from "./create-cubemap.js";
|
|
138
|
+
export { cubeFaceView } from "./cube-face-view.js";
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Consumers import the namespace and get discoverability without shadowing
|
|
142
|
+
the platform type:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { GPU } from "@adobe/data-graphics";
|
|
146
|
+
GPU.createCubemap(device, 256, "rgba16float");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Name by purpose, not by the external type.** `GPU`, not `GPUDevice`.
|
|
150
|
+
|
|
151
|
+
**Tree-shaking still works**: bundlers (Rollup/Rolldown) trace static
|
|
152
|
+
property accesses (`GPU.createCubemap`) and exclude unused helpers.
|
|
153
|
+
Dynamic access (`GPU[name]`) defeats this — avoid it.
|
|
154
|
+
|
|
155
|
+
**When NOT to use a domain namespace**: if a utility is only useful as
|
|
156
|
+
an internal implementation detail of a specific plugin (not callable by
|
|
157
|
+
consumers), keep it private to that plugin's folder instead.
|
|
158
|
+
|
|
159
|
+
## Single file pattern
|
|
160
|
+
|
|
161
|
+
This pattern is ONLY allowed if there is only a single value export and every other export is just a type.
|
|
162
|
+
As soon as there is more than one value type it MUST be promoted to the full namespace pattern.
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
|
|
166
|
+
// core-database.ts
|
|
167
|
+
|
|
168
|
+
const coreDatabasePlugin = Database.Plugin.create({
|
|
169
|
+
components,
|
|
170
|
+
resources,
|
|
171
|
+
archetypes,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export type CoreDatabase = Database.Plugin.ToDatabase<typeof coreDatabasePlugin>;
|
|
175
|
+
|
|
176
|
+
export namespace CoreDatabase {
|
|
177
|
+
export const plugin = coreDatabasePlugin;
|
|
178
|
+
export type Store = Database.Plugin.ToStore<typeof coreDatabasePlugin>;
|
|
179
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/*plugin*'
|
|
4
|
+
- '**/*plugin*/**'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Plugin modelling — model vs. system, authored vs. derived
|
|
8
|
+
|
|
9
|
+
Each plugin has two orthogonal axes:
|
|
10
|
+
|
|
11
|
+
1. **Authored** state — the user inserts / sets it. Designed by the human
|
|
12
|
+
architect.
|
|
13
|
+
2. **Derived** state — a system produces it from other state. Implementation;
|
|
14
|
+
owned by the AI.
|
|
15
|
+
|
|
16
|
+
The human's mental model of a project is the union of every plugin's
|
|
17
|
+
**authored surface**. Systems and derived state are how the world updates
|
|
18
|
+
and renders — read their interface contract, not their innards.
|
|
19
|
+
|
|
20
|
+
## Two tiers of model plugins
|
|
21
|
+
|
|
22
|
+
- **Core** — the primary record types every consumer reads. Small, fixed,
|
|
23
|
+
shared.
|
|
24
|
+
- **Authoring abstractions** — higher-level intents (orbit, animation,
|
|
25
|
+
procedural shape, particle emitter, constraint, …) that systems expand
|
|
26
|
+
into core state. Open-ended; each project picks what it needs.
|
|
27
|
+
|
|
28
|
+
A plugin almost never holds both authored and derived data the user cares
|
|
29
|
+
about. If it appears to, the derived data is the *implementation* of the
|
|
30
|
+
authoring abstraction.
|
|
31
|
+
|
|
32
|
+
## File naming
|
|
33
|
+
|
|
34
|
+
Each file whose primary export is a `Database.Plugin` (created or
|
|
35
|
+
combined) ends in `-plugin.ts` — e.g. `node-plugin.ts`, `model-plugin.ts`,
|
|
36
|
+
`pbr-core-plugin.ts`. The suffix is for discovery: cursor rules,
|
|
37
|
+
codebase grep, and "find all plugins" tooling match `**/*-plugin.ts`
|
|
38
|
+
without inspecting contents.
|
|
39
|
+
|
|
40
|
+
The exported constant's name depends on whether the folder also hosts a
|
|
41
|
+
type namespace:
|
|
42
|
+
|
|
43
|
+
- **Pure-plugin folder** (no owned type) → export the concept name
|
|
44
|
+
itself, lowercase. `node-plugin.ts` exports `node`. Import sites read
|
|
45
|
+
`import { node } from "..."; Database.Plugin.combine(node, ...)`.
|
|
46
|
+
- **Type + plugin folder** (folder hosts both an owned type and its
|
|
47
|
+
plugin) → the plugin file exports `plugin`, re-exported through the
|
|
48
|
+
type's namespace. `camera-plugin.ts` exports `plugin`; `camera/public.ts`
|
|
49
|
+
re-exports it; consumers reach it as `Camera.plugin`. One import gives
|
|
50
|
+
both the type and the plugin.
|
|
51
|
+
|
|
52
|
+
Files with helper exports, value types, schemas, or shaders do **not**
|
|
53
|
+
take the suffix — only plugin files.
|
|
54
|
+
|
|
55
|
+
## Resources and archetypes as types
|
|
56
|
+
|
|
57
|
+
Prefer **one resource per plugin**, of an owned type that the folder also
|
|
58
|
+
hosts. `light/` has one `light` resource of type `Light`; `orbit/` has
|
|
59
|
+
one `orbit` resource of type `Orbit`. Splitting a single coherent
|
|
60
|
+
concept into siblings (`lightDirection`, `lightColor`, `ambientStrength`)
|
|
61
|
+
duplicates the concept's identity at every read site.
|
|
62
|
+
|
|
63
|
+
For each **archetype** the plugin declares, add a TypeScript type with
|
|
64
|
+
the same name describing one row's authored shape. `Node` is the type
|
|
65
|
+
for the `Node` archetype's row. The components stay separate per
|
|
66
|
+
typed-buffer-column convention; the type names the bundle so consumers
|
|
67
|
+
can declare `const node: Node` after a read.
|
|
68
|
+
|
|
69
|
+
Both cases follow the same folder shape:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
<concept>/
|
|
73
|
+
<concept>.ts # type + namespace
|
|
74
|
+
public.ts # re-exports plugin (and any helpers)
|
|
75
|
+
<concept>-plugin.ts # exports `plugin`
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Consumers reach the plugin as `Concept.plugin` and the type as
|
|
79
|
+
`Concept`. Pure-plugin folders (no owned type) still export their
|
|
80
|
+
plugin under a lowercase concept name — see "File naming" below.
|
|
81
|
+
|
|
82
|
+
**Exceptions:**
|
|
83
|
+
- Plugins whose resources have genuinely independent lifecycles
|
|
84
|
+
(`graphics` — `device`, `canvas`, `commandEncoder`, each set at
|
|
85
|
+
different times) keep them as separate resources. Consolidating
|
|
86
|
+
would force any one write to replace the whole struct.
|
|
87
|
+
- Ephemeral implementation archetypes (e.g. `_PbrPrimitive`,
|
|
88
|
+
`_VisibleMaterial`) don't need a value type — consumers iterate
|
|
89
|
+
them by archetype, never construct one.
|
|
90
|
+
|
|
91
|
+
## Shared conventions
|
|
92
|
+
|
|
93
|
+
- **Uppercase identifier** = archetype. **Lowercase** = component or
|
|
94
|
+
resource. Disambiguated by where declared (archetype vs. component vs.
|
|
95
|
+
resource section of the data plugin).
|
|
96
|
+
- **Entity references** are typed `EntityId`; field names drop the `Ref`
|
|
97
|
+
suffix — the type carries the signal.
|
|
98
|
+
- **Implementation prefixes** (`pbr`, `ibl`, …) belong in code where they
|
|
99
|
+
disambiguate cross-plugin reuse. Drop them in the conceptual view.
|
|
100
|
+
- **`_` prefix** = ephemeral / derived / not part of the data model.
|
|
101
|
+
Applies to **components, archetypes, and resources** in code — the
|
|
102
|
+
prefix makes "this is system-owned, not user-authored" visible at every
|
|
103
|
+
read site. The human ignores it when modelling; system-graph views
|
|
104
|
+
still display it. Does **not** apply to plugin names, system names, or
|
|
105
|
+
transactions — those are grouped by feature folder, and the folder
|
|
106
|
+
already communicates whether a plugin is authored surface or
|
|
107
|
+
implementation.
|
|
108
|
+
- **Inline `: Type`** only on `_`-prefixed names. Persistent items get
|
|
109
|
+
their type from the data plugin where they're declared.
|
|
110
|
+
- **Components use JSON schemas**, resources use `{ default: X as T }`.
|
|
111
|
+
Schemas (e.g. `Entity.schema`, `Mat4x4.schema`, `Boolean.schema`,
|
|
112
|
+
`{ type: "string" }`) enable typed-buffer column storage — the right
|
|
113
|
+
choice when a value is stored once per entity. Resources are a single
|
|
114
|
+
slot per database, so the runtime cost of typed storage isn't worth
|
|
115
|
+
it; the default-pattern is the convention there. Use the default
|
|
116
|
+
pattern on components *only* for runtime-only objects (GPU buffers,
|
|
117
|
+
bind groups, closures, complex JS objects with no schema).
|
|
118
|
+
|
|
119
|
+
## Model plugin format
|
|
120
|
+
|
|
121
|
+
Authored surface only — no derived state, no systems.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
pluginName
|
|
125
|
+
components
|
|
126
|
+
fieldName: Type
|
|
127
|
+
...
|
|
128
|
+
resources
|
|
129
|
+
fieldName: Type
|
|
130
|
+
...
|
|
131
|
+
archetypes
|
|
132
|
+
ArchetypeName: [field, field, ...OtherArchetype]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `...Other` composes another archetype's component list.
|
|
136
|
+
- Shape comments (`// { … }`) stay on the same line; do not wrap.
|
|
137
|
+
|
|
138
|
+
## System plugin format
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
systemName // high-level summary
|
|
142
|
+
query: ArchetypeExpr [, ArchetypeExpr ...] // omit if resource-only
|
|
143
|
+
read:
|
|
144
|
+
name // persistent — type from data plugin
|
|
145
|
+
_name: Type // ephemeral — type inline
|
|
146
|
+
write:
|
|
147
|
+
name // writes a component / resource
|
|
148
|
+
_Archetype // creates new entities of this archetype
|
|
149
|
+
// free-text side effect // draw calls, network sends, …
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `read:` and `write:` are outlined one-per-line.
|
|
153
|
+
- A system can have multiple independent queries; comma-separate them on
|
|
154
|
+
the `query:` line.
|
|
155
|
+
|
|
156
|
+
## Query DSL
|
|
157
|
+
|
|
158
|
+
- `Archetype` — entities matching the archetype.
|
|
159
|
+
- `+component` — require this additional component.
|
|
160
|
+
- `-component` — exclude entities that have it.
|
|
161
|
+
- `Archetype+a-b+c` — left-to-right chain of `+` / `-`.
|
|
162
|
+
- `Q1, Q2, ...` — multiple independent queries the system reads from.
|
|
163
|
+
|
|
164
|
+
## Designing or discussing a plugin
|
|
165
|
+
|
|
166
|
+
1. Write the authored surface (model format) **first**.
|
|
167
|
+
2. Then each system as its own interface card with `query` / `read` /
|
|
168
|
+
`write`.
|
|
169
|
+
3. The graph of `write:` → `read:` edges is the implementation; the union
|
|
170
|
+
of authored surfaces is the human's mental model.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- 'packages/**/*presentation.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Presentation authoring
|
|
7
|
+
|
|
8
|
+
A presentation is a pure function: data + action callbacks → renderable
|
|
9
|
+
output. It has no `this`, no hooks, no subscriptions, no side effects,
|
|
10
|
+
no component state, no business logic. The container element supplies
|
|
11
|
+
everything it needs.
|
|
12
|
+
|
|
13
|
+
This rule is framework-agnostic. The framework-specific bindings are in
|
|
14
|
+
the table at the bottom.
|
|
15
|
+
|
|
16
|
+
## Exports
|
|
17
|
+
|
|
18
|
+
A presentation file exports exactly one symbol: `render` — the pure
|
|
19
|
+
function. No types, styles, helpers, constants, or re-exports. If you
|
|
20
|
+
need shared logic, keep it inside `render` as a local helper. If you
|
|
21
|
+
need shared UI, that's another presentation.
|
|
22
|
+
|
|
23
|
+
Consumers derive the props type via `Parameters<typeof render>[0]` —
|
|
24
|
+
never declare or export a separate props type.
|
|
25
|
+
|
|
26
|
+
## Action callbacks are actions, not events
|
|
27
|
+
|
|
28
|
+
Use **verbNoun** form. The presentation invokes the callback when the
|
|
29
|
+
user acts; it *is* the action, with no intermediary event to handle.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// ✅
|
|
33
|
+
export function render({ dismiss, setLayout, createFolder }: {
|
|
34
|
+
dismiss: () => void;
|
|
35
|
+
setLayout: (layout: "grid" | "row") => void;
|
|
36
|
+
createFolder: (name: string) => void;
|
|
37
|
+
}) { /* ... */ }
|
|
38
|
+
|
|
39
|
+
// ❌
|
|
40
|
+
export function render({ onDismiss, onLayoutChange, onCreate }: { ... }) {}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Testing
|
|
44
|
+
|
|
45
|
+
A pure `render(props)` is trivial to test — no DOM, no rendering, no
|
|
46
|
+
component lifecycle. Pass stub props (no-op callbacks) and assert on the
|
|
47
|
+
**declaration** it returns. Co-locate tests as `<name>-presentation.test.ts`.
|
|
48
|
+
|
|
49
|
+
For Lit, wrap the result with `Template.from` (from `@adobe/data-lit`) and
|
|
50
|
+
assert on the query, not on positional `.values` indices:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { describe, it, expect } from "vitest";
|
|
54
|
+
import { Template } from "@adobe/data-lit";
|
|
55
|
+
import { render } from "./todo-toolbar-presentation.js";
|
|
56
|
+
|
|
57
|
+
it("wires the Add button and shows the stats", () => {
|
|
58
|
+
const addTodo = () => {};
|
|
59
|
+
const t = Template.from(render(props({ addTodo, totalCount: 3, completedCount: 1 })));
|
|
60
|
+
expect(t.has("<sp-textfield")).toBe(true); // presence
|
|
61
|
+
expect(t.text).toContain("1 / 3 completed"); // static + primitive text
|
|
62
|
+
expect(t.values).toContain(addTodo); // callback wired (identity, not values[3])
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Template.Query` exposes `text`, `values`, `children`, `has(fragment)`, and
|
|
67
|
+
`find(fragment)`. Prefer **identity** checks (`values` `toContain` a
|
|
68
|
+
callback / `nothing` / a bound value) over positions. To exercise a
|
|
69
|
+
callback, pull it from `values` and call it with a plain synthetic event —
|
|
70
|
+
no mocks, no DOM.
|
|
71
|
+
|
|
72
|
+
A presentation that only composes lazy child-element wrappers (or imports
|
|
73
|
+
Spectrum components) needs a `// @vitest-environment jsdom` docblock so the
|
|
74
|
+
module's custom-element side effects can load — the assertions still never
|
|
75
|
+
render or touch the DOM. Presentations of native elements run under Node.
|
|
76
|
+
|
|
77
|
+
## Framework bindings
|
|
78
|
+
|
|
79
|
+
| Framework | Return type | Common template imports | Test assertion target |
|
|
80
|
+
| --------- | ---------------- | ----------------------------------- | -------------------------------------------- |
|
|
81
|
+
| Lit | `TemplateResult` | `html`, `nothing`, `svg`, `repeat` | `Template.from(...)` query (`@adobe/data-lit`) |
|
|
82
|
+
| React | `ReactNode` | JSX runtime | render-to-string or virtual-DOM utility |
|
|
83
|
+
| Solid | `JSX.Element` | JSX runtime | render-to-string or virtual-DOM utility |
|