@yahoo/uds-create-config 5.51.0 → 5.52.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.
- package/dist/configs/system.js +2 -0
- package/dist/entities/native/NativeModifier.js +1 -0
- package/dist/entities/system/LinkedSystem.d.ts +4 -0
- package/dist/entities/system/LinkedSystem.js +19 -2
- package/dist/entities/system/Modifier.js +1 -0
- package/dist/entities/system/link-plan.d.ts +20 -8
- package/dist/entities/system/link-plan.js +60 -21
- package/dist/framework/Config.d.ts +78 -18
- package/dist/framework/Config.js +206 -35
- package/dist/framework/defineEntity.d.ts +4 -2
- package/dist/framework/defineEntity.js +3 -1
- package/dist/framework/projections.js +1 -1
- package/dist/framework/registry.d.ts +23 -1
- package/dist/framework/registry.js +4 -4
- package/dist/framework/rejection.d.ts +8 -1
- package/dist/framework/render-spec.d.ts +4 -1
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/migrations/20261022184639_link_type.d.ts +8 -0
- package/dist/migrations/20261022184639_link_type.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/framework/Config.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
})))
|
|
1455
|
-
|
|
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(
|
|
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,
|
|
1673
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4682
|
-
if (!this.sameRowsForDerivation(
|
|
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
|
|
4688
|
-
* the same sources the same way — see
|
|
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
|
|
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
|
|
4723
|
-
*
|
|
4724
|
-
*
|
|
4725
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
4734
|
-
held
|
|
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
|
-
}))
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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()))
|
|
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 >
|
|
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.
|
|
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
|
|
@@ -218,8 +218,11 @@ interface ExpandOptions {
|
|
|
218
218
|
*
|
|
219
219
|
* Omitted, everything the config describes expands, which is what a test wants and what a caller
|
|
220
220
|
* with no registry to consult gets.
|
|
221
|
+
*
|
|
222
|
+
* Only `has` is asked, so a caller can answer from its registry directly instead of copying every
|
|
223
|
+
* key into a `Set` first.
|
|
221
224
|
*/
|
|
222
|
-
readonly rendered?: ReadonlySet<string>;
|
|
225
|
+
readonly rendered?: Pick<ReadonlySet<string>, 'has'>;
|
|
223
226
|
/** The functions `$computed` values call. Absent, a computed value stays on the element for the
|
|
224
227
|
* renderer, and a node repeated over a computed list emits once. */
|
|
225
228
|
readonly functions?: ComputedFunctions;
|
|
@@ -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 =
|
|
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 =
|
|
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 >
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
182
|
+
const target = options?.target ?? 20261022184639;
|
|
183
183
|
const entries = [];
|
|
184
184
|
const held = [];
|
|
185
185
|
const retired = [];
|