@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.
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,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 |