@adobe/data-ai 0.9.86 → 0.9.87

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.
@@ -0,0 +1,183 @@
1
+ ---
2
+ paths:
3
+ - '**/services/**/*.ts'
4
+ - '**/*-plugin.ts'
5
+ - '**/*-plugin/**/*.ts'
6
+ - '**/*dependent-state*.ts'
7
+ - '**/*dependent-state*/**/*.ts'
8
+ ---
9
+
10
+ # Observe pattern
11
+
12
+ `Observe<T>` from `@adobe/data/observe` — a subscription function: `(notify: (value: T) => void) => Unobserve`. Callback may be invoked
13
+ synchronously or asynchronously, zero or more times. Returns `Unobserve` to stop observing.
14
+
15
+ **Always use `Observe.*` helpers to create and manipulate observables. Never write raw subscriptions or manual notify wrappers** — the
16
+ standard helpers cover every composition need and keep the code consistent.
17
+
18
+ ---
19
+
20
+ ## Creation helpers
21
+
22
+ | Helper | Usage |
23
+ | ------------------------------------ | -------------------------------------------------- |
24
+ | `Observe.fromConstant(value)` | `Observe<T>` from a constant value |
25
+ | `Observe.fromProperties({ a, b })` | Combine named observables into `Observe<{ a, b }>` |
26
+ | `Observe.fromArray([obs1, obs2])` | Combine array of observables into `Observe<T[]>` |
27
+ | `Observe.fromPromise(() => Promise)` | Lazy; notifies once when promise resolves |
28
+ | `Observe.createState(initial?)` | `[Observe<T>, (value: T) => void]` — mutable state |
29
+
30
+ ```ts
31
+ const constant = Observe.fromConstant(42);
32
+ const combined = Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b });
33
+ const [count, setCount] = Observe.createState(0);
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Transformation helpers
39
+
40
+ | Helper | Usage |
41
+ | ----------------------------------- | ------------------------------------------------------------ |
42
+ | `Observe.withMap(obs, fn)` | Transform value; map from one type to another |
43
+ | `Observe.withFilter(obs, fn)` | Transform or filter; return `undefined` to skip |
44
+ | `Observe.withDefault(default, obs)` | Use default when value is undefined |
45
+ | `Observe.withLazy(() => obs)` | Defer expensive observable creation until first subscription |
46
+ | `Observe.withUnwrap(obs)` | Flatten `Observe<Observe<T>>` to `Observe<T>` |
47
+
48
+ ```ts
49
+ const doubled = Observe.withMap(count, n => n * 2);
50
+ const max = Observe.withFilter(Observe.fromProperties({ a, b }), ({ a, b }) => Math.max(a, b));
51
+ const withFallback = Observe.withDefault('unknown', maybeName);
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Conversion helpers
57
+
58
+ | Helper | Usage |
59
+ | ------------------------ | ----------------------------------- |
60
+ | `Observe.toPromise(obs)` | Resolve with first value (one-shot) |
61
+
62
+ `Observe.toPromise` is for services reading a current value — **never use it in UI elements**.
63
+
64
+ ---
65
+
66
+ ## ⛔ Antipattern: synchronous "peek"
67
+
68
+ **Never read an observable's current value by subscribing and immediately unsubscribing.** The `notify` callback may fire asynchronously
69
+ (auth state, network, derived chains), so the snapshot is stale or `undefined`.
70
+
71
+ ```ts
72
+ // WRONG — only works if the observable happens to emit synchronously on subscribe
73
+ let value: T | undefined;
74
+ const unobserve = someObservable(v => {
75
+ value = v;
76
+ });
77
+ unobserve();
78
+ // use `value` ← undefined for any async-resolving observable
79
+ ```
80
+
81
+ ```ts
82
+ // CORRECT — resolve the current value explicitly
83
+ const value = await Observe.toPromise(someObservable);
84
+ ```
85
+
86
+ A stored `unobserve` is **only** ever for deferred teardown (returned from an Effect, called on unmount/cleanup) — never to bookend a
87
+ synchronous read. If a sync call site (e.g. a DOM event handler that must call `preventDefault()` before any `await`) needs the value, do
88
+ the synchronous work first, then `void asyncHelper(...)` that awaits `Observe.toPromise`. In a Lit render path, read reactive values through
89
+ `useObservableValues`, not `toPromise`.
90
+
91
+ ---
92
+
93
+ ## ⛔ Antipattern: `undefined` as an Observe data value
94
+
95
+ `@adobe/data/observe` reserves `undefined` to mean **"no value has been emitted yet"** — it gates the synchronous initial emit in
96
+ `Observe.createState`, `fromProperties`, and friends. A data value flowing through an Observe pipeline must use `null` for the absent case,
97
+ **never** `undefined`.
98
+
99
+ ```ts
100
+ // WRONG — Observe<X | undefined>
101
+ const [projectId, setProjectId] = Observe.createState<ProjectId | undefined>(undefined);
102
+ return Observe.withDefault(undefined, cloudSync.projectName);
103
+
104
+ // CORRECT — Observe<X | null>
105
+ const [projectId, setProjectId] = Observe.createState<ProjectId | null>(null);
106
+ return Observe.withDefault<string | null>(null, cloudSync.projectName);
107
+ ```
108
+
109
+ Why it bites:
110
+
111
+ - `createState<T | undefined>(undefined)` does **not** fire on subscribe (the implementation treats `undefined` as "not yet set"), so
112
+ downstream `useObservableValues` callers wait forever for a value that never arrives.
113
+ - `fromProperties` re-emits when an input flips `undefined → defined`, not when a value changes — using `undefined` as data corrupts the
114
+ dirty-tracking.
115
+ - `null` JSON-stringifies across the wire; `undefined` silently disappears.
116
+
117
+ When auditing new code, flag `Observe<… | undefined>`, `createState<… | undefined>`, `withDefault(undefined`, and `fromConstant(undefined)`
118
+ — each should use `null`. (A plain function returning `undefined`, e.g. an early `return` from an async helper, is unrelated and fine.)
119
+
120
+ ---
121
+
122
+ ## ⛔ Antipattern: membership-only subscription for value reads (ECS)
123
+
124
+ `db.observe.select(archetype.components)` fires **only when entities enter or leave the archetype** — i.e. on set-membership changes. It
125
+ does **not** fire when a component value changes on an entity already in the set. If the callback reads component **values**, this
126
+ subscription is almost always wrong: it emits once on load, then silently stops reacting when the user edits values (opacity slider,
127
+ selection toggle, drag end). The derived state goes stale with no error — the hardest bug to catch.
128
+
129
+ Pick the subscription by what the callback reads:
130
+
131
+ | Callback needs | Use |
132
+ | ----------------------------------------------------------------------- | --------------------------------------------------------- |
133
+ | Entity **IDs** only ("how many items," "does this track have children") | `db.observe.select(archetype.components)` |
134
+ | Component **values** (any `db.get(id, 'field')` in a map/filter) | `Database.observeSelectDeep(db, [...fields] as const)` |
135
+ | A single UI element bound to one entity | `db.observe.entity(id)` / `observe.entity(id, archetype)` |
136
+
137
+ ```ts
138
+ // WRONG — reads values but only reacts to add/remove; value edits never re-run this
139
+ Observe.withMap(db.observe.select(archetype.components), ids => ids.filter(id => (db.get(id, 'selectedByUsers') ?? []).includes(userId)));
140
+
141
+ // CORRECT — value-tracking subscription; list exactly the fields read downstream
142
+ const SELECTED_FIELDS = ['selectedByUsers'] as const; // hoist the tuple above the factory
143
+ const rows$ = Database.observeSelectDeep(db, SELECTED_FIELDS);
144
+ Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(userId)).map(r => r.id));
145
+ ```
146
+
147
+ `observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually
148
+ read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate
149
+ (`@adobe/data/observe`'s `withDeduplicate` is reference-equality only). `observeSelectDeep` from a UI element is banned — let each element
150
+ observe its own entity (see `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`).
151
+
152
+ ---
153
+
154
+ ## Common patterns
155
+
156
+ **Derived observable from service:**
157
+
158
+ ```ts
159
+ export const isDevelopment = memoize((service: Pick<EnvironmentService, 'type'>) =>
160
+ Observe.withMap(service.type, type => type === 'development')
161
+ );
162
+ ```
163
+
164
+ **Computed from multiple resources:**
165
+
166
+ ```ts
167
+ Observe.withFilter(Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b }), ({ a, b }) => Math.max(a, b));
168
+ ```
169
+
170
+ **Lazy creation:** `Observe.withLazy(() => expensiveObs)` — defers until first subscription.
171
+
172
+ ---
173
+
174
+ ## Execute
175
+
176
+ When adding or using Observe:
177
+
178
+ 1. Use `Observe.fromConstant` for static values.
179
+ 2. Use `Observe.fromProperties` to combine multiple observables.
180
+ 3. Use `Observe.withFilter` when mapping and/or filtering.
181
+ 4. Use `Observe.createState` for mutable state with a setter.
182
+ 5. Use `Observe.withUnwrap` to flatten `Observe<Observe<T>>`.
183
+ 6. Call `unobserve()` when cleaning up (e.g. component unmount).
@@ -0,0 +1,102 @@
1
+ ---
2
+ paths:
3
+ - '**/*.tsx'
4
+ ---
5
+
6
+ # React component authoring
7
+
8
+ React components live in the `components/` layer. Consume Observe and void actions from plugins. Use `@adobe/data-react` for `useDatabase`,
9
+ `useObservableValues`, and `DatabaseProvider`.
10
+
11
+ ---
12
+
13
+ ## useDatabase — single context
14
+
15
+ Binding components **must** call `useDatabase` to obtain the main service context. Do **not** access any other React context. Additional
16
+ contexts create a context waterfall and hurt performance. All other services and state are reachable from the database.
17
+
18
+ ---
19
+
20
+ ## Binding component vs presentation
21
+
22
+ **Binding component** — the React component that:
23
+
24
+ - Injects observed values via `useObservableValues`
25
+ - Triggers re-render when those values change
26
+ - Binds action callbacks to the presentation
27
+
28
+ **Presentation** — a pure function (no hooks) that receives data and action callbacks as props and returns JSX. Keep reactive logic in the
29
+ binding component; presentation stays pure.
30
+
31
+ ---
32
+
33
+ ## Props from parent
34
+
35
+ **Do not** pass values from parent except when needed to identify which entity in the database to bind to.
36
+
37
+ ```tsx
38
+ // Parent: passes entity so child knows which record to bind to
39
+ {
40
+ values.sprites.map(entity => <Sprite key={entity} entity={entity} />);
41
+ }
42
+
43
+ // Child: uses entity prop to observe the right record
44
+ function Sprite({ entity }: { entity: Entity }) {
45
+ const db = useDatabase();
46
+ const values = useObservableValues(() => ({ sprite: db.observe.entity(entity, db.archetypes.Sprite) }), [entity]);
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## useObservableValues
53
+
54
+ _Most_ binding components use a **single** `useObservableValues` call. Collect all observed values in one object. Use `Observe.withDefault`
55
+ for values that may resolve slowly.
56
+
57
+ ```tsx
58
+ function Counter() {
59
+ const db = useDatabase(counterPlugin);
60
+ const values = useObservableValues(() => ({
61
+ count: db.observe.resources.count,
62
+ }));
63
+ if (!values) return null;
64
+ return presentation.render({ ...values, increment: db.transactions.increment });
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Presentation exports
71
+
72
+ Presentation files ONLY export `render` (and localization bundles where appropriate). Nothing else. If you need render args type externally,
73
+ use `Parameters<typeof render>[0]`.
74
+
75
+ ---
76
+
77
+ ## Action callbacks (not events)
78
+
79
+ Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples.
80
+
81
+ **Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments:
82
+
83
+ ```tsx
84
+ increment: db.transactions.increment; // reference — no wrapper needed
85
+ toggleSprite: () => db.transactions.toggleSpriteActive({ entity }); // args required — wrap
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Execute
91
+
92
+ When creating or modifying a React component:
93
+
94
+ 1. Call `useDatabase` for the main service context. Do not use any other React context.
95
+ 2. Split into binding component (reactive) and presentation (pure).
96
+ 3. Use a single `useObservableValues` in the binding component.
97
+ 4. Pass observed values and action callbacks to presentation. Pass function references directly when the signature matches; wrap only when
98
+ supplying arguments.
99
+ 5. Keep presentation pure — no hooks.
100
+ 6. Presentation exports only `render` (and localization bundles where appropriate).
101
+ 7. Add `*-presentation.test.tsx` for presentation when appropriate; do not unit test binding components.
102
+ 8. Never include business logic within binding components. Move into computed values or action handlers.
@@ -0,0 +1,164 @@
1
+ ---
2
+ paths:
3
+ - '**/services/**/*.ts'
4
+ - '**/*-service.ts'
5
+ ---
6
+
7
+ # Service authoring
8
+
9
+ Asynchronous data services. Live in the `services/` layer. Adhere to the namespace rule for type and function organization.
10
+
11
+ **Data** = readonly JSON values or Blobs.
12
+
13
+ ---
14
+
15
+ ## Front-end vs back-end services
16
+
17
+ **Front-end services** — called directly from UI components. Support unidirectional control flow.
18
+
19
+ | Allowed | Not allowed |
20
+ | ------------------------------- | -------------------------------- |
21
+ | `Observe<Data>` | Promise or AsyncGenerator return |
22
+ | Other services (sub-Services) | |
23
+ | Void-returning action functions | |
24
+
25
+ **Back-end services** — usually stateless. Functions typically return `Promise<Data>` or `AsyncGenerator<Data>`. Not called directly from
26
+ UI; front-end services call them.
27
+
28
+ This separation keeps UI components on a strict unidirectional path: data down via void actions, data up via Observe.
29
+
30
+ ---
31
+
32
+ ## Front-end service constraints
33
+
34
+ `Service` interfaces for UI consumption may only contain:
35
+
36
+ - **`Observe<Data>`** — observable properties or factories
37
+ - **Sub-Service**s — nested service interfaces
38
+ - **Action functions** — zero or more `Data` arguments, return `void` only
39
+ - **Factory functions** — create observables or sub-Services
40
+
41
+ Compile-time check: `Assert<AsyncDataService.IsValid<ServiceInterface>>`.
42
+
43
+ ---
44
+
45
+ ## Folder structure and cohesion
46
+
47
+ **MUST NOT** place implementation files alongside the interface file. The `<name>-service/` folder MUST contain only the interface file and
48
+ `public.ts`.
49
+
50
+ ```
51
+ services/<name>-service/
52
+ <name>-service.ts ← interface + contract types; `export * as ServiceName from './public.js'` at bottom
53
+ public.ts ← re-exports factory (and other public exports) from the implementation sub-folder
54
+ <impl-name>/ ← REQUIRED sub-folder for all implementation files
55
+ create-<name>-service.ts ← factory function
56
+ *.ts ← helpers, utils, constants (implementation-private)
57
+ *.test.ts ← tests live here alongside implementation
58
+ ```
59
+
60
+ **Single-file exception**: a bare factory with no helpers, no utilities, and no constants MAY live as a flat file alongside the interface.
61
+ The moment any additional implementation file is needed, the factory MUST move into a sub-folder.
62
+
63
+ **Interface file** (`<name>-service.ts`) — interface declaration and types that define the public service contract. MUST NOT import from any
64
+ implementation file or sub-folder. At the bottom of the file, exports the namespace: `export * as ServiceName from './public.js'`. This
65
+ merges the interface type and the factory namespace under one identifier — callers type against the interface and invoke factory functions
66
+ through the same name.
67
+
68
+ **Separate types file at the interface level** — only permitted if it contains exclusively public-contract types (types that appear in the
69
+ interface's method signatures or `Observe<>` generic arguments). Implementation-detail types — third-party API shapes, `window.*` global
70
+ augmentations, internal utilities — MUST live inside the implementation sub-folder.
71
+
72
+ **`*.test.ts` files MUST NOT appear at the interface folder level.** Every test file belongs inside the implementation sub-folder next to
73
+ the code it tests; there is nothing at the interface level to test.
74
+
75
+ **Implementation sub-folder** — all implementation logic including the factory, utilities, constants, helpers, and their tests. MUST NOT
76
+ depend on the interface file's types via the `public.js` re-export; import directly from `<name>-service.js`.
77
+
78
+ **public.ts** — MUST re-export the factory function and any other public exports from the implementation sub-folder. Does NOT re-export the
79
+ interface — the interface is exported directly from `<name>-service.ts`.
80
+
81
+ **No classes** — factory function or static plain object only. `this` bindings are brittle and block higher-order composition.
82
+
83
+ ---
84
+
85
+ ## Factory export shape — `singletonFactory` + dual export
86
+
87
+ Every service factory MUST be wrapped with `singletonFactory` from `factory-functions.js`. This guarantees one instance per argument tuple
88
+ when the same factory is called multiple times, preventing duplicate subscriptions and torn state.
89
+
90
+ Export **both** the raw `factory` and the singleton-wrapped `create`:
91
+
92
+ ```ts
93
+ // <name>-service/<impl-name>/create-<name>-service.ts
94
+ import { singletonFactory } from 'factory-functions.js';
95
+
96
+ export const factory = (deps: Deps): NameService => {
97
+ // implementation
98
+ };
99
+
100
+ // `factory` — for test injection (bypasses the singleton cache; each test gets a fresh instance)
101
+ // `create` — for production use (singleton per deps-tuple)
102
+ export const create = singletonFactory(factory);
103
+ ```
104
+
105
+ `public.ts` re-exports both:
106
+
107
+ ```ts
108
+ export { create, factory } from './<impl-name>/create-<name>-service.js';
109
+ ```
110
+
111
+ **Why:** Tests must create fresh instances without sharing a singleton cache. Production callers use `create`; tests that need isolation
112
+ import `factory` directly.
113
+
114
+ ---
115
+
116
+ ## Service interface cleanliness
117
+
118
+ The `<name>-service.ts` interface MUST NOT reference internal implementation tools, third-party SDK names, or infrastructure system
119
+ identifiers in any method signature, property name, or type parameter.
120
+
121
+ **Wrong** — leaks an internal infrastructure name into the UI contract:
122
+
123
+ ```ts
124
+ interface SessionService extends Service {
125
+ readonly dunamis: DunamisSdk; // UI now knows Dunamis exists
126
+ }
127
+ ```
128
+
129
+ **Correct** — surfaces only what UI needs, implementation-tool-agnostic:
130
+
131
+ ```ts
132
+ interface SessionService extends Service {
133
+ readonly sessionId: Observe<string>;
134
+ }
135
+ ```
136
+
137
+ The interface is the contract between UI and data layers. If the underlying tool is swapped (Dunamis → another SDK, cookies → localStorage),
138
+ the interface must not change. Any leak of an internal tool name into the interface couples the UI to an implementation detail it must not
139
+ know about.
140
+
141
+ ---
142
+
143
+ ## Execute
144
+
145
+ When creating or modifying a service:
146
+
147
+ 1. Place in `services/<name>-service/`.
148
+ 2. Interface in `<name>-service.ts` — types only, extend `Service`, add `Assert<AsyncDataService.IsValid<>>`.
149
+ 3. All implementation files go inside a named sub-folder (e.g. `create-<name>-service/`).
150
+ 4. At the bottom of `<name>-service.ts`, add `export * as ServiceName from './public.js'`.
151
+ 5. In `public.ts`, re-export the factory (and any other public exports) from the implementation sub-folder.
152
+ 6. For front-end services: actions return void only; observables observe Data or Service only. For back-end services: functions return
153
+ `Promise<Data>` or `AsyncGenerator<Data>`.
154
+
155
+ ---
156
+
157
+ ## Verify
158
+
159
+ When any `src/**/services/**/*.ts` file is in the diff, confirm all three invariants:
160
+
161
+ 1. **Interface/implementation separation** — every `.ts` file added or modified in `<name>-service/` is either `<name>-service.ts`,
162
+ `public.ts`, or inside a named sub-folder. Any file at the root of the folder that is neither is a violation.
163
+ 2. **`public.ts` completeness** — `public.ts` re-exports the factory function (and any other public exports from the sub-folder).
164
+ 3. **Interface file purity** — `<name>-service.ts` contains no imports from implementation sub-folders.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  paths:
3
- - 'packages/**/*.ts'
4
- - 'packages/**/*.tsx'
3
+ - '**/*.ts'
4
+ - '**/*.tsx'
5
5
  ---
6
6
 
7
7
  # Type casts — only two valid uses
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adobe-data-ai",
3
- "version": "0.9.86",
3
+ "version": "0.9.87",
4
4
  "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
5
5
  "author": {
6
6
  "name": "Adobe"
package/README.md CHANGED
@@ -76,5 +76,5 @@ This presumes you are already running the agent within the directory you want to
76
76
 
77
77
  ```
78
78
  Find the @adobe/data-ai package on npmjs and then install it in the current directory.
79
- Next use it's /build-application skill to build <promt here>
80
- ```
79
+ Next use it's /build-application skill to build <prompt here>
80
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/data-ai",
3
- "version": "0.9.86",
3
+ "version": "0.9.87",
4
4
  "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,12 +29,12 @@
29
29
  "skills"
30
30
  ],
31
31
  "peerDependencies": {
32
- "@adobe/data": "0.9.86"
32
+ "@adobe/data": "0.9.87"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^5.8.3",
36
36
  "vitest": "^1.6.0",
37
- "@adobe/data": "0.9.86"
37
+ "@adobe/data": "0.9.87"
38
38
  },
39
39
  "license": "MIT",
40
40
  "scripts": {
@@ -1,60 +0,0 @@
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.