@yahoo/uds-create-config 5.20.0 → 5.21.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.
@@ -64,6 +64,7 @@ import { buildReactMigration } from "../migrations/20261008181849_build_react.js
64
64
  import { cssMotionStatesMigration } from "../migrations/20261009000514_css_motion_states.js";
65
65
  import { startingStyleModifierMigration } from "../migrations/20261009022722_starting_style_modifier.js";
66
66
  import { cssMotionStrictMigration } from "../migrations/20261009181635_css_motion_strict.js";
67
+ import { linkOverrideRefMigration } from "../migrations/20261009193004_link_override_ref.js";
67
68
  const System = defineConfig({
68
69
  name: WEB_CONFIG_NAME,
69
70
  description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
@@ -139,5 +140,6 @@ if (!registeredSchemaMigrations().has(20261008181849)) registerSchemaMigrations(
139
140
  if (!registeredSchemaMigrations().has(20261009000514)) registerSchemaMigrations(cssMotionStatesMigration);
140
141
  if (!registeredSchemaMigrations().has(20261009022722)) registerSchemaMigrations(startingStyleModifierMigration);
141
142
  if (!registeredSchemaMigrations().has(20261009181635)) registerSchemaMigrations(cssMotionStrictMigration);
143
+ if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(linkOverrideRefMigration);
142
144
  //#endregion
143
145
  export { System };
@@ -249,7 +249,7 @@ type PinOptionsInput<K extends string = string> = { [A in keyof RegisteredEntiti
249
249
  /** A motion entry: an adjustment whose `runtime` is one the linked preset can be read as
250
250
  * ({@link RegisteredMotionRuntimes}), so a swap the conversion would refuse doesn't type. */
251
251
  type PinMotionAdjustment<E, P extends string> = {
252
- override?: PinMotionBody<E, P>;
252
+ override?: PinMotionBody<E, P> | KindRefOf<E>;
253
253
  extend?: PinMotionBody<E, P>;
254
254
  hidden?: boolean;
255
255
  };
@@ -259,8 +259,14 @@ type PinMotionBody<E, P extends string> = Extract<StoredBodyOf<PartialBodyOf<E>>
259
259
  /** The linked presets registered at `path`, from whichever source. */
260
260
  type LinkedMotionKey<P extends string> = Extract<keyof RegisteredMotionRuntimes, `${P}@${string}`>;
261
261
  type MotionRuntimeOf<P extends string> = [LinkedMotionKey<P>] extends [never] ? 'css' | 'js' : RegisteredMotionRuntimes[LinkedMotionKey<P>];
262
+ /** A ref to another entity of `E`'s kind, which re-points the path to it. */
263
+ type KindRefOf<E> = E extends {
264
+ readonly kind: infer K extends string;
265
+ } ? {
266
+ readonly __ref: `${K}:${string}`;
267
+ } : never;
262
268
  type PinComponentAdjustment<E, P extends string> = {
263
- override?: PinComponentBody<E, P>;
269
+ override?: PinComponentBody<E, P> | KindRefOf<E>;
264
270
  extend?: PinComponentBody<E, P>;
265
271
  hidden?: boolean;
266
272
  };
@@ -280,7 +286,7 @@ type LinkDeclaration = '*' | Record<string, '*' | readonly string[]>;
280
286
  /** One `options` entry, checked against the kind its accessor addresses. The runtime floor — that an
281
287
  * entry names at least one of the three — is the schema's `refine`, which reports the path. */
282
288
  type PinAdjustment<E> = {
283
- override?: StoredBodyOf<PartialBodyOf<E>>;
289
+ override?: StoredBodyOf<PartialBodyOf<E>> | KindRefOf<E>;
284
290
  extend?: StoredBodyOf<PartialBodyOf<E>>;
285
291
  hidden?: boolean;
286
292
  };
@@ -296,6 +302,7 @@ type StoredBodyOf<Body> = 'styles' extends keyof Body ? Omit<Body, 'styles'> & {
296
302
  * the same entry, with nothing to check the bodies against yet.
297
303
  */
298
304
  interface PinDirective {
305
+ /** A body, or a ref that re-points the path to another entity of the same kind. */
299
306
  readonly override?: Record<string, unknown>;
300
307
  readonly extend?: Record<string, unknown>;
301
308
  readonly hidden?: boolean;
@@ -428,5 +435,23 @@ declare function suggestLinkSlug({
428
435
  name: string;
429
436
  taken: ReadonlySet<string>;
430
437
  }): string;
438
+ /**
439
+ * A link's `options` entry whose `override` is a ref: the path it names reads as that entity
440
+ * (`'js/scale': { override: Motion.ref('css/scale') }`). `problem` says why it can't: the ref names
441
+ * another kind, it leads back to this path through other re-pointed paths, or, once the link's
442
+ * source is attached, it names nothing.
443
+ */
444
+ interface LinkRedirect {
445
+ /** The link's slug. */
446
+ readonly link: string;
447
+ readonly accessor: string;
448
+ readonly path: string;
449
+ readonly target: string;
450
+ readonly problem?: 'kind' | 'cycle' | 'missing';
451
+ }
452
+ /** Every re-pointed path the config's links declare, with what stops each one, if anything. */
453
+ declare function linkRedirects(config: Config): LinkRedirect[];
454
+ /** One sentence for a re-pointed path that can't be read. */
455
+ declare function redirectSentence(redirect: LinkRedirect): string;
431
456
  //#endregion
432
- export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkDeclaration, LinkedSystem, LinkedSystemClass, LinkedSystemCreateInput, LinkedSystemItemInput, LinkedSystemResolver, LinkedSystemState, PinDirective, PinOptions, PinOptionsField, PinOptionsInput, ResolvedSource, Source, SourceUnavailable, borrowedGroup, borrowedItem, isLinkSlug, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
457
+ export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkDeclaration, LinkRedirect, LinkedSystem, LinkedSystemClass, LinkedSystemCreateInput, LinkedSystemItemInput, LinkedSystemResolver, LinkedSystemState, PinDirective, PinOptions, PinOptionsField, PinOptionsInput, ResolvedSource, Source, SourceUnavailable, borrowedGroup, borrowedItem, isLinkSlug, linkRedirects, redirectSentence, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
@@ -1,5 +1,5 @@
1
1
  import { rejectData } from "../../framework/rejection.js";
2
- import { isRef } from "../../framework/utils/refs.js";
2
+ import { isRef, kindOf, pathOf, refSchema } from "../../framework/utils/refs.js";
3
3
  import { isPlainObject } from "../../framework/utils/field-path.js";
4
4
  import { pathInput } from "../../framework/schemas.js";
5
5
  import { kindBorrowable } from "../../framework/registry.js";
@@ -262,7 +262,7 @@ function exampleBorrowable(config, slug) {
262
262
  }
263
263
  }
264
264
  const pinOptions = z.record(z.string(), z.record(z.string(), z.strictObject({
265
- override: z.record(z.string(), z.unknown()).optional(),
265
+ override: z.union([refSchema, z.record(z.string(), z.unknown())]).optional(),
266
266
  extend: z.record(z.string(), z.unknown()).optional(),
267
267
  hidden: z.boolean().optional()
268
268
  }).refine((entry) => entry.override !== void 0 || entry.extend !== void 0 || entry.hidden !== void 0, { message: "An `options` entry needs `override`, `extend` or `hidden`." }))).optional().describe("Per-entity adjustments, keyed by accessor, then path, then verb.");
@@ -270,7 +270,7 @@ const LinkedSystemDefinition = defineEntity({
270
270
  kind: "linked-system",
271
271
  label: "Linked system",
272
272
  labelPlural: "Linked systems",
273
- invariants: (config) => config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`),
273
+ invariants: (config) => [...config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`), ...linkRedirects(config).filter((redirect) => redirect.problem !== void 0 && redirect.problem !== "missing").map(redirectSentence)],
274
274
  schemas: {
275
275
  create: { example: ({ config }) => {
276
276
  const path = freeExampleSlug(config);
@@ -892,5 +892,56 @@ function suggestLinkSlug({ name, taken }) {
892
892
  if (!taken.has(candidate)) return candidate;
893
893
  }
894
894
  }
895
+ /** Every re-pointed path the config's links declare, with what stops each one, if anything. */
896
+ function linkRedirects(config) {
897
+ const kindOfAccessor = (accessor) => config.accessors()[accessor]?.kind ?? accessor;
898
+ const declared = [];
899
+ for (const link of config.list("linked-system")) {
900
+ const { options } = link.toJSON();
901
+ for (const [accessor, entries] of Object.entries(options ?? {})) for (const [path, entry] of Object.entries(entries ?? {})) {
902
+ const override = isPlainObject(entry) ? entry.override : void 0;
903
+ if (!isRef(override)) continue;
904
+ declared.push({
905
+ link: link.path,
906
+ accessor,
907
+ path,
908
+ target: override.__ref
909
+ });
910
+ }
911
+ }
912
+ const next = /* @__PURE__ */ new Map();
913
+ for (const { accessor, path, target } of declared) next.set(`${kindOfAccessor(accessor)}:${path}`, `${kindOf(target)}:${pathOf(target)}`);
914
+ const loops = (start) => {
915
+ const seen = /* @__PURE__ */ new Set();
916
+ for (let at = start; at !== void 0; at = next.get(at)) {
917
+ if (seen.has(at)) return at === start;
918
+ seen.add(at);
919
+ }
920
+ return false;
921
+ };
922
+ return declared.map((redirect) => {
923
+ const kind = kindOfAccessor(redirect.accessor);
924
+ if (kindOf(redirect.target) !== kind) return {
925
+ ...redirect,
926
+ problem: "kind"
927
+ };
928
+ if (loops(`${kind}:${redirect.path}`)) return {
929
+ ...redirect,
930
+ problem: "cycle"
931
+ };
932
+ if (config.sourceFor(redirect.link) && !config.resolveRef(redirect.target)) return {
933
+ ...redirect,
934
+ problem: "missing"
935
+ };
936
+ return redirect;
937
+ });
938
+ }
939
+ /** One sentence for a re-pointed path that can't be read. */
940
+ function redirectSentence(redirect) {
941
+ const at = `Link "${redirect.link}": options.${redirect.accessor}["${redirect.path}"] re-points to ${redirect.target}`;
942
+ if (redirect.problem === "kind") return `${at}, which is no ${redirect.accessor} entry. A path can only read as another entity of its own kind.`;
943
+ if (redirect.problem === "cycle") return `${at}, which leads back to it through other re-pointed paths.`;
944
+ return `${at}, which names nothing this config can read.`;
945
+ }
895
946
  //#endregion
896
- export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkedSystem, borrowedGroup, borrowedItem, isLinkSlug, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
947
+ export { LINK_SLUG, LINK_SLUG_MESSAGE, LinkedSystem, borrowedGroup, borrowedItem, isLinkSlug, linkRedirects, redirectSentence, resolvedSource, sourceOfGroup, sourceOfItem, sourceSlugFor, sourceUnavailable, suggestLinkSlug };
@@ -73,7 +73,7 @@ interface RecordedSource {
73
73
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
74
74
  * that replaced it (see `detectedWireVersion`).
75
75
  */
76
- declare const SERIALIZED_CONFIG_VERSION = 20261009181635;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261009193004;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -1328,6 +1328,31 @@ declare class Config {
1328
1328
  * only as a borrow the picker offered and the read then refused.
1329
1329
  */
1330
1330
  private extensionBody;
1331
+ /**
1332
+ * A source's body with a linked extension's `extend` laid over it, the one merge every read and
1333
+ * write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
1334
+ * (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
1335
+ * this config resolves it, with the rest of the `extend` over it. The kind's
1336
+ * {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
1337
+ *
1338
+ * A target that resolves to nothing, names another kind, or leads back to this path through other
1339
+ * re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
1340
+ * nothing. The pin's invariants refuse the last two when they're written, and the build reports a
1341
+ * missing target (`brokenLinkRedirects`).
1342
+ */
1343
+ laidLinkedBody({
1344
+ kind,
1345
+ path,
1346
+ body,
1347
+ extend
1348
+ }: {
1349
+ kind: string;
1350
+ path: string;
1351
+ body: Record<string, unknown>;
1352
+ extend: Record<string, unknown>;
1353
+ }): Record<string, unknown>;
1354
+ /** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
1355
+ private redirectedBody;
1331
1356
  /**
1332
1357
  * Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
1333
1358
  * authored here, then parsed through this config's kind. Parsing on this side is what lets a React
@@ -1800,7 +1800,12 @@ var Config = class Config {
1800
1800
  kind: patch.kind,
1801
1801
  path: pathOf(extension)
1802
1802
  }) : void 0;
1803
- const inherited = current ? extendBody(current.source, current.overlay) : resolved.toJSON();
1803
+ const inherited = current ? this.at(snapshot).laidLinkedBody({
1804
+ kind: patch.kind,
1805
+ path: pathOf(extension),
1806
+ body: current.source,
1807
+ extend: current.overlay
1808
+ }) : resolved.toJSON();
1804
1809
  const materialized = snapshot.withChanges([itemChange(entry, patch.kind, patch.path, inherited)]);
1805
1810
  const changed = this.applySub(materialized, patch, materialized.get(patch.kind, patch.path) ?? resolved, mode);
1806
1811
  const body = changed.get(patch.kind, patch.path)?.toJSON();
@@ -1884,24 +1889,24 @@ var Config = class Config {
1884
1889
  kind: patch.kind,
1885
1890
  path: pathOf(extension)
1886
1891
  }) : void 0;
1887
- const written = applyPathDelta(current ? extendBody(rebasedLinkedBody({
1888
- entry,
1892
+ const written = applyPathDelta(current ? this.at(snapshot).laidLinkedBody({
1893
+ kind: patch.kind,
1889
1894
  path: pathOf(extension),
1890
1895
  body: current.source,
1891
1896
  extend: current.overlay
1892
- }), current.overlay) : resolved.toJSON(), delta, isField);
1897
+ }) : resolved.toJSON(), delta, isField);
1893
1898
  const local = { ...current ? current.overlay : extension.extend };
1894
1899
  for (const field of touched) if (Object.hasOwn(delta, field) && delta[field] === null) delete local[field];
1895
1900
  else if (Object.hasOwn(written, field)) local[field] = written[field];
1896
1901
  else delete local[field];
1897
1902
  const sourceBody = this.configFor(extension)?.resolve(patch.kind, pathOf(extension))?.toJSON();
1898
1903
  if (!sourceBody) reject("NOT_FOUND", `Cannot edit ${patch.kind} "${patch.path}" — its source "${extension.__ref}" is unavailable`);
1899
- const candidate = extendBody(rebasedLinkedBody({
1900
- entry,
1904
+ const candidate = this.at(snapshot).laidLinkedBody({
1905
+ kind: patch.kind,
1901
1906
  path: pathOf(extension),
1902
1907
  body: sourceBody,
1903
1908
  extend: local
1904
- }), local);
1909
+ });
1905
1910
  const createSchema = wholeBodySchema(entry, { config: this });
1906
1911
  let merged = candidate;
1907
1912
  if (createSchema) {
@@ -1920,8 +1925,12 @@ var Config = class Config {
1920
1925
  if (!sourceKind || !write) reject("UNSUPPORTED", `Cannot edit ${patch.kind} "${patch.path}" — "${slug}" has no way to record the change`);
1921
1926
  const accessor = view.accessorOf(patch.kind) ?? patch.kind;
1922
1927
  const existing = view.sourceOverlay(slug, patch.kind, patch.path);
1928
+ const { target: redirect, rest } = redirectOf(local);
1923
1929
  const overlay = {
1924
- ...classifyOverlay(local, sourceBody),
1930
+ ...redirect === void 0 ? classifyOverlay(local, sourceBody) : {
1931
+ override: { __ref: redirect },
1932
+ ...Object.keys(rest).length > 0 ? { extend: rest } : {}
1933
+ },
1925
1934
  ...existing?.hidden ? { hidden: true } : {}
1926
1935
  };
1927
1936
  const data = view.directiveWrite({
@@ -2642,13 +2651,12 @@ var Config = class Config {
2642
2651
  owns,
2643
2652
  namesItAlready: (target) => this.borrowsExactly(target, slug)
2644
2653
  }) : base.toJSON();
2645
- const extend = extension.extend ?? {};
2646
- const merged = extendBody(rebasedLinkedBody({
2647
- entry: this.entityOf(kind),
2654
+ const merged = this.laidLinkedBody({
2655
+ kind,
2648
2656
  path: pathOf(extension),
2649
2657
  body: inherited,
2650
- extend
2651
- }), extend);
2658
+ extend: extension.extend ?? {}
2659
+ });
2652
2660
  return this.entityOf(kind)?.prepareLinkedBody?.({
2653
2661
  path: pathOf(extension),
2654
2662
  body: merged,
@@ -2657,6 +2665,46 @@ var Config = class Config {
2657
2665
  }) ?? merged;
2658
2666
  }
2659
2667
  /**
2668
+ * A source's body with a linked extension's `extend` laid over it, the one merge every read and
2669
+ * write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
2670
+ * (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
2671
+ * this config resolves it, with the rest of the `extend` over it. The kind's
2672
+ * {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
2673
+ *
2674
+ * A target that resolves to nothing, names another kind, or leads back to this path through other
2675
+ * re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
2676
+ * nothing. The pin's invariants refuse the last two when they're written, and the build reports a
2677
+ * missing target (`brokenLinkRedirects`).
2678
+ */
2679
+ laidLinkedBody({ kind, path, body, extend }) {
2680
+ const { target, rest } = redirectOf(extend);
2681
+ const base = target === void 0 ? body : this.redirectedBody({
2682
+ kind,
2683
+ path,
2684
+ target
2685
+ }) ?? {};
2686
+ return extendBody(rebasedLinkedBody({
2687
+ entry: this.entityOf(kind),
2688
+ path,
2689
+ body: base,
2690
+ extend: rest
2691
+ }), rest);
2692
+ }
2693
+ /** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
2694
+ redirectedBody({ kind, path, target }) {
2695
+ if (kindOf(target) !== kind) return void 0;
2696
+ const reading = redirecting.get(this.state) ?? /* @__PURE__ */ new Set();
2697
+ const key = `${kind}:${path}`;
2698
+ if (reading.has(key)) return void 0;
2699
+ reading.add(key);
2700
+ redirecting.set(this.state, reading);
2701
+ try {
2702
+ return this.resolveRef(target)?.toJSON();
2703
+ } finally {
2704
+ reading.delete(key);
2705
+ }
2706
+ }
2707
+ /**
2660
2708
  * Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
2661
2709
  * authored here, then parsed through this config's kind. Parsing on this side is what lets a React
2662
2710
  * Native kind transform or reject a web value rather than trusting the source kind's rules.
@@ -3552,6 +3600,12 @@ var Config = class Config {
3552
3600
  },
3553
3601
  theirs
3554
3602
  });
3603
+ if (laid && redirectOf(laid).target !== void 0) return this.laidLinkedBody({
3604
+ kind,
3605
+ path,
3606
+ body: theirs,
3607
+ extend: laid
3608
+ });
3555
3609
  return laid ? {
3556
3610
  ...theirs,
3557
3611
  ...laid
@@ -5458,7 +5512,7 @@ var Config = class Config {
5458
5512
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5459
5513
  hydrateFrom(stored, options) {
5460
5514
  const claimed = schemaVersionOf(stored);
5461
- if (claimed > 20261009181635) throw new SchemaVersionTooNew(claimed);
5515
+ if (claimed > 20261009193004) throw new SchemaVersionTooNew(claimed);
5462
5516
  const wire = upgradeSerializedConfig(stored);
5463
5517
  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, or port the repo once with \`uds migrate\`.`);
5464
5518
  const leftover = wire.options;
@@ -5623,6 +5677,20 @@ function extendBody(base, delta) {
5623
5677
  * differently, and why the source's body is a parameter. `extend: { values: [Token.ref('surface')] }`
5624
5678
  * means "alongside what they already accept", which has nothing to join without their list.
5625
5679
  */
5680
+ /** The paths each config is reading through a re-pointed link, for {@link Config.laidLinkedBody} to
5681
+ * find a loop by. Keyed on the config's state, which every read of one config shares. */
5682
+ const redirecting = /* @__PURE__ */ new WeakMap();
5683
+ /**
5684
+ * The entity an `extend` re-points its path to, and the rest of it: a link's `override` written as a
5685
+ * ref lands in the stored `extend` as its `__ref`, beside whatever the `extend` verb adds.
5686
+ */
5687
+ function redirectOf(extend) {
5688
+ const { __ref: target, ...rest } = extend;
5689
+ return typeof target === "string" ? {
5690
+ target,
5691
+ rest
5692
+ } : { rest: extend };
5693
+ }
5626
5694
  /** A source's body as an extension reads it before its `extend` merges in: the kind's
5627
5695
  * {@link EntityKind.rebaseLinkedBody}, where it declares one. */
5628
5696
  function rebasedLinkedBody({ entry, path, body, extend }) {
@@ -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 = 20261009181635;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261009193004;
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 = 20261009181635;
16
+ const CURRENT_SCHEMA_VERSION = 20261009193004;
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 > 20261009181635) 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 > 20261009193004) 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 ?? 20261009181635;
105
+ const target = options?.target ?? 20261009193004;
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 ?? 20261009181635;
163
+ const target = options?.target ?? 20261009193004;
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 ?? 20261009181635;
182
+ const target = options?.target ?? 20261009193004;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A link's `options` entry may give `override` as a ref to another entity of the same kind, which
3
+ * re-points the path to it (`'js/scale': { override: Motion.ref('css/scale') }`). Every stored
4
+ * `override` is a body, so the wire shape of every stored config is unchanged. Additive, so the
5
+ * envelope passes through as it is.
6
+ */
7
+ import type { SchemaMigration } from '../framework/schema-version';
8
+ export declare const LINK_OVERRIDE_REF_VERSION = 20261009193004;
9
+ export declare const linkOverrideRefMigration: SchemaMigration;
@@ -0,0 +1,10 @@
1
+ //#region src/migrations/20261009193004_link_override_ref.ts
2
+ const LINK_OVERRIDE_REF_VERSION = 20261009193004;
3
+ const linkOverrideRefMigration = {
4
+ version: LINK_OVERRIDE_REF_VERSION,
5
+ up(json) {
6
+ return json;
7
+ }
8
+ };
9
+ //#endregion
10
+ export { LINK_OVERRIDE_REF_VERSION, linkOverrideRefMigration };
@@ -0,0 +1,2 @@
1
+ import { LinkRedirect, linkRedirects, redirectSentence } from "../entities/system/LinkedSystem.js";
2
+ export { type LinkRedirect, linkRedirects, redirectSentence };
@@ -0,0 +1,2 @@
1
+ import { linkRedirects, redirectSentence } from "../entities/system/LinkedSystem.js";
2
+ export { linkRedirects, redirectSentence };