@vworlds/vecs 1.0.45 → 1.0.46

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 (136) hide show
  1. package/README.md +7 -1
  2. package/dist/component.d.ts +57 -60
  3. package/dist/component.js +24 -17
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +67 -79
  6. package/dist/component_meta.js +331 -37
  7. package/dist/component_meta.js.map +1 -1
  8. package/dist/dsl.js +1 -1
  9. package/dist/dsl.js.map +1 -1
  10. package/dist/entity/entity.base.js +4 -0
  11. package/dist/entity/entity.base.js.map +1 -1
  12. package/dist/entity/entity.components.d.ts +39 -41
  13. package/dist/entity/entity.components.js +249 -82
  14. package/dist/entity/entity.components.js.map +1 -1
  15. package/dist/entity/entity.d.ts +1 -2
  16. package/dist/entity/entity.js +2 -3
  17. package/dist/entity/entity.js.map +1 -1
  18. package/dist/entity/entity.lifecycle.d.ts +7 -1
  19. package/dist/entity/entity.lifecycle.js +40 -50
  20. package/dist/entity/entity.lifecycle.js.map +1 -1
  21. package/dist/entity/entity.queries.js +30 -0
  22. package/dist/entity/entity.queries.js.map +1 -1
  23. package/dist/entity/entity.relationships.d.ts +4 -0
  24. package/dist/entity/entity.relationships.js +18 -10
  25. package/dist/entity/entity.relationships.js.map +1 -1
  26. package/dist/index.d.ts +7 -4
  27. package/dist/index.js +4 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/inject.d.ts +63 -14
  30. package/dist/inject.js +165 -89
  31. package/dist/inject.js.map +1 -1
  32. package/dist/modules/identity.js +2 -2
  33. package/dist/modules/identity.js.map +1 -1
  34. package/dist/modules/implements.js +2 -2
  35. package/dist/modules/implements.js.map +1 -1
  36. package/dist/modules/pipeline.js +5 -5
  37. package/dist/modules/pipeline.js.map +1 -1
  38. package/dist/modules/relationship_types.d.ts +70 -6
  39. package/dist/modules/relationship_types.js +70 -6
  40. package/dist/modules/relationship_types.js.map +1 -1
  41. package/dist/modules/relationships.d.ts +31 -9
  42. package/dist/modules/relationships.js +120 -44
  43. package/dist/modules/relationships.js.map +1 -1
  44. package/dist/modules/singleton.js +2 -2
  45. package/dist/modules/singleton.js.map +1 -1
  46. package/dist/modules/snapshot.d.ts +26 -0
  47. package/dist/modules/snapshot.js +45 -0
  48. package/dist/modules/snapshot.js.map +1 -0
  49. package/dist/modules/snapshot_types.d.ts +85 -0
  50. package/dist/modules/snapshot_types.js +130 -0
  51. package/dist/modules/snapshot_types.js.map +1 -0
  52. package/dist/package.json +2 -2
  53. package/dist/query/callbacks.d.ts +9 -7
  54. package/dist/query/callbacks.js +27 -41
  55. package/dist/query/callbacks.js.map +1 -1
  56. package/dist/query/grouped_query.js +7 -7
  57. package/dist/query/grouped_query.js.map +1 -1
  58. package/dist/query/grouping.d.ts +6 -2
  59. package/dist/query/grouping.js +84 -15
  60. package/dist/query/grouping.js.map +1 -1
  61. package/dist/query/query.d.ts +4 -4
  62. package/dist/query/query.js +37 -14
  63. package/dist/query/query.js.map +1 -1
  64. package/dist/snapshot/capture.d.ts +1 -0
  65. package/dist/snapshot/capture.js +212 -0
  66. package/dist/snapshot/capture.js.map +1 -0
  67. package/dist/snapshot/clone.d.ts +1 -0
  68. package/dist/snapshot/clone.js +107 -0
  69. package/dist/snapshot/clone.js.map +1 -0
  70. package/dist/snapshot/index.d.ts +5 -0
  71. package/dist/snapshot/index.js +21 -0
  72. package/dist/snapshot/index.js.map +1 -0
  73. package/dist/snapshot/preflight.d.ts +1 -0
  74. package/dist/snapshot/preflight.js +40 -0
  75. package/dist/snapshot/preflight.js.map +1 -0
  76. package/dist/snapshot/restore.d.ts +1 -0
  77. package/dist/snapshot/restore.js +111 -0
  78. package/dist/snapshot/restore.js.map +1 -0
  79. package/dist/snapshot/types.d.ts +135 -0
  80. package/dist/snapshot/types.js +26 -0
  81. package/dist/snapshot/types.js.map +1 -0
  82. package/dist/system.d.ts +3 -1
  83. package/dist/system.js +33 -23
  84. package/dist/system.js.map +1 -1
  85. package/dist/terms/all_term.js +19 -0
  86. package/dist/terms/all_term.js.map +1 -1
  87. package/dist/terms/any_term.js +19 -0
  88. package/dist/terms/any_term.js.map +1 -1
  89. package/dist/terms/not_term.js +4 -0
  90. package/dist/terms/not_term.js.map +1 -1
  91. package/dist/terms/target_term.d.ts +1 -2
  92. package/dist/terms/target_term.js +2 -4
  93. package/dist/terms/target_term.js.map +1 -1
  94. package/dist/terms/term.d.ts +2 -0
  95. package/dist/terms/term.js +59 -10
  96. package/dist/terms/term.js.map +1 -1
  97. package/dist/util/array_map.d.ts +43 -12
  98. package/dist/util/array_map.js +90 -41
  99. package/dist/util/array_map.js.map +1 -1
  100. package/dist/util/dense_set.js +26 -0
  101. package/dist/util/dense_set.js.map +1 -1
  102. package/dist/world/world.base.js +12 -0
  103. package/dist/world/world.base.js.map +1 -1
  104. package/dist/world/world.d.ts +7 -0
  105. package/dist/world/world.deferred.js +17 -0
  106. package/dist/world/world.deferred.js.map +1 -1
  107. package/dist/world/world.entities.d.ts +13 -2
  108. package/dist/world/world.entities.js +16 -2
  109. package/dist/world/world.entities.js.map +1 -1
  110. package/dist/world/world.js +15 -3
  111. package/dist/world/world.js.map +1 -1
  112. package/dist/world/world.pipeline.d.ts +2 -2
  113. package/dist/world/world.pipeline.js +4 -2
  114. package/dist/world/world.pipeline.js.map +1 -1
  115. package/dist/world/world.queries.d.ts +7 -0
  116. package/dist/world/world.queries.js +52 -0
  117. package/dist/world/world.queries.js.map +1 -1
  118. package/dist/world/world.transfers.d.ts +137 -0
  119. package/dist/world/world.transfers.js +188 -0
  120. package/dist/world/world.transfers.js.map +1 -0
  121. package/docs/README.md +18 -6
  122. package/docs/components.md +90 -53
  123. package/docs/concepts.md +21 -17
  124. package/docs/design-guide.md +125 -94
  125. package/docs/entities.md +62 -76
  126. package/docs/execution-model.md +26 -10
  127. package/docs/getting-started.md +6 -6
  128. package/docs/glossary.md +19 -10
  129. package/docs/modules.md +8 -8
  130. package/docs/queries-and-filters.md +15 -10
  131. package/docs/relationships.md +245 -124
  132. package/docs/snapshots.md +181 -0
  133. package/docs/systems.md +64 -21
  134. package/docs/utilities.md +10 -6
  135. package/package.json +2 -2
  136. package/skills/vecs/SKILL.md +5 -4
