yarramate 1.21.0 → 1.22.1

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.
@@ -110,15 +110,28 @@ export declare function nestingTree(edges: readonly FoldEdge[], nesting: readonl
110
110
  /**
111
111
  * The containment tree: what a view nests, plus what a pattern owns.
112
112
  *
113
- * A slot member joins the tree only when all three hold, and each condition is
114
- * a different way of getting the answer wrong:
113
+ * A slot member joins the tree only when all of these hold, and each condition
114
+ * is a different way of getting the answer wrong:
115
115
  *
116
- * - **Exclusive.** A subject bound into two instances has two owners, and a
117
- * single-parent tree would silently pick one. Shared subjects stay outside.
118
- * - **`owned` or `unwired`, never `context`.** A context slot names something
119
- * the instance USES and does not contain the upstream API it calls, the
120
- * plane it runs on. Folding those would swallow half the landscape into
121
- * whichever box happened to reference it.
116
+ * - **Held inside one box.** A member's HOLDERS are every instance whose slots
117
+ * name it. One holder puts the member in that holder. Several put it in their
118
+ * lowest common ancestor, which is the level at which the holders diverge and
119
+ * therefore the innermost box that contains all of them. Holders with no
120
+ * common ancestor leave the member outside, because there is no one box it
121
+ * sits within and a single-parent tree would have to pick.
122
+ *
123
+ * This AMENDS ADR 0143's "Exclusive" rule, which kept every shared subject
124
+ * outside (#473 phase 3, ADR 0145, Nabeel's decision of 2026-09-05). The
125
+ * original reasoning was that two owners force a silent choice; it is only
126
+ * true when the owners sit in different boxes. Where both already sit under
127
+ * one box there is nothing to choose, and the old rule left 14 of the
128
+ * reference Landscape's 30 data objects outside the single application whose
129
+ * own parts were the things binding them.
130
+ * - **`owned` or `unwired`, never `context` alone.** A context slot names
131
+ * something the instance USES and does not contain — the upstream API it
132
+ * calls, the plane it runs on. Folding those would swallow half the landscape
133
+ * into whichever box happened to reference it. At least one binding must be
134
+ * `owned` or `unwired` for the member to fold at all.
122
135
  * - **Not a ruling.** See {@link RULING_CORE_KINDS}.
123
136
  *
124
137
  * A view's own nesting wins where both apply: the view is the more specific
package/dist/fold-tree.js CHANGED
@@ -127,6 +127,18 @@ export function nestingTree(edges, nesting, coreKindOf) {
127
127
  * a straight-line ancestor of a cycle is still validly nested under its own
128
128
  * non-cyclic parent. Mutates `parentOf` and returns what it removed.
129
129
  */
130
+ /** Whether `id` sits anywhere inside `ancestor` in the tree built so far. */
131
+ const isDescendantOf = (id, ancestor, parentOf) => {
132
+ const seen = new Set([id]);
133
+ let current = parentOf.get(id);
134
+ while (current !== undefined && !seen.has(current)) {
135
+ if (current === ancestor)
136
+ return true;
137
+ seen.add(current);
138
+ current = parentOf.get(current);
139
+ }
140
+ return false;
141
+ };
130
142
  function unnestCycles(parentOf) {
131
143
  const cycleMembers = new Set();
132
144
  for (const start of parentOf.keys()) {
@@ -149,18 +161,59 @@ function unnestCycles(parentOf) {
149
161
  parentOf.delete(id);
150
162
  return [...cycleMembers];
151
163
  }
164
+ /**
165
+ * The lowest node that contains every one of `ids`, counting each id as an
166
+ * ancestor of itself, or `undefined` when they do not share one.
167
+ *
168
+ * "Counting each id as an ancestor of itself" is the part that matters: where
169
+ * one holder already sits inside another, the answer is the outer holder rather
170
+ * than something above them both.
171
+ */
172
+ const lowestCommonAncestor = (ids, parentOf) => {
173
+ const chainOf = (id) => {
174
+ const chain = [];
175
+ const seen = new Set();
176
+ let current = id;
177
+ while (current !== undefined && !seen.has(current)) {
178
+ seen.add(current);
179
+ chain.push(current);
180
+ current = parentOf.get(current);
181
+ }
182
+ return chain;
183
+ };
184
+ const [first, ...rest] = ids;
185
+ if (first === undefined)
186
+ return undefined;
187
+ const others = rest.map((id) => new Set(chainOf(id)));
188
+ // Walking the first chain from the node OUTWARDS makes the first hit the
189
+ // lowest by construction.
190
+ return chainOf(first).find((candidate) => others.every((chain) => chain.has(candidate)));
191
+ };
152
192
  /**
153
193
  * The containment tree: what a view nests, plus what a pattern owns.
154
194
  *
155
- * A slot member joins the tree only when all three hold, and each condition is
156
- * a different way of getting the answer wrong:
195
+ * A slot member joins the tree only when all of these hold, and each condition
196
+ * is a different way of getting the answer wrong:
157
197
  *
158
- * - **Exclusive.** A subject bound into two instances has two owners, and a
159
- * single-parent tree would silently pick one. Shared subjects stay outside.
160
- * - **`owned` or `unwired`, never `context`.** A context slot names something
161
- * the instance USES and does not contain the upstream API it calls, the
162
- * plane it runs on. Folding those would swallow half the landscape into
163
- * whichever box happened to reference it.
198
+ * - **Held inside one box.** A member's HOLDERS are every instance whose slots
199
+ * name it. One holder puts the member in that holder. Several put it in their
200
+ * lowest common ancestor, which is the level at which the holders diverge and
201
+ * therefore the innermost box that contains all of them. Holders with no
202
+ * common ancestor leave the member outside, because there is no one box it
203
+ * sits within and a single-parent tree would have to pick.
204
+ *
205
+ * This AMENDS ADR 0143's "Exclusive" rule, which kept every shared subject
206
+ * outside (#473 phase 3, ADR 0145, Nabeel's decision of 2026-09-05). The
207
+ * original reasoning was that two owners force a silent choice; it is only
208
+ * true when the owners sit in different boxes. Where both already sit under
209
+ * one box there is nothing to choose, and the old rule left 14 of the
210
+ * reference Landscape's 30 data objects outside the single application whose
211
+ * own parts were the things binding them.
212
+ * - **`owned` or `unwired`, never `context` alone.** A context slot names
213
+ * something the instance USES and does not contain — the upstream API it
214
+ * calls, the plane it runs on. Folding those would swallow half the landscape
215
+ * into whichever box happened to reference it. At least one binding must be
216
+ * `owned` or `unwired` for the member to fold at all.
164
217
  * - **Not a ruling.** See {@link RULING_CORE_KINDS}.
165
218
  *
166
219
  * A view's own nesting wins where both apply: the view is the more specific
@@ -178,22 +231,64 @@ export function foldTree(input) {
178
231
  else
179
232
  instances.add(membership.instance);
180
233
  }
181
- const parentOf = new Map(fromNesting.parentOf);
234
+ // Whether ANY of a member's bindings is one the instance holds it out by. A
235
+ // member bound only through context slots never folds, however many hold it.
236
+ const heldOutSomewhere = new Set();
182
237
  for (const membership of input.memberships) {
183
- if (parentOf.has(membership.member))
184
- continue;
185
- if ((instancesOf.get(membership.member)?.size ?? 0) !== 1)
186
- continue;
187
- if (membership.wiring === 'context')
188
- continue;
189
- if (RULING_CORE_KINDS.has(coreKindOf(membership.member)))
190
- continue;
191
- // A node the input does not carry cannot be drawn inside anything.
192
- if (!coreKindById.has(membership.member))
193
- continue;
194
- if (membership.member === membership.instance)
195
- continue;
196
- parentOf.set(membership.member, membership.instance);
238
+ if (membership.wiring !== 'context')
239
+ heldOutSomewhere.add(membership.member);
240
+ }
241
+ const parentOf = new Map(fromNesting.parentOf);
242
+ const candidates = [
243
+ ...new Set(input.memberships
244
+ .map(({ member }) => member)
245
+ .filter((member) =>
246
+ // A view's own nesting already placed it, and the view wins.
247
+ !parentOf.has(member) &&
248
+ heldOutSomewhere.has(member) &&
249
+ !RULING_CORE_KINDS.has(coreKindOf(member)) &&
250
+ // A node the input does not carry cannot be drawn inside anything.
251
+ coreKindById.has(member) &&
252
+ // Something that holds itself is not held by anything.
253
+ instancesOf.get(member)?.has(member) !== true)),
254
+ ];
255
+ // Resolved in ROUNDS rather than one pass, because a member's holders may
256
+ // themselves be members whose own parents are decided here. Placing a member
257
+ // before its holders are settled would measure the lowest common ancestor
258
+ // against a tree that is still missing the levels that separate them, and the
259
+ // reference has five-deep chains (spec, mapping, call, client, application),
260
+ // so this is exercised rather than theoretical.
261
+ //
262
+ // A member is settled once it is placed or once it is known to stay outside.
263
+ // Whatever a round cannot decide it hands to the next; when a round decides
264
+ // nothing, what is left is a mutual dependency and stays outside, which is the
265
+ // same answer the cycle guard below would reach for it anyway.
266
+ let pending = candidates;
267
+ while (pending.length > 0) {
268
+ const deferred = [];
269
+ let decided = false;
270
+ for (const member of pending) {
271
+ const holders = [...(instancesOf.get(member) ?? [])];
272
+ if (holders.some((holder) => pending.includes(holder) && holder !== member)) {
273
+ deferred.push(member);
274
+ continue;
275
+ }
276
+ decided = true;
277
+ const parent = holders.length === 1
278
+ ? holders[0]
279
+ : lowestCommonAncestor(holders, parentOf);
280
+ if (parent === undefined || parent === member)
281
+ continue;
282
+ // A member that already contains one of its holders cannot also sit
283
+ // inside it. The cycle guard below is the backstop, not the rule.
284
+ if (holders.some((holder) => isDescendantOf(holder, member, parentOf))) {
285
+ continue;
286
+ }
287
+ parentOf.set(member, parent);
288
+ }
289
+ if (!decided)
290
+ break;
291
+ pending = deferred;
197
292
  }
198
293
  // Slot membership can close a loop the view's nesting alone did not, so the
199
294
  // guard runs again over the combined tree rather than trusting the first.