@yahoo/uds-create-config 5.50.1 → 5.52.0

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.
@@ -222,6 +222,25 @@ function editionFor(prev, next) {
222
222
  */
223
223
  const derivedMembersCache = /* @__PURE__ */ new WeakMap();
224
224
  const derivedCache = /* @__PURE__ */ new WeakMap();
225
+ /** How many colliding paths a refusal names before it counts the rest. */
226
+ const COLLISIONS_NAMED = 5;
227
+ /** Refuses an edit that leaves paths two sources offer as different entities, which derive neither
228
+ * and fail the build, naming them and the two ways out. */
229
+ function rejectCollisions(collisions) {
230
+ const shown = collisions.slice(0, COLLISIONS_NAMED).map(({ kind, path, slugs, local }) => `${kind}:${path} (${[...local ? ["this system"] : [], ...slugs].join(", ")})`).join("; ");
231
+ const rest = collisions.length > COLLISIONS_NAMED ? `; and ${collisions.length - COLLISIONS_NAMED} more` : "";
232
+ const one = collisions.length === 1;
233
+ const local = collisions.some((at) => at.local);
234
+ const pins = collisions.some((at) => !at.local);
235
+ let by = "two linked systems";
236
+ if (local && pins) by = "two linked systems, or by a linked system and this one,";
237
+ else if (local) by = "a linked system and this one";
238
+ throw new ConfigRejection([{
239
+ code: "SOURCE_COLLISION",
240
+ collisions,
241
+ message: `This would leave ${collisions.length} ${one ? "path" : "paths"} offered by ${by} as different things, so neither applies: ${shown}${rest}. Take ${one ? "it" : "each"} from the system ${one ? "it" : "each"} should follow (\`linked-system/link\` with \`onLinked: 'take'\`), or make the link that only reaches ${one ? "it" : "them"} \`hybrid\`, so it uses what this system already has.`
242
+ }]);
243
+ }
225
244
  /** The snapshot a derived snapshot came from, while the parent holds a row derivation the child has
226
245
  * not yet checked against itself. See {@link Config.inheritedDerivation}.
227
246
  *
@@ -1344,7 +1363,10 @@ var Config = class Config {
1344
1363
  deferred.reached = true;
1345
1364
  return applied;
1346
1365
  }
1347
- return applied.derivedForEdit(actionOf(patch));
1366
+ return applied.derivedForEdit({
1367
+ action: actionOf(patch),
1368
+ before: this
1369
+ });
1348
1370
  }
1349
1371
  /**
1350
1372
  * `run` with each kind's invariants judged once, on the config it returns, rather than after every
@@ -1372,7 +1394,10 @@ var Config = class Config {
1372
1394
  const settled = result.derive({ deferredInvariants: void 0 });
1373
1395
  settled.assertInvariantsOf(settled.state.snapshot, deferred.kinds);
1374
1396
  if (!deferred.reached) return settled;
1375
- return settled.derivedForEdit(EDIT_ACTION);
1397
+ return settled.derivedForEdit({
1398
+ action: EDIT_ACTION,
1399
+ before: this
1400
+ });
1376
1401
  } catch {
1377
1402
  deferred.open = false;
1378
1403
  return run(this);
@@ -1403,6 +1428,7 @@ var Config = class Config {
1403
1428
  if (!onlyReversions(patch, sourceIdentity, this.entityOf(patch.kind)?.sourceOrigin)) applied.assertDerivedDropsUnreferenced();
1404
1429
  const derived = applied.withDerivedRows();
1405
1430
  derived.assertDistinctSourceSlugs();
1431
+ if (!onlyReversions(patch, sourceIdentity, this.entityOf(patch.kind)?.sourceOrigin)) this.assertNoNewCollisions(derived);
1406
1432
  return derived;
1407
1433
  }
1408
1434
  /**
@@ -1447,12 +1473,13 @@ var Config = class Config {
1447
1473
  };
1448
1474
  const landed = settled();
1449
1475
  const edits = patches.filter((patch) => !this.entityOf(patch.kind)?.sourceIdentity);
1450
- if (!(edits.length > 0 && this.reachesDerivation({
1476
+ const derived = edits.length > 0 && this.reachesDerivation({
1451
1477
  patches: edits,
1452
1478
  before: this.state.snapshot,
1453
1479
  after: landed.state.snapshot
1454
- }))) return landed;
1455
- return landed.derivedForEdit(actionOf(edits));
1480
+ }) ? landed.derivedForEdit({ action: actionOf(edits) }) : landed;
1481
+ this.assertNoNewCollisions(derived);
1482
+ return derived;
1456
1483
  }
1457
1484
  const land = (checked) => {
1458
1485
  let config = checked ? this : this.derive({ snapshot: this.state.snapshot.drafting() });
@@ -1474,7 +1501,10 @@ var Config = class Config {
1474
1501
  before: this.state.snapshot,
1475
1502
  after: config.state.snapshot
1476
1503
  })) return config;
1477
- return config.derivedForEdit(actionOf(patches));
1504
+ return config.derivedForEdit({
1505
+ action: actionOf(patches),
1506
+ before: this
1507
+ });
1478
1508
  }
1479
1509
  /**
1480
1510
  * `patches` with each delete preceded by the deletes that go with it, each judged against the state
@@ -1669,8 +1699,8 @@ var Config = class Config {
1669
1699
  /**
1670
1700
  * Whether patches on kinds that name no source can change the rows a derive mints between `before`
1671
1701
  * and `after`: they wrote a pin (an edit to a minted row lands in its pin's `options`), or a row
1672
- * they touched points at a source, changes which source-qualified refs it holds, or appears or
1673
- * vanishes at a path a source offers. An authored style bag's keys are this config's own
1702
+ * they touched points at a source, changes which source-qualified refs it holds, appears or vanishes
1703
+ * at a path a source offers, or is a row of this config's own that a linked pin reaches. An authored style bag's keys are this config's own
1674
1704
  * properties, so they don't count.
1675
1705
  *
1676
1706
  * Asked of the patches' own paths rather than of every row, so a batch pays per patch.
@@ -1680,6 +1710,7 @@ var Config = class Config {
1680
1710
  const slugs = this.sourceSlugs();
1681
1711
  if (slugs.length === 0) return false;
1682
1712
  const offered = (kind, path) => slugs.some((slug) => this.sourceFor(slug)?.resolve(kind, path));
1713
+ const shadowed = (kind, path) => this.caches.derivedRows?.shadowed.has(`${kind}:${path}`) ?? false;
1683
1714
  const reaches = (kind, path) => {
1684
1715
  return [[before.get(kind, path), after.get(kind, path)], [before.group(kind, path), after.group(kind, path)]].some(([prior, next]) => {
1685
1716
  if (prior === next) return false;
@@ -1687,7 +1718,8 @@ var Config = class Config {
1687
1718
  const priorBody = prior?.toJSON() ?? {};
1688
1719
  const nextBody = next?.toJSON() ?? {};
1689
1720
  if (pointsAtSource(priorBody) || pointsAtSource(nextBody)) return true;
1690
- return sourcedRefs(priorBody) !== sourcedRefs(nextBody);
1721
+ if (sourcedRefs(priorBody) !== sourcedRefs(nextBody)) return true;
1722
+ return prior !== void 0 && next !== void 0 && shadowed(kind, path);
1691
1723
  });
1692
1724
  };
1693
1725
  return patches.some((patch) => {
@@ -4510,21 +4542,42 @@ var Config = class Config {
4510
4542
  return [...reached].sort();
4511
4543
  }
4512
4544
  /**
4545
+ * A refusal for a pin patch that would leave paths two sources offer as different entities, which
4546
+ * derive neither and fail the build. Named here, when the patch is made, with the two ways out.
4547
+ */
4548
+ assertNoNewCollisions(after) {
4549
+ this.assertNoNewCollisionsIn(after.collidingSources());
4550
+ }
4551
+ /** {@link assertNoNewCollisions} against the collisions a derivation already found. */
4552
+ assertNoNewCollisionsIn(collisions) {
4553
+ const before = new Set((this.caches.derivedRows ?? this.rowDerivation()).collisions.map(({ kind, path }) => `${kind}:${path}`));
4554
+ const added = collisions.filter(({ kind, path }) => !before.has(`${kind}:${path}`));
4555
+ if (added.length > 0) rejectCollisions(added);
4556
+ }
4557
+ /**
4513
4558
  * A refusal for a patch whose derive pass would retire a minted row that something authored here
4514
4559
  * still points at. A minted row has no body of its own, so once it goes the referrer dangles;
4515
4560
  * copying what is used, or breaking the link with `keep`, is how the value stays.
4516
4561
  *
4562
+ * A row retired because two sources now offer its path as different entities is refused as that
4563
+ * collision instead, since resolving the collision is what keeps the row.
4564
+ *
4517
4565
  * `action` names what is refused, after "Cannot": a pin patch removes the link, and any other patch
4518
4566
  * that reaches derivation — a deleted extension whose source reached the row — says what it did.
4519
4567
  */
4520
4568
  assertDerivedDropsUnreferenced({ action = "remove the link", derivation = this.rowDerivation() } = {}) {
4521
4569
  const { snapshot } = this.state;
4570
+ const contested = new Set(derivation.collisions.map(({ kind, path }) => `${kind}:${path}`));
4522
4571
  const stranded = [];
4572
+ let collides = false;
4523
4573
  for (const change of derivation.changes) {
4524
4574
  if (change.value !== void 0) continue;
4525
4575
  const referrers = this.referrersOf(snapshot, change.kind, change.path).filter((referrer) => !isMintedAt(snapshot, referrer));
4526
- if (referrers.length > 0) stranded.push(`${change.kind}:${change.path} (used by ${referrers.join(", ")})`);
4576
+ if (referrers.length === 0) continue;
4577
+ if (contested.has(`${change.kind}:${change.path}`)) collides = true;
4578
+ stranded.push(`${change.kind}:${change.path} (used by ${referrers.join(", ")})`);
4527
4579
  }
4580
+ if (collides) rejectCollisions(derivation.collisions);
4528
4581
  if (stranded.length === 0) return;
4529
4582
  reject("REFERRERS_BLOCK", `Cannot ${action} — it would take away ${stranded.join("; ")}. Copy what is used first (\`linked-system/copy\`), or break the link keeping its values (\`linked-system/break\` with \`keep: true\`).`);
4530
4583
  }
@@ -4608,10 +4661,13 @@ var Config = class Config {
4608
4661
  * A session derives every value it advances to, so without the mark the same edit would pay for two
4609
4662
  * derive passes: this one and the session's. The mark covers this instance only, and any later
4610
4663
  * write is a new config, so each edit still gets exactly one pass, as it did from the session alone.
4664
+ *
4665
+ * `before` is the config the edit was made on, when the edit is refused for a collision it adds.
4611
4666
  */
4612
- derivedForEdit(action) {
4667
+ derivedForEdit({ action, before }) {
4613
4668
  if (derivedInEdit.has(this)) return this;
4614
4669
  const derivation = this.rowDerivation();
4670
+ if (derivation.collisions.length > 0) before?.assertNoNewCollisionsIn(derivation.collisions);
4615
4671
  this.assertDerivedDropsUnreferenced({
4616
4672
  action,
4617
4673
  derivation
@@ -4652,6 +4708,14 @@ var Config = class Config {
4652
4708
  return this.rowDerivation().collisions;
4653
4709
  }
4654
4710
  /**
4711
+ * The paths a `hybrid` pin reaches as a different entity from the one another pin offers there.
4712
+ * The other pin's row stands at the path, and what the hybrid pin brings is drawn with it
4713
+ * ({@link drawsWithLocalRow}). Nothing here is refused, so this is what says the two differ.
4714
+ */
4715
+ settledSources() {
4716
+ return this.rowDerivation().settled;
4717
+ }
4718
+ /**
4655
4719
  * The derivation made for the snapshot this one came from, when this edit could not have changed
4656
4720
  * it: the same sources, no change to a pin or to which paths exist, and every row that did change
4657
4721
  * is authored on both sides with the same source-qualified refs and the same style-bag keys —
@@ -4678,18 +4742,29 @@ var Config = class Config {
4678
4742
  const after = snapshot.slice(kind);
4679
4743
  if (before === after) continue;
4680
4744
  if (sourceKinds.has(kind)) return void 0;
4681
- if (!this.sameRowsForDerivation(kind, before.items, after.items)) return;
4682
- if (!this.sameRowsForDerivation(kind, before.groups, after.groups)) return;
4745
+ const { shadowed } = prior;
4746
+ if (!this.sameRowsForDerivation({
4747
+ kind,
4748
+ before: before.items,
4749
+ after: after.items,
4750
+ shadowed
4751
+ }) || !this.sameRowsForDerivation({
4752
+ kind,
4753
+ before: before.groups,
4754
+ after: after.groups,
4755
+ shadowed
4756
+ })) return;
4683
4757
  }
4684
4758
  return prior;
4685
4759
  }
4686
4760
  /**
4687
- * Same paths in the same order, and every row that changed is authored on both sides and reaches
4688
- * the same sources the same way — see {@link inheritedDerivation}. Order counts because the walk
4761
+ * Same paths in the same order, and every row that changed is authored on both sides, isn't one a
4762
+ * linked pin reaches (`shadowed`), and reaches the same sources the same way — see
4763
+ * {@link inheritedDerivation}. Order counts because the walk
4689
4764
  * visits rows in stored order and mints in the order it reaches them, and key order is meaning
4690
4765
  * downstream.
4691
4766
  */
4692
- sameRowsForDerivation(kind, before, after) {
4767
+ sameRowsForDerivation({ kind, before, after, shadowed }) {
4693
4768
  if (before.size !== after.size) return false;
4694
4769
  const afterPaths = after.keys();
4695
4770
  for (const [path, prior] of before) {
@@ -4697,6 +4772,7 @@ var Config = class Config {
4697
4772
  const next = after.get(path);
4698
4773
  if (!next) return false;
4699
4774
  if (prior === next) continue;
4775
+ if (shadowed.has(`${kind}:${path}`)) return false;
4700
4776
  const priorBody = prior.toJSON();
4701
4777
  const nextBody = next.toJSON();
4702
4778
  if (pointsAtSource(priorBody) || pointsAtSource(nextBody)) return false;
@@ -4716,26 +4792,34 @@ var Config = class Config {
4716
4792
  return JSON.stringify([[...sourced].sort(), [...bagKeys].sort()]);
4717
4793
  }
4718
4794
  /**
4719
- * Which of several sources offering `target` takes it, when exactly one pin's `link` declares it and
4720
- * every other source only reaches it through a borrowed style bag. `undefined` leaves it a collision.
4795
+ * Which of several sources offering `target` takes it. `undefined` leaves it a collision.
4721
4796
  *
4722
- * A reaching source counts only while its copy reads the same as the declarer's: one system built on
4723
- * another, pinned beside that other, reaches the same entity twice. Identity can't be traced instead,
4724
- * because an attached sibling arrives with what it borrowed inlined to literals. A copy that reads
4725
- * differently is its own entity, and picking either would hide the other.
4797
+ * A path one pin's `link` declares goes to that pin when every other source only reaches it and
4798
+ * either holds the same entity there or is `hybrid`, which brings in only what no other pin offers.
4799
+ * A `linked` source reaching a different entity at a declared path is a collision.
4800
+ *
4801
+ * A path no pin declares goes to the first `linked` source when every `linked` source holding it
4802
+ * reads the same entity, the `hybrid` ones yielding to them. With only `hybrid` sources it goes to
4803
+ * the first on the same condition. Two pins declaring one path is a collision either way.
4726
4804
  */
4727
4805
  declaringClaimant({ target, slugs, declaredBy }) {
4728
4806
  const declarers = slugs.filter((slug) => declaredBy.get(slug)?.has(target));
4729
- const [declarer] = declarers;
4730
- if (declarer === void 0 || declarers.length > 1) return void 0;
4807
+ if (declarers.length > 1) return void 0;
4731
4808
  const kind = kindOf(target);
4732
4809
  const path = pathOf(target);
4733
- return slugs.every((slug) => slug === declarer || this.sameEntityThrough({
4734
- held: declarer,
4810
+ const agree = (held, others) => others.every((slug) => slug === held || this.sameEntityThrough({
4811
+ held,
4735
4812
  other: slug,
4736
4813
  kind,
4737
4814
  path
4738
- })) ? declarer : void 0;
4815
+ }));
4816
+ const linked = slugs.filter((slug) => this.sourceTypeOf(slug) === "linked");
4817
+ const [declarer] = declarers;
4818
+ if (declarer !== void 0) return agree(declarer, linked) ? declarer : void 0;
4819
+ const deciding = linked.length > 0 ? linked : slugs;
4820
+ const [first] = deciding;
4821
+ if (first === void 0) return void 0;
4822
+ return agree(first, deciding) ? first : void 0;
4739
4823
  }
4740
4824
  /**
4741
4825
  * Whether `other`'s entity at `kind:path` is the one `held` offers there: one entity reached through
@@ -4744,14 +4828,60 @@ var Config = class Config {
4744
4828
  */
4745
4829
  sameEntityThrough({ held, other, kind, path }) {
4746
4830
  const theirs = this.sourceFor(held)?.resolve(kind, path);
4831
+ if (!theirs) return false;
4832
+ return this.sameEntityAs({
4833
+ body: theirs.body,
4834
+ other,
4835
+ kind,
4836
+ path
4837
+ });
4838
+ }
4839
+ /** Whether `other`'s entity at `kind:path` reads as `body`, spelled the way this config spells it. */
4840
+ sameEntityAs({ body, other, kind, path }) {
4747
4841
  const reaching = this.sourceFor(other);
4748
4842
  const copy = reaching?.resolve(kind, path);
4749
- if (!theirs || !reaching || !copy) return false;
4843
+ if (!reaching || !copy) return false;
4844
+ const provenance = Object.fromEntries([...this.entityOf(kind)?.computedKeys ?? [], "copiedFrom"].map((key) => [key, void 0]));
4750
4845
  const entityOf = (body) => JSON.stringify({
4751
4846
  ...body,
4752
- borrowedFrom: void 0
4847
+ ...provenance
4848
+ });
4849
+ const bare = [...new Set([...reaching.sourceSlugs(), ...collectRefs(copy.body).flatMap((marker) => sourceOf(marker) ?? [])])].reduce((body, slug) => rewriteRefSource(body, slug, void 0), copy.body);
4850
+ const ours = rewriteRefSource(body, other, void 0);
4851
+ return entityOf(bare) === entityOf(ours);
4852
+ }
4853
+ /** How `slug`'s source joins this config ({@link EntityKind.sourceType}): `linked` unless its pin
4854
+ * says otherwise. */
4855
+ sourceTypeOf(slug) {
4856
+ for (const sourceKind of this.sourceKinds()) {
4857
+ const asks = this.entityOf(sourceKind)?.sourceType;
4858
+ const pin = this.storedBody(sourceKind, slug);
4859
+ if (asks && pin) return asks(pin);
4860
+ }
4861
+ return "linked";
4862
+ }
4863
+ /**
4864
+ * Whether a value `slug` brings is drawn with this config's row at `kind:path` rather than declared
4865
+ * under the pin's own name: a token that something taken through `slug` names.
4866
+ *
4867
+ * A `hybrid` pin binds to any row here that isn't its own, authored or held through another pin, so
4868
+ * what it brings uses this config's values. A `linked` pin binds only to a row held through another
4869
+ * pin that is the same entity, the way {@link heldThroughAnotherPin} reads it; a different one there
4870
+ * is a collision, and a row this config authored is its own entity under the same name.
4871
+ */
4872
+ drawsWithLocalRow({ slug, kind, path }) {
4873
+ const stored = this.storedBody(kind, path);
4874
+ if (stored === void 0) return false;
4875
+ const held = isRef(stored) ? sourceOf(stored.__ref) : void 0;
4876
+ if (held === slug) return false;
4877
+ if (this.sourceTypeOf(slug) === "hybrid") return true;
4878
+ if (!isMintedRow(stored) || held === void 0) return false;
4879
+ return this.sameEntityThrough({
4880
+ held,
4881
+ other: slug,
4882
+ kind,
4883
+ path
4753
4884
  });
4754
- return entityOf([...new Set([...reaching.sourceSlugs(), ...collectRefs(copy.body).flatMap((marker) => sourceOf(marker) ?? [])])].reduce((body, slug) => rewriteRefSource(body, slug, void 0), copy.body)) === entityOf(theirs.body);
4755
4885
  }
4756
4886
  /**
4757
4887
  * Whether `slug`'s entity at `kind:path` is one this config already holds through another pin, so a
@@ -4813,6 +4943,10 @@ var Config = class Config {
4813
4943
  const declaredBy = /* @__PURE__ */ new Map();
4814
4944
  /** The slugs whose source answered this pass, so a row of theirs that went unproduced is gone. */
4815
4945
  const consulted = /* @__PURE__ */ new Set();
4946
+ /** Per target, the linked slugs that reach a different entity than this config's own row there. */
4947
+ const ownedAgainst = /* @__PURE__ */ new Map();
4948
+ /** Every row of this config's own a linked pin reaches without declaring it. */
4949
+ const shadowed = /* @__PURE__ */ new Set();
4816
4950
  for (const slug of this.sourceSlugs()) {
4817
4951
  const source = this.sourceFor(slug);
4818
4952
  if (!source) continue;
@@ -4821,17 +4955,29 @@ var Config = class Config {
4821
4955
  const mine = [];
4822
4956
  const declared = new Set(this.adoptedFrom(slug));
4823
4957
  declaredBy.set(slug, declared);
4958
+ const strict = this.sourceTypeOf(slug) === "linked";
4824
4959
  const wanted = new Set([...declared, ...this.groupRefMembers(slug)]);
4825
4960
  for (const target of wanted) {
4826
4961
  const kind = kindOf(target);
4827
4962
  const path = pathOf(target);
4828
- if (!kindBorrowable(this.entityOf(kind), {
4963
+ const entry = this.entityOf(kind);
4964
+ if (!kindBorrowable(entry, {
4829
4965
  config: this,
4830
4966
  source
4831
4967
  })) continue;
4832
4968
  if (this.sourceExcludes(slug, kind, path)) continue;
4833
4969
  const current = this.state.snapshot.get(kind, path);
4834
- if (current && !isMintedRow(current.toJSON())) continue;
4970
+ if (current && !isMintedRow(current.toJSON())) {
4971
+ const judged = strict && !entry?.answeredHere && !declared.has(target) && !isRef(current.toJSON()) && !placed.has(target);
4972
+ if (judged) shadowed.add(target);
4973
+ if (judged && source.resolve(kind, path) && !this.sameEntityAs({
4974
+ body: current.body,
4975
+ other: slug,
4976
+ kind,
4977
+ path
4978
+ })) ownedAgainst.set(target, [...ownedAgainst.get(target) ?? [], slug]);
4979
+ continue;
4980
+ }
4835
4981
  if (placed.has(target)) continue;
4836
4982
  if (!source.resolve(kind, path)) continue;
4837
4983
  if (!this.canBorrow({
@@ -4844,7 +4990,13 @@ var Config = class Config {
4844
4990
  }
4845
4991
  offeredBy.set(slug, mine);
4846
4992
  }
4847
- const collisions = [];
4993
+ const collisions = [...ownedAgainst].map(([target, slugs]) => ({
4994
+ kind: kindOf(target),
4995
+ path: pathOf(target),
4996
+ slugs,
4997
+ local: true
4998
+ }));
4999
+ const settled = [];
4848
5000
  /** Every target this pass will derive a row for, and the slug it comes from. */
4849
5001
  const claimedFrom = /* @__PURE__ */ new Map();
4850
5002
  for (const [target, slugs] of wantedBy) {
@@ -4862,6 +5014,22 @@ var Config = class Config {
4862
5014
  continue;
4863
5015
  }
4864
5016
  claimedFrom.set(target, claimant);
5017
+ if (slugs.length > 1) {
5018
+ const kind = kindOf(target);
5019
+ const path = pathOf(target);
5020
+ const over = slugs.filter((slug) => slug !== claimant && !this.sameEntityThrough({
5021
+ held: claimant,
5022
+ other: slug,
5023
+ kind,
5024
+ path
5025
+ }));
5026
+ if (over.length > 0) settled.push({
5027
+ kind,
5028
+ path,
5029
+ kept: claimant,
5030
+ over
5031
+ });
5032
+ }
4865
5033
  }
4866
5034
  const folders = [];
4867
5035
  /** Folders this pass leaves as they stand — already held, over rows it still claims. */
@@ -4964,7 +5132,9 @@ var Config = class Config {
4964
5132
  }
4965
5133
  const planned = {
4966
5134
  changes,
4967
- collisions
5135
+ collisions,
5136
+ settled,
5137
+ shadowed
4968
5138
  };
4969
5139
  this.caches.derivedRows = planned;
4970
5140
  return planned;
@@ -5171,6 +5341,7 @@ var Config = class Config {
5171
5341
  });
5172
5342
  continue;
5173
5343
  }
5344
+ if (local !== void 0 && !isMintedRow(local) && !(isRef(local) && sourceOf(local.__ref) === slug)) continue;
5174
5345
  const body = source.resolve(kindOf(target), pathOf(target))?.toJSON();
5175
5346
  if (body) visitEntity(kindOf(target), body, true, true);
5176
5347
  }
@@ -5873,7 +6044,7 @@ var Config = class Config {
5873
6044
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5874
6045
  hydrateFrom(stored, options) {
5875
6046
  const claimed = schemaVersionOf(stored);
5876
- if (claimed > 20261022152120) throw new SchemaVersionTooNew(claimed);
6047
+ if (claimed > 20261022184639) throw new SchemaVersionTooNew(claimed);
5877
6048
  const wire = upgradeSerializedConfig(stored);
5878
6049
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating.`);
5879
6050
  const leftover = wire.options;
@@ -5,7 +5,7 @@ import { CreateInput, FieldsSchema } from "./utils/field-schema.js";
5
5
  import { FieldsCtx, UserSchemas } from "./schemas.js";
6
6
  import { SubEntityClass } from "./defineSubEntity.js";
7
7
  import { BulkOverlayWrite, BulkQuery, SourceOverlay, SourceResolutionSchemas } from "./sources.js";
8
- import { AuthoredValueForm, ComputedFields, EntityKind, Linkable } from "./registry.js";
8
+ import { AuthoredValueForm, ComputedFields, EntityKind, Linkable, SourceType } from "./registry.js";
9
9
  import { PathOf } from "./registered.js";
10
10
  import { GroupBody, GroupInputValues, GroupInputs } from "./utils/group.js";
11
11
  import { Config, ListOptions, TreeNode } from "./Config.js";
@@ -310,7 +310,9 @@ declare function defineEntity<F extends FieldsSchema, G extends FieldsSchema = z
310
310
  * mean the ref has to say which.
311
311
  */
312
312
  bareMember?: string; /** Whether a leaf names an item on its own — see {@link EntityKind.uniqueLeaves}. */
313
- uniqueLeaves?: boolean;
313
+ uniqueLeaves?: boolean; /** For a source kind: how a pin's source joins this config. See {@link EntityKind.sourceType}. */
314
+ sourceType?: (body: Record<string, unknown>) => SourceType; /** This config answers for every item of the kind. See {@link EntityKind.answeredHere}. */
315
+ answeredHere?: boolean;
314
316
  /**
315
317
  * Registration-time inputs `group()` takes beside the group's fields, keyed by body key. Each is
316
318
  * handed to its handler with the rest of the body and stripped before the body is branded, so the
@@ -135,7 +135,9 @@ function defineEntity(spec) {
135
135
  mentionFields: spec.mentionFields,
136
136
  invariants: spec.invariants,
137
137
  structuralBody: spec.structuralBody,
138
- authoredBody: spec.authoredBody
138
+ authoredBody: spec.authoredBody,
139
+ sourceType: spec.sourceType,
140
+ answeredHere: spec.answeredHere
139
141
  });
140
142
  const schema = refTo(spec.kind);
141
143
  /**
@@ -83,7 +83,7 @@ function linkedVarNaming(config, slug) {
83
83
  return (tokenPath) => {
84
84
  const local = held.get(tokenPath);
85
85
  if (local !== void 0) return cssVar(config, "token", local);
86
- if (config.heldThroughAnotherPin({
86
+ if (config.drawsWithLocalRow({
87
87
  slug,
88
88
  kind: "token",
89
89
  path: tokenPath
@@ -297,6 +297,17 @@ interface EntityKind {
297
297
  * can be read but never edited through a minted row.
298
298
  */
299
299
  readonly bulkOverlayWrite?: (args: BulkOverlayWrite) => Record<string, unknown>;
300
+ /**
301
+ * Declared by a source kind: how a pin's source joins this config, read off the pin's body.
302
+ * {@link Config.sourceTypeOf} asks it.
303
+ */
304
+ readonly sourceType?: (body: Record<string, unknown>) => SourceType;
305
+ /**
306
+ * This config answers for every item of the kind, whatever a linked pin reaches: a borrowed value
307
+ * conditioned on one names it by path, and this config's item of that path is the one that holds.
308
+ * A source's different item at a path this config holds is never a collision.
309
+ */
310
+ readonly answeredHere?: true;
300
311
  /**
301
312
  * Edges this kind's data implies by name rather than by ref — `nameEdges(body)` for one item.
302
313
  *
@@ -389,9 +400,20 @@ interface EntityKind {
389
400
  element?: unknown;
390
401
  }[];
391
402
  }
403
+ /** Derive a kind's immutable metadata from its field schemas — the value a `defineEntity`/
404
+ * `defineDerivedEntity` attaches to its class and a `Config.register` stores in `#entities`. */
405
+ /**
406
+ * How a pin's source joins a config.
407
+ *
408
+ * - `linked`: everything a linked entity references lands as a row from this pin, and a path another
409
+ * pin offers as a different entity is a collision.
410
+ * - `hybrid`: it brings in only what no other pin offers. Where another pin offers a path, that pin's
411
+ * row stands, and what this pin brings uses it; a component it renders stays its own source's.
412
+ */
413
+ type SourceType = 'linked' | 'hybrid';
392
414
  /** Every operation a kind exposes — its merged `schemas` map, flattened to `{ kind, verb, ... }`.
393
415
  * The single derivation the Operation entity maps over; nothing hardcodes a CRUD list. Reads the
394
416
  * kind def a `Config` holds (`config.entityOf(kind)`); `undefined` → no ops. */
395
417
  declare function opsForKind(entry: EntityKind | undefined, config?: Config): DerivedOp[];
396
418
  //#endregion
397
- export { AuthoredValueForm, ComputedFields, ComputedInput, EntityKind, LayerKey, Linkable, opsForKind };
419
+ export { AuthoredValueForm, ComputedFields, ComputedInput, EntityKind, LayerKey, Linkable, SourceType, opsForKind };
@@ -29,10 +29,8 @@ function entityClassName(kind) {
29
29
  function titleCase(kind) {
30
30
  return kind.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
31
31
  }
32
- /** Derive a kind's immutable metadata from its field schemas — the value a `defineEntity`/
33
- * `defineDerivedEntity` attaches to its class and a `Config.register` stores in `#entities`. */
34
32
  function buildKind(spec) {
35
- const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkExcludes, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants, structuralBody, authoredBody } = spec;
33
+ const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkExcludes, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants, structuralBody, authoredBody, sourceType, answeredHere } = spec;
36
34
  const allComputed = {
37
35
  sources: ({ member, config }) => sourceSlugs(member, config),
38
36
  borrowedFrom: ({ config, path }) => config.borrowedFrom(name, path),
@@ -85,7 +83,9 @@ function buildKind(spec) {
85
83
  ...mentionFields?.length ? { mentionFields } : {},
86
84
  ...invariants ? { invariants } : {},
87
85
  ...structuralBody ? { structuralBody } : {},
88
- ...authoredBody ? { authoredBody } : {}
86
+ ...authoredBody ? { authoredBody } : {},
87
+ ...sourceType ? { sourceType } : {},
88
+ ...answeredHere ? { answeredHere: true } : {}
89
89
  };
90
90
  }
91
91
  /** Every operation a kind exposes — its merged `schemas` map, flattened to `{ kind, verb, ... }`.
@@ -1,3 +1,4 @@
1
+ import { SourceCollision } from "./Config.js";
1
2
  import { z } from "zod";
2
3
 
3
4
  //#region src/framework/rejection.d.ts
@@ -22,7 +23,10 @@ type ConfigIssueCode = /** Nothing at that path / no such member — an update,
22
23
  | 'UNSUPPORTED'
23
24
  /** A delete that something still points at. Carries the referrers, because "42 things break" is
24
25
  * only actionable as a list of which ones. */
25
- | 'REFERRERS_BLOCK';
26
+ | 'REFERRERS_BLOCK'
27
+ /** A pin patch that would leave a path two linked systems offer as different entities, which
28
+ * derives neither. Carries the collisions. */
29
+ | 'SOURCE_COLLISION';
26
30
  interface ConfigIssue {
27
31
  readonly code: ConfigIssueCode;
28
32
  readonly message: string;
@@ -33,6 +37,9 @@ interface ConfigIssue {
33
37
  /** What still points at the target of a refused delete, as `kind:path` refs — the
34
38
  * `REFERRERS_BLOCK` list, so a surface can name each one in its own words. */
35
39
  readonly referrers?: readonly string[];
40
+ /** The paths a refused edit would leave two linked systems offering as different entities — the
41
+ * `SOURCE_COLLISION` list, so a surface can say what differs in its own words. */
42
+ readonly collisions?: readonly SourceCollision[];
36
43
  }
37
44
  /**
38
45
  * A write this config refused. `issues` is never empty; `message` joins them so an uncaught one still
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
15
15
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
16
16
  * newer than the constant, and the assertion fails until the author re-mints.
17
17
  */
18
- declare const CURRENT_SCHEMA_VERSION = 20261022152120;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261022184639;
19
19
  /** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
20
20
  * and a raw old-shape record both flow in without casts. */
21
21
  interface VersionStamped {
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
13
13
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
14
14
  * newer than the constant, and the assertion fails until the author re-mints.
15
15
  */
16
- const CURRENT_SCHEMA_VERSION = 20261022152120;
16
+ const CURRENT_SCHEMA_VERSION = 20261022184639;
17
17
  /** The mint that moved `options` into the `settings` kind — restated here, rather than imported
18
18
  * from the migration that owns it, because that migration imports this module. */
19
19
  const SETTINGS_AS_KIND_VERSION = 20260908171448;
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
45
45
  for (const migration of migrations) {
46
46
  if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
47
47
  if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
48
- if (migration.version > 20261022152120) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
48
+ if (migration.version > 20261022184639) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
49
49
  REGISTERED.set(migration.version, migration);
50
50
  }
51
51
  }
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
102
102
  * Already-current input passes through untouched — same reference, so hot paths pay nothing.
103
103
  */
104
104
  function upgradeSerializedConfig(json, options) {
105
- const target = options?.target ?? 20261022152120;
105
+ const target = options?.target ?? 20261022184639;
106
106
  const version = detectedWireVersion(json);
107
107
  if (version > target) throw new SchemaVersionTooNew(version, target);
108
108
  const raw = json;
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
160
160
  * meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
161
161
  */
162
162
  function upgradePatch(patch, writtenAt, options) {
163
- const target = options?.target ?? 20261022152120;
163
+ const target = options?.target ?? 20261022184639;
164
164
  if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
165
165
  let current = patch;
166
166
  for (const migration of pendingFor(writtenAt, target, options)) {
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
179
179
  * stamp would cause.
180
180
  */
181
181
  function upgradeDraftEntries(draft, options) {
182
- const target = options?.target ?? 20261022152120;
182
+ const target = options?.target ?? 20261022184639;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];