@@ -49,9 +49,9 @@ world.system("Render").phase(ON_STORE).with(Sprite, Position).each([Sprite, Posi
49
49
  ```
50
50
 
51
51
  `.phase(anchor)` accepts a phase name or an entity. It sets `DependsOn` on the system's backing
52
- entity (replacing any previous target) and tags the anchor with `Phase`, creating the anchor
53
- entity if the name is new. **A system lives in exactly one phase.** If a concern needs work in two
54
- phases, split it into two systems.
52
+ entity (atomically replacing its previous exclusive edge) and tags the anchor with `Phase`,
53
+ creating the anchor entity if the name is new. **A system lives in exactly one phase.** If a concern
54
+ needs work in two phases, split it into two systems.
55
55
 
56
56
  ### Ordering guarantees
57
57
 
@@ -81,7 +81,7 @@ to the spec so the result flattens to systems.
81
81
  ### Custom phases: `insertPhaseAfter`
82
82
 
83
83
  `insertPhaseAfter(phase, anchor)` splices an existing `Phase` entity into the spine directly after
84
- `anchor`, retargeting the anchor's downstream phases onto the new one:
84
+ `anchor`, replacing the anchor's downstream `DependsOn` edges with edges to the new one:
85
85
 
86
86
  ```ts
87
87
  import { insertPhaseAfter, ON_UPDATE, Phase } from "@vworlds/vecs";
@@ -98,8 +98,14 @@ built-in taxonomy.
98
98
 
99
99
  Inside a system body, a `Query.forEach` / `Filter.forEach` iteration, or any `world.defer(...)`
100
100
  block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `ensure` /
101
- `ensureTarget` / `remove` / `destroy` / `modified`, and `world.entity()` creation) are queued as
102
- commands instead of applied inline. The queue drains when the scope that opened deferral ends.
101
+ `remove` / `destroy` / `modified`, explicit relationship edge additions/removals, and
102
+ `world.entity()` creation) are queued as commands instead of applied inline. The queue drains when
103
+ the scope that opened deferral ends.
104
+
105
+ All commands share one FIFO queue, including relationship edge commands and commands enqueued by
106
+ hooks while that queue is draining. Their final state is therefore determined by call order. For
107
+ example, deferred `add(R, a)`, `add(R, b)`, `remove(R, a)` leaves only `b`; on an `Exclusive`
108
+ relationship each queued addition atomically replaces the edge present when that command executes.
103
109
 
