@yahoo/uds-create-config 5.58.0 → 5.59.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.
@@ -303,8 +303,8 @@ const LinkedSystemDefinition = defineEntity({
303
303
  * Draw entities from a linked system: the pin's `link` declaration grows to cover them and the
304
304
  * build mints a pointer row per entity, in this one op. A row of this config's own at a covered
305
305
  * path is deleted so theirs shows through; a row held through another pin becomes an extension
306
- * through this one when `onLinked` is `'take'`. `link-plan` says what that replaces before anything
307
- * is written.
306
+ * through this one when `onLinked` is `'take'`, and the other pin's `options` entry there goes with
307
+ * it. `link-plan` says what that replaces before anything is written.
308
308
  */
309
309
  link: {
310
310
  input: linkInput,
@@ -332,7 +332,7 @@ const LinkedSystemDefinition = defineEntity({
332
332
  input: linkInput,
333
333
  readOnly: true,
334
334
  label: "Plan a link",
335
- description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
335
+ description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), the `options` entries another linked system holds at what a take moves, which it removes since they would apply to nothing (`dropped`), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
336
336
  scope: "item",
337
337
  title: (entity) => `Plan a link from ${entity}`,
338
338
  example: linkExample,
@@ -22,6 +22,13 @@ interface LinkedElsewhere extends EntityAddress {
22
22
  /** The slug of the pin it comes through. */
23
23
  readonly from: string;
24
24
  }
25
+ /** An entry in another pin's `options` that a take leaves applying to nothing, and removes. */
26
+ interface DroppedDirective extends EntityAddress {
27
+ /** The slug of the pin whose entry it was. */
28
+ readonly from: string;
29
+ /** The accessor the entry sits under in that pin's `options`. */
30
+ readonly accessor: string;
31
+ }
25
32
  /** One row a link mints. `named` when a path the call named covers it; otherwise it comes along
26
33
  * because something linked points at it. */
27
34
  interface LinkArrival extends EntityAddress {
@@ -40,6 +47,10 @@ interface LinkPlan {
40
47
  readonly linked: readonly LinkedElsewhere[];
41
48
  /** Rows held through another pin, turned into extensions through this one. */
42
49
  readonly extended: readonly EntityAddress[];
50
+ /** The other pins' `options` entries at what the take moves, removed with it: they adjust only the
51
+ * rows their pin derives, and the row there follows this one now. Not carried onto the taken row,
52
+ * since an entry corrects what that pin brings rather than stating a value of this config's own. */
53
+ readonly dropped: readonly DroppedDirective[];
43
54
  /** Members already here from the source's own origin, left alone and kept out of the declaration. */
44
55
  readonly shared: readonly EntityAddress[];
45
56
  /** Paths another pin offers that a `hybrid` link reaches as a different entity. The other pin's
@@ -193,4 +204,4 @@ declare function sourceRowStates({
193
204
  readonly slug: string;
194
205
  }): SourceRowState[];
195
206
  //#endregion
196
- export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
207
+ export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, DroppedDirective, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
@@ -1,7 +1,8 @@
1
1
  import { collectRefs, isMintedRow, ref, sourceOf, splitRef } from "../../framework/utils/refs.js";
2
2
  import { isConfigRejection, rejectData } from "../../framework/rejection.js";
3
3
  import { blockedBy } from "../../framework/plan.js";
4
- import { excludedCondition } from "../../framework/source-integrity.js";
4
+ import { danglingDirectives, excludedCondition } from "../../framework/source-integrity.js";
5
+ import { sameOrigin } from "../../framework/Config.js";
5
6
  import { PIN_KIND, covers, declarationOf, entriesFor, excludePaths, excludes, includePaths, narrowDeclaration } from "./link-declaration.js";
6
7
  import { planCopy, sourceEntries } from "./copy-plan.js";
7
8
  //#region src/entities/system/link-plan.ts
@@ -40,7 +41,7 @@ function sharedWithOrigin({ config, source, slug, kind, path }) {
40
41
  kind,
41
42
  path
42
43
  });
43
- if (!theirs || !mine || theirs.key !== mine.key) return void 0;
44
+ if (!theirs || !mine || !sameOrigin(theirs, mine)) return void 0;
44
45
  return theirs.pin.displayName ?? theirs.key;
45
46
  }
46
47
  /** Another pin whose declaration covers the path and whose source has it: the pin the path would
@@ -237,11 +238,20 @@ function planLink({ config, input }) {
237
238
  }
238
239
  declared = Object.keys(next).length === 0 ? current : next;
239
240
  }
240
- const patches = [
241
+ const planned = [
241
242
  ...takes,
242
243
  ...declares,
243
244
  ...shadows
244
245
  ];
246
+ const { dropped, patches: drops } = takes.length > 0 ? directivesTakenFrom({
247
+ config,
248
+ slug,
249
+ patches: planned
250
+ }) : {
251
+ dropped: [],
252
+ patches: []
253
+ };
254
+ const patches = [...planned, ...drops];
245
255
  return {
246
256
  declared,
247
257
  ...outcome({
@@ -254,10 +264,74 @@ function planLink({ config, input }) {
254
264
  addresses,
255
265
  linked,
256
266
  extended,
267
+ dropped,
257
268
  shared,
258
269
  patches
259
270
  };
260
271
  }
272
+ /**
273
+ * The other pins' `options` entries a take leaves applying to nothing, and the pin edits that remove
274
+ * them.
275
+ *
276
+ * An entry adjusts the rows its pin derives. A take writes this config's own row at the path, one that
277
+ * follows `slug`, so the other pin's entry stays in its `options` reading as though it still applied,
278
+ * and the build refuses it ({@link danglingDirectives}). Read off the config the take leaves, so a
279
+ * folder whose members all moved counts the way the build will count it, and only what the take
280
+ * itself left behind: an entry that already applied to nothing is the build's to report, not this
281
+ * take's to remove.
282
+ *
283
+ * Nothing when the take is refused; `outcome` says why.
284
+ */
285
+ function directivesTakenFrom({ config, slug, patches }) {
286
+ let after;
287
+ try {
288
+ after = config.applyAll(patches).withDerivedRows();
289
+ } catch (error) {
290
+ if (!isConfigRejection(error)) throw error;
291
+ return {
292
+ dropped: [],
293
+ patches: []
294
+ };
295
+ }
296
+ const key = (at) => `${at.slug}\u0000${at.accessor}\u0000${at.path}`;
297
+ const already = new Set(danglingDirectives(config).map(key));
298
+ const dropped = [];
299
+ for (const at of danglingDirectives(after)) {
300
+ if (at.missing !== "row" || at.heldBy !== slug || already.has(key(at))) continue;
301
+ dropped.push({
302
+ kind: config.accessors()[at.accessor]?.kind ?? at.accessor,
303
+ path: at.path,
304
+ from: at.slug,
305
+ accessor: at.accessor
306
+ });
307
+ }
308
+ const edits = [];
309
+ for (const pin of new Set(dropped.map((at) => at.from))) {
310
+ const options = config.storedBody("linked-system", pin).options ?? {};
311
+ const data = {};
312
+ let emptied = 0;
313
+ for (const [accessor, entries] of Object.entries(options)) {
314
+ const going = dropped.filter((at) => at.from === pin && at.accessor === accessor).map((at) => at.path);
315
+ if (going.length === 0) continue;
316
+ if (Object.keys(entries ?? {}).filter((path) => !going.includes(path)).length > 0) {
317
+ for (const path of going) data[`options.${accessor}.${path}`] = null;
318
+ continue;
319
+ }
320
+ data[`options.${accessor}`] = null;
321
+ emptied += 1;
322
+ }
323
+ edits.push({
324
+ kind: PIN_KIND,
325
+ operation: "update",
326
+ path: pin,
327
+ data: emptied === Object.keys(options).length ? { options: null } : data
328
+ });
329
+ }
330
+ return {
331
+ dropped,
332
+ patches: edits
333
+ };
334
+ }
261
335
  /** What a plan reports of a refusal: its message, and the collisions when that's what it names. */
262
336
  function refusal(error) {
263
337
  const collisions = error.issues.flatMap((issue) => issue.collisions ?? []);
@@ -1972,10 +1972,7 @@ declare class Config {
1972
1972
  * kind's `sourceOrigin`, so the two pins may name different versions. `undefined` for a source of
1973
1973
  * one hop, or one whose system no pin here reaches.
1974
1974
  *
1975
- * The two pins match when they agree on every origin field both set, and share at least one. Not on
1976
- * the whole key: a pin by path is stamped with its sibling's id when it's pushed, so the Bridge's
1977
- * `yos` reads `configPath` and `systemId` where this config's pin to the same system names only the
1978
- * id — and a `configPath` is relative to the config holding it, so it can't be compared across two.
1975
+ * The two pins match by {@link sameOriginFields}.
1979
1976
  */
1980
1977
  private directRoute;
1981
1978
  /**
@@ -2569,11 +2566,15 @@ declare class Config {
2569
2566
  * to a system linked to a foundation answers with the foundation. A row a materialized artifact
2570
2567
  * flattened to a literal has no pointer left, so the pin whose `link` declaration covers the path
2571
2568
  * stands in, provided the row still reads as the source's ({@link matchesSource}); one the
2572
- * publisher changed is its own. Two answers with one `key` are one entity reached by two routes,
2573
- * whatever versions the pins name: the key is the pin kind's `sourceOrigin`, not its identity.
2569
+ * publisher changed is its own, and so is a linked row this config lays a value of its own over: the
2570
+ * Bridge's `spectrum/blue/0` points at Yahoo OS's with the Bridge's colours on it, which is the
2571
+ * Bridge's entity, not a second route to Yahoo OS's. Two answers that are {@link sameOrigin} are one
2572
+ * entity reached by two routes, whatever versions the pins name: they compare the pin kind's
2573
+ * `sourceOrigin`, not its identity.
2574
2574
  *
2575
2575
  * `pin` is the pin entity at the hop where the answer was found, for a surface that names the
2576
- * origin the way it names any source.
2576
+ * origin the way it names any source. `key` is its origin fields joined, for a surface with no
2577
+ * name to show.
2577
2578
  */
2578
2579
  originOf({
2579
2580
  kind,
@@ -2581,10 +2582,7 @@ declare class Config {
2581
2582
  }: {
2582
2583
  kind: string;
2583
2584
  path: string;
2584
- }): {
2585
- key: string;
2586
- pin: Entity;
2587
- } | undefined;
2585
+ }): Origin | undefined;
2588
2586
  /**
2589
2587
  * The pin whose `link` declaration covers `kind:path` while the row there still reads as that
2590
2588
  * source's: how a row a materialized artifact flattened to a literal is traced back to the pin it
@@ -2899,5 +2897,14 @@ declare class Config {
2899
2897
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
2900
2898
  protected hydrateFrom(stored: StoredConfig, options?: HydrationOptions): this;
2901
2899
  }
2900
+ /** Where an entity originates ({@link Config.originOf}): the pin at the hop that answered, and its origin fields. */
2901
+ interface Origin {
2902
+ readonly key: string;
2903
+ /** The pin kind's `sourceOrigin` fields as the pin reads them, `''` where unset. */
2904
+ readonly fields: readonly string[];
2905
+ readonly pin: Entity;
2906
+ }
2907
+ /** Whether two {@link Config.originOf} answers are one origin, by {@link sameOriginFields}. */
2908
+ declare function sameOrigin(a: Origin, b: Origin): boolean;
2902
2909
  //#endregion
2903
- export { AppliedOp, AttachedSources, BatchReceipt, Config, CssGrammar, Edition, HydrationOptions, ListOptions, LiveConfig, Patch, RecordedSource, SERIALIZED_CONFIG_VERSION, SerializedConfig, SettledSource, SkippedOp, SourceCollision, SourcesBySlug, StoredConfig, SubPatch, TreeNode };
2910
+ export { AppliedOp, AttachedSources, BatchReceipt, Config, CssGrammar, Edition, HydrationOptions, ListOptions, LiveConfig, Origin, Patch, RecordedSource, SERIALIZED_CONFIG_VERSION, SerializedConfig, SettledSource, SkippedOp, SourceCollision, SourcesBySlug, StoredConfig, SubPatch, TreeNode, sameOrigin };
@@ -3992,10 +3992,7 @@ var Config = class Config {
3992
3992
  * kind's `sourceOrigin`, so the two pins may name different versions. `undefined` for a source of
3993
3993
  * one hop, or one whose system no pin here reaches.
3994
3994
  *
3995
- * The two pins match when they agree on every origin field both set, and share at least one. Not on
3996
- * the whole key: a pin by path is stamped with its sibling's id when it's pushed, so the Bridge's
3997
- * `yos` reads `configPath` and `systemId` where this config's pin to the same system names only the
3998
- * id — and a `configPath` is relative to the config holding it, so it can't be compared across two.
3995
+ * The two pins match by {@link sameOriginFields}.
3999
3996
  */
4000
3997
  directRoute(source) {
4001
3998
  const hops = sourceHops(source);
@@ -4009,8 +4006,7 @@ var Config = class Config {
4009
4006
  if (theirs === void 0) continue;
4010
4007
  const direct = this.sourceSlugs().find((slug) => {
4011
4008
  const mine = this.sourceFields(sourceKind, slug, "sourceOrigin");
4012
- const shared = theirs.flatMap((value, field) => value !== "" && mine?.[field] ? [field] : []);
4013
- return shared.length > 0 && shared.every((field) => theirs[field] === mine?.[field]);
4009
+ return mine !== void 0 && sameOriginFields(theirs, mine);
4014
4010
  });
4015
4011
  if (direct !== void 0) return direct;
4016
4012
  }
@@ -5142,6 +5138,40 @@ var Config = class Config {
5142
5138
  const folders = [];
5143
5139
  /** Folders this pass leaves as they stand — already held, over rows it still claims. */
5144
5140
  const standing = /* @__PURE__ */ new Set();
5141
+ const taken = [];
5142
+ for (const kind of this.ownedKinds()) for (const [path, stored] of this.state.snapshot.slice(kind).items) {
5143
+ const body = stored.toJSON();
5144
+ if (!isExtendedRef(body) || isMintedRow(body)) continue;
5145
+ if (pathOf(body) !== path) continue;
5146
+ const slug = sourceOf(body);
5147
+ if (slug !== void 0) taken.push({
5148
+ kind,
5149
+ path,
5150
+ slug
5151
+ });
5152
+ }
5153
+ /** Every folder a pin has a member under this pass, claimed or taken, as `slug kind path`. */
5154
+ const occupied = /* @__PURE__ */ new Set();
5155
+ const occupy = (slug, kind, path) => {
5156
+ const segments = path.split("/");
5157
+ for (let depth = 1; depth < segments.length; depth++) occupied.add(`${slug}\u0000${kind}\u0000${segments.slice(0, depth).join("/")}`);
5158
+ };
5159
+ for (const [target, slug] of claimedFrom) occupy(slug, kindOf(target), pathOf(target));
5160
+ for (const { kind, path, slug } of taken) occupy(slug, kind, path);
5161
+ /**
5162
+ * Whether a folder already here stays as it is. One this config authored does, and so does one
5163
+ * minted through a pin that still has a member under it. One minted through a pin that has none
5164
+ * left there goes to the pin its members follow now: after a take, a `colorMode` whose modes all
5165
+ * follow Yahoo OS kept pointing at the Bridge's, its fields read from a system none of it follows.
5166
+ */
5167
+ const stands = (kind, path) => {
5168
+ const held = this.state.snapshot.group(kind, path)?.toJSON();
5169
+ if (held === void 0) return false;
5170
+ if (!isMintedRow(held)) return true;
5171
+ const pointer = held.ref;
5172
+ const through = isRef(pointer) ? sourceOf(pointer) : void 0;
5173
+ return through !== void 0 && occupied.has(`${through}\u0000${kind}\u0000${path}`);
5174
+ };
5145
5175
  for (const [slug, paths] of offeredBy) {
5146
5176
  const source = this.sourceFor(slug);
5147
5177
  if (!source) continue;
@@ -5153,7 +5183,7 @@ var Config = class Config {
5153
5183
  for (const group of source.groups(kind)) {
5154
5184
  const prefix = `${group.path}/`;
5155
5185
  if (!under.some((path) => path.startsWith(prefix))) continue;
5156
- if (this.state.snapshot.group(kind, group.path)) {
5186
+ if (stands(kind, group.path)) {
5157
5187
  standing.add(`group:${kind}:${group.path}`);
5158
5188
  continue;
5159
5189
  }
@@ -5166,6 +5196,27 @@ var Config = class Config {
5166
5196
  }
5167
5197
  }
5168
5198
  }
5199
+ const derivedFolders = new Set(folders.map(({ kind, path }) => `${kind}:${path}`));
5200
+ for (const { kind, path, slug } of taken) {
5201
+ const source = this.sourceFor(slug);
5202
+ if (!source || !this.entityOf(kind)) continue;
5203
+ for (const group of source.groups(kind)) {
5204
+ if (!path.startsWith(`${group.path}/`)) continue;
5205
+ const target = `${kind}:${group.path}`;
5206
+ if (derivedFolders.has(target) || standing.has(`group:${target}`)) continue;
5207
+ if (stands(kind, group.path)) {
5208
+ standing.add(`group:${target}`);
5209
+ continue;
5210
+ }
5211
+ derivedFolders.add(target);
5212
+ folders.push({
5213
+ kind,
5214
+ path: group.path,
5215
+ slug
5216
+ });
5217
+ claimedFrom.set(target, slug);
5218
+ }
5219
+ }
5169
5220
  for (const [target, slug] of claimedFrom) {
5170
5221
  if (folders.some(({ kind, path }) => `${kind}:${path}` === target)) continue;
5171
5222
  changes.push(derivedItemChange(kindOf(target), pathOf(target), ref(kindOf(target), pathOf(target), slug).__ref, overlayBody({
@@ -5328,7 +5379,22 @@ var Config = class Config {
5328
5379
  const source = this.sourceFor(slug);
5329
5380
  if (!source) return [];
5330
5381
  const members = /* @__PURE__ */ new Set();
5331
- const walked = /* @__PURE__ */ new Set();
5382
+ /**
5383
+ * Every entity or folder visited, and whether a linked row reached it.
5384
+ *
5385
+ * A taken row is visited twice — first as an authored row, then as one its pin adopts — and only a
5386
+ * linked visit lands the items a body points at. Remembering only THAT a target was visited let the
5387
+ * unlinked pass claim the style properties and token folders it found, so the linked pass skipped
5388
+ * them and a mode their tokens vary under never got a row. A target reached unlinked is visited
5389
+ * again when a linked row reaches it; one reached linked is done.
5390
+ */
5391
+ const reached = /* @__PURE__ */ new Map();
5392
+ const reaching = (target, linked) => {
5393
+ const prior = reached.get(target);
5394
+ if (prior === true || prior === false && !linked) return false;
5395
+ reached.set(target, linked);
5396
+ return true;
5397
+ };
5332
5398
  /**
5333
5399
  * A style bag's KEYS name style properties, and a borrowed bag's name the SOURCE's.
5334
5400
  *
@@ -5346,11 +5412,11 @@ var Config = class Config {
5346
5412
  if (!sites) return;
5347
5413
  for (const { bag } of sites(body)) for (const prop of Object.keys(bag)) {
5348
5414
  const target = `style-property:${prop}`;
5349
- if (members.has(target)) continue;
5350
5415
  const local = this.storedBody("style-property", prop);
5351
5416
  if (local && !isMintedRow(local)) continue;
5352
5417
  const declared = source.resolve("style-property", prop);
5353
5418
  if (!declared) continue;
5419
+ if (!reaching(target, linked)) continue;
5354
5420
  members.add(target);
5355
5421
  visitEntity("style-property", declared.toJSON(), true, linked);
5356
5422
  }
@@ -5381,19 +5447,19 @@ var Config = class Config {
5381
5447
  const target = `${kind}:${path}`;
5382
5448
  const item = source.resolve(kind, path);
5383
5449
  if (item) {
5384
- if (!linked || members.has(target)) continue;
5450
+ if (!linked || !reaching(target, true)) continue;
5385
5451
  members.add(target);
5386
5452
  visitEntity(kind, item.toJSON(), true, true);
5387
5453
  continue;
5388
5454
  }
5389
- if (walked.has(target)) continue;
5390
- walked.add(target);
5455
+ if (!reaching(target, linked)) continue;
5391
5456
  if (!source.groups(kind).some((node) => node.path === path)) continue;
5392
5457
  const prefix = `${path}/`;
5393
5458
  for (const member of source.list(kind)) {
5394
5459
  if (!member.path.startsWith(prefix)) continue;
5395
- members.add(`${kind}:${member.path}`);
5396
- visitEntity(kind, member.toJSON(), true, linked);
5460
+ const at = `${kind}:${member.path}`;
5461
+ members.add(at);
5462
+ if (reaching(at, linked)) visitEntity(kind, member.toJSON(), true, linked);
5397
5463
  }
5398
5464
  }
5399
5465
  };
@@ -5519,15 +5585,21 @@ var Config = class Config {
5519
5585
  * to a system linked to a foundation answers with the foundation. A row a materialized artifact
5520
5586
  * flattened to a literal has no pointer left, so the pin whose `link` declaration covers the path
5521
5587
  * stands in, provided the row still reads as the source's ({@link matchesSource}); one the
5522
- * publisher changed is its own. Two answers with one `key` are one entity reached by two routes,
5523
- * whatever versions the pins name: the key is the pin kind's `sourceOrigin`, not its identity.
5588
+ * publisher changed is its own, and so is a linked row this config lays a value of its own over: the
5589
+ * Bridge's `spectrum/blue/0` points at Yahoo OS's with the Bridge's colours on it, which is the
5590
+ * Bridge's entity, not a second route to Yahoo OS's. Two answers that are {@link sameOrigin} are one
5591
+ * entity reached by two routes, whatever versions the pins name: they compare the pin kind's
5592
+ * `sourceOrigin`, not its identity.
5524
5593
  *
5525
5594
  * `pin` is the pin entity at the hop where the answer was found, for a surface that names the
5526
- * origin the way it names any source.
5595
+ * origin the way it names any source. `key` is its origin fields joined, for a surface with no
5596
+ * name to show.
5527
5597
  */
5528
5598
  originOf({ kind, path }) {
5529
5599
  const held = this.resolve(kind, path);
5530
5600
  if (!held) return void 0;
5601
+ const stored = this.storedBody(kind, path);
5602
+ if (isExtendedRef(stored) && Object.keys(stored.extend).length > 0) return;
5531
5603
  let through = held.borrowedFrom;
5532
5604
  if (through === void 0 && held.copiedThrough !== void 0) {
5533
5605
  if (this.copyDiffers({
@@ -5543,11 +5615,12 @@ var Config = class Config {
5543
5615
  });
5544
5616
  if (upstream) return upstream;
5545
5617
  for (const sourceKind of this.sourceKinds()) {
5546
- const key = this.sourceKey(sourceKind, through, "sourceOrigin");
5618
+ const fields = this.sourceFields(sourceKind, through, "sourceOrigin");
5547
5619
  const pin = this.resolve(sourceKind, through);
5548
- if (key === void 0 || !pin) continue;
5620
+ if (fields === void 0 || !pin) continue;
5549
5621
  return {
5550
- key,
5622
+ key: fields.join("\0"),
5623
+ fields,
5551
5624
  pin
5552
5625
  };
5553
5626
  }
@@ -6325,6 +6398,21 @@ function extendBody(base, delta) {
6325
6398
  /** The paths each config is reading through a re-pointed link, for {@link Config.laidLinkedBody} to
6326
6399
  * find a loop by. Keyed on the config's state, which every read of one config shares. */
6327
6400
  const redirecting = /* @__PURE__ */ new WeakMap();
6401
+ /** Whether two {@link Config.originOf} answers are one origin, by {@link sameOriginFields}. */
6402
+ function sameOrigin(a, b) {
6403
+ return sameOriginFields(a.fields, b.fields);
6404
+ }
6405
+ /**
6406
+ * Whether two pins' `sourceOrigin` fields name one system: they agree on every field both set, and
6407
+ * share at least one. Not the whole key: a pin by path is stamped with its sibling's id when it's
6408
+ * pushed, so the Bridge's `yos` reads `configPath` and `systemId` where a pin to the same system by id
6409
+ * names only the id — and a `configPath` is relative to the config holding it, so it can't be compared
6410
+ * across two.
6411
+ */
6412
+ function sameOriginFields(theirs, mine) {
6413
+ const shared = theirs.flatMap((value, field) => value !== "" && mine[field] ? [field] : []);
6414
+ return shared.length > 0 && shared.every((field) => theirs[field] === mine[field]);
6415
+ }
6328
6416
  /**
6329
6417
  * The entity an `extend` re-points its path to, and the rest of it: a link's `override` written as a
6330
6418
  * ref lands in the stored `extend` as its `__ref`, beside whatever the `extend` verb adds.
@@ -6539,4 +6627,4 @@ function loadGroupChange(meta, kind, path, raw, phase = "author") {
6539
6627
  };
6540
6628
  }
6541
6629
  //#endregion
6542
- export { Config, SERIALIZED_CONFIG_VERSION };
6630
+ export { Config, SERIALIZED_CONFIG_VERSION, sameOrigin };
@@ -239,12 +239,17 @@ interface DanglingDirective {
239
239
  /** The accessor as written — this config may have no kind under it. */
240
240
  accessor: string;
241
241
  path: string;
242
- /** `accessor` when no kind is registered under that name, `path` when the source has no such entity. */
243
- missing: 'accessor' | 'path';
242
+ /**
243
+ * `accessor` when no kind is registered under that name, `path` when the source has no such entity,
244
+ * `row` when the source has it but the row here isn't one the pin derives.
245
+ */
246
+ missing: 'accessor' | 'path' | 'row';
247
+ /** With `row`: the pin the row there follows instead, absent when it is this config's own. */
248
+ heldBy?: string;
244
249
  }
245
250
  /**
246
- * Every directive aimed at nothing — an accessor this config has no kind for, or a path the source
247
- * does not have.
251
+ * Every directive aimed at nothing — an accessor this config has no kind for, a path the source
252
+ * does not have, or a path where the row here isn't one the pin derives.
248
253
  *
249
254
  * The one mistake the other checks structurally cannot see. Each of them answers for a path it is
250
255
  * already holding, so it asks "does the declaration mention this one" — and a name nobody has is
@@ -254,6 +259,10 @@ interface DanglingDirective {
254
259
  * Also what notices the source dropping a path. A directive written against an entity a later version
255
260
  * removed keeps parsing and keeps doing nothing, which is the same failure the declared verb exists to
256
261
  * catch one level down.
262
+ *
263
+ * And what notices a path leaving the pin. A row taken through another pin (`linked-system/link` with
264
+ * `onLinked: 'take'`), or authored here, stands where the pin's row stood, and the entry the pin
265
+ * still carries for it applies to nothing.
257
266
  */
258
267
  declare function danglingDirectives(config: Config): DanglingDirective[];
259
268
  /** A borrowed token whose condition is missing or has a different category here. */
@@ -489,8 +489,25 @@ function unadoptedDirectives(config) {
489
489
  return found;
490
490
  }
491
491
  /**
492
- * Every directive aimed at nothing — an accessor this config has no kind for, or a path the source
493
- * does not have.
492
+ * Who holds the row at a path a directive names, when it isn't the directive's own pin: the slug of
493
+ * the pin the row follows instead, `null` for a row of this config's own, `undefined` when the pin's
494
+ * row is there or nothing is.
495
+ *
496
+ * A pin's `options` lay over the rows its declaration derives, and nothing else. A row taken through
497
+ * another pin, or authored here, stands at the path in its place, so the entry applies to nothing.
498
+ */
499
+ function heldElsewhere(config, { slug, kind, path }) {
500
+ const item = config.storedBody(kind, path);
501
+ const group = item === void 0 ? config.groupMeta(kind, path) : void 0;
502
+ if (item === void 0 && group === void 0) return void 0;
503
+ const pointer = item ?? group?.ref;
504
+ const through = isRef(pointer) ? sourceOf(pointer) : void 0;
505
+ if (through === slug) return void 0;
506
+ return through ?? null;
507
+ }
508
+ /**
509
+ * Every directive aimed at nothing — an accessor this config has no kind for, a path the source
510
+ * does not have, or a path where the row here isn't one the pin derives.
494
511
  *
495
512
  * The one mistake the other checks structurally cannot see. Each of them answers for a path it is
496
513
  * already holding, so it asks "does the declaration mention this one" — and a name nobody has is
@@ -500,6 +517,10 @@ function unadoptedDirectives(config) {
500
517
  * Also what notices the source dropping a path. A directive written against an entity a later version
501
518
  * removed keeps parsing and keeps doing nothing, which is the same failure the declared verb exists to
502
519
  * catch one level down.
520
+ *
521
+ * And what notices a path leaving the pin. A row taken through another pin (`linked-system/link` with
522
+ * `onLinked: 'take'`), or authored here, stands where the pin's row stood, and the entry the pin
523
+ * still carries for it applies to nothing.
503
524
  */
504
525
  function danglingDirectives(config) {
505
526
  const found = [];
@@ -516,7 +537,23 @@ function danglingDirectives(config) {
516
537
  });
517
538
  continue;
518
539
  }
519
- if (source.resolve(kind, path) || source.members(kind, path).length > 0) continue;
540
+ if (source.resolve(kind, path) || source.members(kind, path).length > 0) {
541
+ if (config.sourceOverlay(slug, kind, path)?.hidden === true) continue;
542
+ const heldBy = heldElsewhere(config, {
543
+ slug,
544
+ kind,
545
+ path
546
+ });
547
+ if (heldBy === void 0) continue;
548
+ found.push({
549
+ slug,
550
+ accessor,
551
+ path,
552
+ missing: "row",
553
+ ...heldBy === null ? {} : { heldBy }
554
+ });
555
+ continue;
556
+ }
520
557
  found.push({
521
558
  slug,
522
559
  accessor,