104
110
  For the pipeline this means: **a structural change made by system A is applied in the flush
105
111
  immediately after A finishes, so it is visible to every later system in the same tick** — later in
@@ -119,8 +125,10 @@ While a mutation is queued but not yet applied, reads return the pre-mutation st
119
125
  | `entity.ensure(C)` | A mutable component, even when newly created. |
120
126
  | `entity.remove(C)` | Still the component instance. |
121
127
 
122
- `entity.ensureTarget(R)` similarly returns one shared pending target while `entity.target(R)`
123
- continues to return the previously committed target until flush.
128
+ Relationship reads follow the same committed-state rule. After queued `add(R, target)` or
129
+ `remove(R, target)`, `has(R)`, `target(R)`, `targets(R)`, `hasTarget(R, target)`, and reverse
130
+ `children(R)` continue to report the state from before the deferred scope until the FIFO queue
131
+ applies those edge commands.
124
132
 
125
133
  Outside any deferred scope (top-level user code), the same calls execute inline and effects are
126
134
  visible immediately.
@@ -146,6 +154,8 @@ component:
146
154
 
147
155
  - **Hooks** (`onAdd` / `onSet` / `onRemove`) fire synchronously at the moment the command is
148
156
  applied — inline outside deferred mode, or during the drain.
157
+ - **Relationship edge hooks** (`onTargetAdd` / `onTargetRemove`) fire synchronously after each
158
+ edge and reverse-index change. Edge changes do not fire `onSet`.
149
159
  - **Standalone `Query` callbacks** (`enter` / `update` / `exit`) also fire synchronously during
150
160
  routing.
151
161
  - **Systems** queue events into an ordered **inbox** instead, and replay the inbox at the top of
@@ -157,8 +167,14 @@ Three routing rules worth memorizing:
157
167
  - **`update(C)` is change-only unless you opt into entry.** When an entity enters a system,
158
168
  `{ onEnter: true }` update watchers push an `update` event for already-present watched components.
159
169
  Use that for initialization; leave it off when a separate `enter` owns setup.
160
- - **`exit` reads a snapshot.** Components injected into an `exit` callback are captured when the
161
- exit is routed, so they are still resolvable even though they are being (or have been) removed.
170
+ - **`enter` and `exit` injection read snapshots.** Direct components, singular/plural target
171
+ projections, and `Iter.src` are captured when the event is routed, so inbox replay retains those
172
+ component and entity references even if storage or edges change before the system runs. Component
173
+ objects are not deep-cloned.
174
+ - **`enter` is drain-time truthful.** A system only receives `enter` when the entity is still a
175
+ member when that system runs. If it left before the inbox drains, vecs drops both that queued
176
+ `enter` and its paired `exit`; an entity that re-enters before draining receives the ordered
177
+ lifecycle events for its final membership.
162
178
  - **In-place mutation needs `modified`.** Assigning to component fields directly notifies no one.
163
179
  Call `entity.modified(C)` (or use `set` / `getMut` / `ensure`) to route the change.
164
180
 
@@ -109,12 +109,12 @@ entity:
109
109
  ```ts
110
110
  world
111
111
  .component(Health)
112
- .onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
113
- .onRemove((entity) => console.log(`entity ${entity.eid} died`));
112
+ .on("add", (entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
113
+ .on("remove", (entity) => console.log(`entity ${entity.eid} died`));
114
114
  ```
115
115
 
116
- `onAdd` fires when the component is first attached, `onRemove` when it is detached or its entity is
117
- destroyed, and `onSet` whenever its data is set or marked modified.
116
+ The `add` event fires when the component is first attached, `remove` when it is detached or its
117
+ entity is destroyed, and `set` whenever its data is set or marked modified.
118
118
 
119
119
  ## 6. Spawn entities
120
120
 
@@ -167,8 +167,8 @@ world.component(Position);
167
167
  world.component(Velocity);
168
168
  world
169
169
  .component(Health)
170
- .onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
171
- .onRemove((entity) => console.log(`entity ${entity.eid} died`));
170
+ .on("add", (entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
171
+ .on("remove", (entity) => console.log(`entity ${entity.eid} died`));
172
172
 
173
173
  world
174
174
  .system("Move")
package/docs/glossary.md CHANGED
@@ -25,7 +25,7 @@ sources strip/die). [Components](./components.md#cleanup-policies-destroying-a-c
25
25
 
26
26
  **Companion (wrapper) component** — a local, non-networked component holding a non-ECS resource
27
27
  (render object, WASM handle) so the entity's component set drives the resource lifecycle.
28
- [Components](./components.md#companion-components-for-non-ecs-resources).
28
+ [Components](./components.md#resources-as-components-non-ecs-objects).
29
29
 
30
30
  **Component** — a registered plain data class with a no-argument constructor; pure data, no
31
31
  behavior. [Components](./components.md).
@@ -44,6 +44,10 @@ pipeline). [Execution model](./execution-model.md#deferred-mutation-and-flush-bo
44
44
  **Exclusive components** — a declared group where at most one member may exist on an entity;
45
45
  setting one removes the others. [Components](./components.md#exclusive-component-groups).
46
46
 
47
+ **Exclusive relationship** — a relationship carrying the `Exclusive` trait, restricting each
48
+ source to at most one target. Adding a different edge replaces the current edge. This is separate
49
+ from an exclusive component group. [Relationships](./relationships.md).
50
+
47
51
  **Events** — a minimal typed event emitter helper exported for package-level events. It is not an
48
52
  entity lifecycle API. [Utilities](./utilities.md#events).
49
53
 
@@ -51,8 +55,8 @@ entity lifecycle API. [Utilities](./utilities.md#events).
51
55
  [Queries and filters](./queries-and-filters.md#filters-one-shot-scans).
52
56
 
53
57
  **Hook** — an `onAdd` / `onSet` / `onRemove` callback registered per component class, or an
54
- `onRetarget` callback for relationships, fired synchronously at the mutation site.
55
- [Components](./components.md#hooks-onadd-onset-onremove-onretarget).
58
+ `onTargetAdd` / `onTargetRemove` callback for relationship edges, fired synchronously at the
59
+ mutation site. [Components](./components.md#hooks-component-and-relationship-edges).
56
60
 
57
61
  **Implements / interface alias** — a declaration that a concrete component is queryable through
58
62
  abstract/interface component types; aliases are read-only views of the same instance.
@@ -62,7 +66,8 @@ abstract/interface component types; aliases are read-only views of the same inst
62
66
  top of the system's run. [Execution model](./execution-model.md#reactive-routing).
63
67
 
64
68
  **Injection** — resolving component instances per entity into a typed callback tuple, including
65
- `target` (relationship target's components) and `down` (fan-out over children) markers.
69
+ singular `target` (the first relationship target), plural `targets` (one projection row per target),
70
+ and `down` (fan-out over children) markers.
66
71
  [Systems](./systems.md#component-injection).
67
72
 
68
73
  **Iter** — an opt-in reusable cursor passed to callbacks instead of the bare entity, exposing the
@@ -72,9 +77,10 @@ visited entity and the source entity of each injected slot.
72
77
  **Module** — a packaging unit (`class M extends Module<Config>`) whose `init()` registers
73
78
  components, systems, and policy once per world. [Modules](./modules.md).
74
79
 
75
- **Owned target entity** — a zero-or-one aggregate entity targeted through a relationship carrying
76
- the `OwnedTarget` trait. It is parented to the source and destroyed when displaced, when the
77
- relationship is removed, or when the source dies. [Relationships](./relationships.md#owned-targets).
80
+ **Owned target entity** — an explicitly created zero-or-one aggregate entity targeted through a
81
+ relationship carrying both `Exclusive` and `OwnedTarget`. It is parented to the source and destroyed
82
+ when displaced, when its edge or relationship is removed, or when the source dies.
83
+ [Relationships](./relationships.md#owned-targets).
78
84
 
79
85
  **Phase** — an entity used as a pipeline ordering anchor (tagged with the `Phase` component);
80
86
  systems attach to exactly one phase via `DependsOn`.
@@ -90,8 +96,10 @@ ordered query over system entities. [Execution model](./execution-model.md#the-p
90
96
  `source` / `parent` / `children` / `test`).
91
97
  [Queries and filters](./queries-and-filters.md#the-query-dsl).
92
98
 
93
- **Relationship** — a component with a `target: Entity` field linking its source entity to a target,
94
- with reverse lookup (`children`) and cleanup cascades. [Relationships](./relationships.md).
99
+ **Relationship** — a component holding explicit edges from its source entity to one or more targets,
100
+ with `target` / `targets` / `hasTarget` lookup, reverse lookup (`children`), and cleanup cascades.
101
+ Target storage is private; mutate edges with `add(Rel, target)` and `remove(Rel, target)`.
102
+ [Relationships](./relationships.md).
95
103
 
96
104
  **Silence domain / `ignoreSource`** — a query configuration that drops `update` events originating
97
105
  from systems matching another query, breaking write feedback loops.
@@ -109,7 +117,8 @@ how often a system fires. [Systems](./systems.md#cadence-interval-rate-and-tick-
109
117
 
110
118
  **Trait (component trait)** — policy attached as a component to a registered component entity,
111
119
  configuring every use of that component type. Built-ins include `Singleton`, `Implements`,
112
- `Traversable`, and `OwnedTarget`. [Components](./components.md#component-traits-policy-on-component-entities).
120
+ `Exclusive`, `Traversable`, and `OwnedTarget`.
121
+ [Components](./components.md#component-traits-policy-on-component-entities).
113
122
 
114
123
  **Watchlist** — the set of component types a query or system reacts to via `update`, stored as a
115
124
  bitmask. [Systems](./systems.md#reactive-callbacks-enter-update-exit).
package/docs/modules.md CHANGED
@@ -62,7 +62,7 @@ is often one module per item plus a thin umbrella that composes them:
62
62
  ```ts
63
63
  export class PhaserRenderModule extends Module<{ scene: Scene }> {
64
64
  public init(cfg: { scene: Scene }): void {
65
- this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
65
+ this.world.component(GameObject).on("remove", (_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
66
66
  this.world.module(ArcModule, { scene: cfg.scene });
67
67
  this.world.module(RectangleModule, { scene: cfg.scene });
68
68
  // … shape modules first …
@@ -89,13 +89,13 @@ Guidelines:
89
89
  Every `World` constructor installs these (`lib/vecs/src/world/world.ts`); you interact with their
90
90
  _components_, not with the module objects:
91
91
 
92
- | Module | Provides |
93
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
- | `IdentityModule` | The `Identity` name component and the name → entity index behind `world.entity("name")` / `entity.name`. |
95
- | `ImplementsModule` | Maintains [`Implements` interface aliases](./components.md#interface-aliases-with-implements) and their transitive closure. |
96
- | `SingletonModule` | Enforces the [`Singleton`](./components.md#singletons-and-world-global-data) one-carrier constraint. |
97
- | `RelationshipsModule` | Registers `ChildOf`, installs [`OwnedTarget`](./relationships.md#owned-targets) ownership, and maintains [`Traversable` depth tracking](./relationships.md#traversal-order-traversable-and-cascade). |
98
- | `PipelineModule` | Creates the eight [built-in phases](./execution-model.md#phases-the-pipeline-spine) and installs the default pipeline query. |
92
+ | Module | Provides |
93
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `IdentityModule` | The `Identity` name component and the name → entity index behind `world.entity("name")` / `entity.name`. |
95
+ | `ImplementsModule` | Maintains [`Implements` interface aliases](./components.md#interface-aliases-with-implements) and their transitive closure. |
96
+ | `SingletonModule` | Enforces the [`Singleton`](./components.md#singletons-and-world-global-data) one-carrier constraint. |
97
+ | `RelationshipsModule` | Registers relationship traits, marks built-in single-target relationships `Exclusive`, installs explicit-edge [`OwnedTarget`](./relationships.md#owned-targets) ownership, and maintains exclusive [`Traversable` depth tracking](./relationships.md#traversal-order-traversable-and-cascade). |
98
+ | `PipelineModule` | Creates the eight [built-in phases](./execution-model.md#phases-the-pipeline-spine) and installs the default pipeline query. |
99
99
 
100
100
  All are exported, so a custom world setup can reuse them.
101
101
 
@@ -17,7 +17,7 @@ It is accepted by `query().with`, `system().with`, and `world.filter`:
17
17
  | `{ any: [q1, q2] }` | At least one sub-expression matches |
18
18
  | `{ not: q }` | Sub-expression must not match |
19
19
  | `{ only: [A, B] }` | Entity has exactly `A` and `B`, nothing else |
20
- | `{ target: [R, q] }` | Relationship `R`'s target matches `q` |
20
+ | `{ target: [R, q] }` | At least one of relationship `R`'s targets matches `q` |
21
21
  | `{ source: [R, q] }` | Filter-only: some entity targeting this one through `R` matches `q` |
22
22
  | `{ parent: q }` | Built-in `ChildOf` target matches `q` |
23
23
  | `{ children: q }` | Filter-only: some `ChildOf` child matches `q` |
@@ -36,12 +36,13 @@ Class-valued terms are components — register them first; an unregistered class
36
36
  whose changes should re-evaluate it.
37
37
 
38
38
  `target` and `source` take tuple form only: `{ target: [RelationshipClass, innerDSL] }`. The first
39
- element must extend `Relationship` (a plain component throws). `target` evaluates the inner DSL
40
- against the relationship's target; `source` evaluates it against each entity pointing at the
41
- candidate and matches when at least one does. `parent` / `children` are the same operators
42
- specialized to `ChildOf`. **`source` and `children` are non-reactive**: exact for each filter
43
- scan, but rejected by tracked queries and systems. `target` / `parent` are fully reactive — see
44
- [Relationships](./relationships.md#querying-through-relationships).
39
+ element must extend `Relationship` (a plain component throws). `target` is existential: it evaluates
40
+ the inner DSL against every relationship target and matches when at least one does. `source`
41
+ evaluates it against each entity pointing at the candidate and also matches when at least one does.
42
+ `parent` / `children` are the same operators specialized to the exclusive `ChildOf` relationship.
43
+ **`source` and `children` are non-reactive**: exact for each filter scan, but rejected by tracked
44
+ queries and systems. `target` / `parent` are fully reactive to edge changes and target-side changes
45
+ — see [Relationships](./relationships.md#querying-through-relationships).
45
46
 
46
47
  ### `QuerySpec`: adding `cascade` and `hint`
47
48
 
@@ -144,15 +145,19 @@ Two forms (`lib/vecs/src/query/grouped_query.ts`):
144
145
 
145
146
  - `groupBy([components], keyFn)` / `groupBy({ inject: [components] }, keyFn)` — bucket by a
146
147
  computed key; injected components are watched, so changing them re-keys the entity.
147
- - `groupBy(RelationshipClass)` / `groupBy({ relationship: RelationshipClass })` — bucket by the
148
- relationship's target entity. Moving between buckets is a retarget, a plain data change (see
149
- [Relationships](./relationships.md#retargeting)).
148
+ - `groupBy(RelationshipClass)` / `groupBy({ relationship: RelationshipClass })` — place each member
149
+ in one bucket per relationship target. Adding or removing edges reconciles only the affected
150
+ buckets; an exclusive replacement moves the member from the old target bucket to the new one.
150
151
 
151
152
  The result is a `GroupedQuery<K, R>` adding `group(key)`, `groups()`, `groupKeys()`, `groupCount`,
152
153
  and `onGroupEnter` / `onGroupExit` callbacks for bucket creation and disposal. Each `Group` exposes
153
154
  `key`, `count`, `has`, iteration, and injected `forEach`. Grouping implies tracking. Systems do
154
155
  not support `groupBy` — keep the grouped query standalone and read it from a system.
155
156
 
157
+ A many-target member can therefore appear in several groups at once. Re-adding an existing edge
158
+ does not duplicate it within a bucket, removing one edge leaves its other buckets intact, and
159
+ destroying a target removes that target's bucket according to normal relationship cleanup.
160
+
156
161
  ## Filters: one-shot scans
157
162
 
158
163
  `world.filter(spec)` returns a `Filter` (`lib/vecs/src/filter.ts`): a non-reactive predicate that