@yahoo/uds-create-config 5.69.0 → 5.71.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.
@@ -36,6 +36,8 @@ import { lintIgnoreMigration } from "../migrations/20261027205147_lint_ignore.js
36
36
  import { componentMapPropsMigration } from "../migrations/20261030134149_component_map_props.js";
37
37
  import { layerPrefixMigration } from "../migrations/20261030163028_layer_prefix.js";
38
38
  import { defaultColorsMigration } from "../migrations/20261102143821_default_colors.js";
39
+ import { linkReplaceMigration } from "../migrations/20261102185610_link_replace.js";
40
+ import { mapPropsFromMigration } from "../migrations/20261102193912_map_props_from.js";
39
41
  import { CanvasRole } from "../entities/system/CanvasRole.js";
40
42
  import { GlobalStyle } from "../entities/system/GlobalStyle.js";
41
43
  import { Guidance } from "../entities/system/Guidance.js";
@@ -205,5 +207,7 @@ if (!registeredSchemaMigrations().has(20261027205147)) registerSchemaMigrations(
205
207
  if (!registeredSchemaMigrations().has(20261030134149)) registerSchemaMigrations(componentMapPropsMigration);
206
208
  if (!registeredSchemaMigrations().has(20261030163028)) registerSchemaMigrations(layerPrefixMigration);
207
209
  if (!registeredSchemaMigrations().has(20261102143821)) registerSchemaMigrations(defaultColorsMigration);
210
+ if (!registeredSchemaMigrations().has(20261102185610)) registerSchemaMigrations(linkReplaceMigration);
211
+ if (!registeredSchemaMigrations().has(20261102193912)) registerSchemaMigrations(mapPropsFromMigration);
208
212
  //#endregion
209
213
  export { System };
@@ -873,11 +873,16 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
873
873
  readonly input: z.ZodObject<{
874
874
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
875
875
  data: z.ZodObject<{
876
- rules: z.ZodArray<z.ZodObject<{
876
+ from: z.ZodOptional<z.ZodString>;
877
+ only: z.ZodOptional<z.ZodObject<{
878
+ canvas: z.ZodString;
879
+ nodes: z.ZodArray<z.ZodString>;
880
+ }, z.core.$strip>>;
881
+ rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
877
882
  when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
878
883
  set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
879
884
  unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
880
- }, z.core.$strip>>;
885
+ }, z.core.$strip>>>;
881
886
  }, z.core.$strip>;
882
887
  }, z.core.$strip>;
883
888
  };
@@ -885,11 +890,12 @@ declare const ComponentEntity: EntityClass<z.ZodObject<{
885
890
  readonly input: z.ZodObject<{
886
891
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
887
892
  data: z.ZodObject<{
888
- rules: z.ZodArray<z.ZodObject<{
893
+ from: z.ZodOptional<z.ZodString>;
894
+ rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
889
895
  when: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
890
896
  set: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
891
897
  unset: z.ZodOptional<z.ZodArray<z.ZodString>>;
892
- }, z.core.$strip>>;
898
+ }, z.core.$strip>>>;
893
899
  instances: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
894
900
  }, z.core.$strip>;
895
901
  }, z.core.$strip>;
@@ -24,7 +24,7 @@ import { slotAnatomyPropOf, slotTargetsOf, withoutTruthyTerm } from "../../frame
24
24
  import { listSlots, namedSlots, slotHostedBy, slotOf, slotsFor } from "../../spec/slot.js";
25
25
  import { cellAt } from "../../spec/cell.js";
26
26
  import { instanceAt } from "../../spec/instance.js";
27
- import { mapComponentProps, propMappingErrors, propMappingRulesSchema } from "../../spec/prop-mapping.js";
27
+ import { mapComponentProps, mapPropsRulesSchema, mappingFromSchema, mappingScopeSchema, propMappingErrors } from "../../spec/prop-mapping.js";
28
28
  import { forwardOptions, forwardsWrite } from "./component-forwards.js";
29
29
  import { assertComponentSourceInput } from "./component-source-fields.js";
30
30
  import { checkStyleSet, styleSetEdits } from "./component-style-set.js";
@@ -2947,12 +2947,16 @@ element: refSchema })
2947
2947
  "map-props": {
2948
2948
  input: z.object({
2949
2949
  path: pathInput("component"),
2950
- data: z.object({ rules: propMappingRulesSchema })
2950
+ data: z.object({
2951
+ from: mappingFromSchema.optional(),
2952
+ only: mappingScopeSchema.optional(),
2953
+ rules: mapPropsRulesSchema
2954
+ })
2951
2955
  }),
2952
2956
  readOnly: false,
2953
2957
  approval: "reference-change",
2954
2958
  label: "Map props",
2955
- description: "Move a component's instances onto its props as they are now, after its API changed: `variant: 'primary'` becomes `color: 'brand', prominence: 'solid'`. It changes nothing in the config. Where it runs decides what it moves: in Create, the instances in the branch's design files; in a checkout (`uds config edit`, the local UDS MCP), the call sites in the project's source. Refuses a rule that writes a prop or value the component doesn't take. Run it with `plan: true` first: the plan says what it would move, and writes nothing.",
2959
+ description: "Move a component's instances onto its props as they are now, after its API changed: `variant: 'primary'` becomes `color: 'brand', prominence: 'solid'`. It changes nothing in the config. Where it runs decides what it moves: in Create, the instances in the branch's design files; in a checkout (`uds config edit`, the local UDS MCP), the call sites in the project's source. With `from`, it swaps the component too: instances of `from`, a component the system may no longer have, become this component's, and the rules move their props onto it (`when: {}` reaches every swapped instance). Refuses a rule that writes a prop or value the component doesn't take. Run it with `plan: true` first: the plan says what it would move, and writes nothing.",
2956
2960
  scope: "item",
2957
2961
  title: (entity) => `Map ${entity} props`,
2958
2962
  example: ({ config, path }) => {
@@ -2963,10 +2967,7 @@ element: refSchema })
2963
2967
  };
2964
2968
  },
2965
2969
  handler: (input, config) => {
2966
- const errors = propMappingErrors(config, {
2967
- component: String(input.path),
2968
- rules: input.data.rules
2969
- });
2970
+ const errors = propMappingErrors(config, mappingOf(String(input.path), input.data));
2970
2971
  if (errors.length > 0) rejectData(errors.join(" "));
2971
2972
  return config;
2972
2973
  }
@@ -2975,7 +2976,8 @@ element: refSchema })
2975
2976
  input: z.object({
2976
2977
  path: pathInput("component"),
2977
2978
  data: z.object({
2978
- rules: propMappingRulesSchema,
2979
+ from: mappingFromSchema.optional(),
2980
+ rules: mapPropsRulesSchema,
2979
2981
  instances: z.array(z.record(z.string(), z.unknown())).default([]).describe("Instances' props as they are stored or written: each comes back with the rules applied.")
2980
2982
  })
2981
2983
  }),
@@ -2987,10 +2989,7 @@ element: refSchema })
2987
2989
  example: ({ config, path }) => exampleMapping(config, path),
2988
2990
  handler: (input, config) => {
2989
2991
  const data = input.data;
2990
- return mapComponentProps(config, {
2991
- component: String(input.path),
2992
- rules: data.rules
2993
- }, data.instances ?? []);
2992
+ return mapComponentProps(config, mappingOf(String(input.path), input.data), data.instances ?? []);
2994
2993
  }
2995
2994
  }
2996
2995
  },
@@ -3140,6 +3139,17 @@ function exampleProps(config, componentPath) {
3140
3139
  }
3141
3140
  return {};
3142
3141
  }
3142
+ /** The mapping a `map-props` op's input names, refused when a mapping without `from` has no rule. */
3143
+ function mappingOf(component, data) {
3144
+ const { from, only, rules = [] } = data ?? {};
3145
+ if (from === void 0 && rules.length === 0) rejectData("Name at least one rule in `rules`, or `from` to swap another component's instances onto this one.");
3146
+ return {
3147
+ component,
3148
+ ...from === void 0 ? {} : { from: from.replace(/^component:/, "") },
3149
+ ...only === void 0 ? {} : { only },
3150
+ rules
3151
+ };
3152
+ }
3143
3153
  /** A runnable `map-props-plan` example: an instance holding the first variant prop's first value, and
3144
3154
  * a rule moving it onto the prop's second value (or the same one, for a prop with a single value).
3145
3155
  * A component with no variant gets the other common case, dropping a prop it no longer has.
@@ -123,6 +123,7 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
123
123
  keep: "keep";
124
124
  take: "take";
125
125
  }>>;
126
+ replace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
126
127
  }, z.core.$strip>;
127
128
  };
128
129
  readonly 'link-plan': {
@@ -133,6 +134,7 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
133
134
  keep: "keep";
134
135
  take: "take";
135
136
  }>>;
137
+ replace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
136
138
  }, z.core.$strip>;
137
139
  };
138
140
  readonly unlink: {
@@ -171,7 +171,16 @@ const linkInput = z.object({
171
171
  data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]),
172
172
  /** What to do at a path another linked system provides: `'take'` makes it follow this source,
173
173
  * `'keep'` leaves it where it comes from. Required when there is one; `link-plan` lists them. */
174
- onLinked: z.enum(["keep", "take"]).optional()
174
+ onLinked: z.enum(["keep", "take"]).optional(),
175
+ /**
176
+ * A component this config holds under one name, replaced with the source's of another name, keyed
177
+ * by accessor: `{ components: { SelectContent: 'SelectMenu' } }` replaces the `SelectContent` this
178
+ * config holds with the source's `SelectMenu`. Every ref to `SelectContent` is renamed to
179
+ * `SelectMenu`, so design files and code follow the way they follow a rename, and the system
180
+ * `SelectContent` came through stops deriving it unless its own rows still use it. Each source path
181
+ * is one `data` names; `link-plan` lists each in `replaced`. A take at the same name is `onLinked`.
182
+ */
183
+ replace: z.record(z.string(), z.record(z.string().min(1), z.string().min(1))).optional()
175
184
  });
176
185
  const unlinkInput = z.object({
177
186
  path: pathInput("linked-system"),
@@ -216,7 +225,8 @@ function linkPlanInputOf(input) {
216
225
  return {
217
226
  path: String(input.path),
218
227
  data: input.data,
219
- ...input.onLinked === void 0 ? {} : { onLinked: input.onLinked }
228
+ ...input.onLinked === void 0 ? {} : { onLinked: input.onLinked },
229
+ ...input.replace === void 0 ? {} : { replace: input.replace }
220
230
  };
221
231
  }
222
232
  /** A `link` or `unlink` example: one entity the source offers, under the accessor this config files it by. */
@@ -310,7 +320,7 @@ const LinkedSystemDefinition = defineEntity({
310
320
  input: linkInput,
311
321
  readOnly: false,
312
322
  label: "Link from",
313
- description: "Draw entities from a linked system, following the source: `data` names them by accessor — `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What this config already holds at those paths is replaced by theirs, so run `linked-system/link-plan` with the same input first to see what that is. To hold their values as your own instead, `copy`.",
323
+ description: "Draw entities from a linked system, following the source: `data` names them by accessor — `{ tokens: ['color/brand', 'color'] }` takes an entity and a folder's members, `{ tokens: '*' }` every token, and `'*'` the whole system. What this config already holds at those paths is replaced by theirs, so run `linked-system/link-plan` with the same input first to see what that is. To replace something this config holds under another name (the `SelectContent` it has with the source's `SelectMenu`), name the pair in `replace` (`{ components: { SelectContent: 'SelectMenu' } }`): every reference to the old name moves to the new one, in design files and code too. To hold their values as your own instead, `copy`.",
314
324
  scope: "item",
315
325
  approval: "reference-change",
316
326
  title: (entity) => `Link something from ${entity}`,
@@ -333,7 +343,7 @@ const LinkedSystemDefinition = defineEntity({
333
343
  input: linkInput,
334
344
  readOnly: true,
335
345
  label: "Plan a link",
336
- description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), the `options` entries another linked system holds at what a take moves, which it removes since they would apply to nothing (`dropped`), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
346
+ description: "What `link` with the same input replaces — rows of yours deleted so the source shows through, every path another linked system provides (`linked`, turned into extensions when `onLinked` says take), the `options` entries another linked system holds at what a take moves, which it removes since they would apply to nothing (`dropped`), members left alone because they are already here from the same origin, paths another linked system declares that the link reaches as a different entity (`settled`), whose row stays, what `replace` replaces under another name (`replaced`: each path, what replaces it, the pin it came through, and whether it is left out of that pin or kept because rows of that pin still use it), and why the config would refuse the link (`blocked`), a collision it would leave among them. Writes nothing.",
337
347
  scope: "item",
338
348
  title: (entity) => `Plan a link from ${entity}`,
339
349
  example: linkExample,
@@ -9,12 +9,22 @@ type LinkPaths = '*' | Readonly<Record<string, '*' | readonly string[]>>;
9
9
  /** What a link does at a path held through another linked system: `'take'` makes the row follow this
10
10
  * source, `'keep'` leaves it with the system it comes from. */
11
11
  type OnLinked = 'keep' | 'take';
12
+ /** Per accessor, a path this config holds → the source's path that replaces it under another name. */
13
+ type LinkReplacements = Readonly<Record<string, Readonly<Record<string, string>>>>;
12
14
  interface LinkPlanInput {
13
15
  /** The pin's slug. */
14
16
  readonly path: string;
15
17
  readonly data: LinkPaths;
16
18
  /** Planned as `'take'` when omitted; the `link` op refuses without it while `linked` is not empty. */
17
19
  readonly onLinked?: OnLinked;
20
+ /**
21
+ * What this config holds under one name that the source's entity replaces under another:
22
+ * `{ components: { SelectContent: 'SelectMenu' } }` replaces the `SelectContent` this config holds
23
+ * with the source's `SelectMenu`, and everything that pointed at `SelectContent` points at
24
+ * `SelectMenu` after it. Each source path is one `data` names. A take at the same path is
25
+ * `onLinked: 'take'`, not this.
26
+ */
27
+ readonly replace?: LinkReplacements;
18
28
  }
19
29
  /** A path the link reaches that another linked system provides, or declares under a row of this
20
30
  * config's own. */
@@ -40,6 +50,22 @@ interface StrandedDirective extends DroppedDirective {
40
50
  interface LinkArrival extends EntityAddress {
41
51
  readonly named: boolean;
42
52
  }
53
+ /** A row this config held that a `replace` entry replaces with the source's entity of another name.
54
+ * `path` is the row's; every ref to it is renamed to `with`. */
55
+ interface LinkReplacement extends EntityAddress {
56
+ /** The source's path that replaces it, which is where the row lands. */
57
+ readonly with: string;
58
+ /** The slug of the pin the replaced row came through; absent for a row of this config's own. */
59
+ readonly from?: string;
60
+ /** Whether the replaced path was left out of that pin, so it no longer derives here. */
61
+ readonly excluded: boolean;
62
+ /** That pin's own rows still drawing on the replaced path, which keep it held. */
63
+ readonly keptFor?: readonly EntityAddress[];
64
+ /** Why the replaced path couldn't be left out of that pin, when it couldn't. */
65
+ readonly keptBecause?: string;
66
+ /** That pin's `options` entries at the replaced path, removed with it. */
67
+ readonly dropped: readonly EntityAddress[];
68
+ }
43
69
  interface LinkPlan {
44
70
  /** The pin's `link` once the op has run. */
45
71
  readonly declared: LinkDeclaration | undefined;
@@ -53,6 +79,8 @@ interface LinkPlan {
53
79
  readonly linked: readonly LinkedElsewhere[];
54
80
  /** Rows held through another pin, turned into extensions through this one. */
55
81
  readonly extended: readonly EntityAddress[];
82
+ /** What `replace` replaces, each with the source's entity it's replaced with. */
83
+ readonly replaced: readonly LinkReplacement[];
56
84
  /** The other pins' `options` entries at what the take moves, removed with it: they adjust only the
57
85
  * rows their pin derives, and the row there follows this one now. Not carried onto the taken row,
58
86
  * since an entry corrects what that pin brings rather than stating a value of this config's own. */
@@ -156,6 +184,11 @@ interface SourceRowState {
156
184
  * that leaves a referrer dangling until the declaration covers the path. A member
157
185
  * already here from the source's own origin is left alone and kept out of the declaration, so a
158
186
  * folder that holds one is declared by its other members.
187
+ *
188
+ * A `replace` entry replaces a row this config holds with the source's entity of another name: that
189
+ * row becomes an extension of the source's, is renamed to the source's path so every ref follows
190
+ * (`replacementTakes`), and is left out of the pin it came through unless that pin's own rows still
191
+ * draw on it.
159
192
  */
160
193
  declare function planLink({
161
194
  config,
@@ -214,4 +247,4 @@ declare function sourceRowStates({
214
247
  readonly slug: string;
215
248
  }): SourceRowState[];
216
249
  //#endregion
217
- export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, DroppedDirective, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
250
+ export { BreakOutcome, BreakPlan, BreakPlanInput, BreakPlanRow, BreakReach, DroppedDirective, LinkArrival, LinkPaths, LinkPlan, LinkPlanInput, LinkReplacement, LinkReplacements, LinkedElsewhere, OnLinked, SourceRowState, UnlinkPlan, UnlinkPlanInput, planBreak, planLink, planUnlink, sourceRowStates };
@@ -71,6 +71,11 @@ function declaredElsewhere({ config, slug, kind, path }) {
71
71
  * that leaves a referrer dangling until the declaration covers the path. A member
72
72
  * already here from the source's own origin is left alone and kept out of the declaration, so a
73
73
  * folder that holds one is declared by its other members.
74
+ *
75
+ * A `replace` entry replaces a row this config holds with the source's entity of another name: that
76
+ * row becomes an extension of the source's, is renamed to the source's path so every ref follows
77
+ * (`replacementTakes`), and is left out of the pin it came through unless that pin's own rows still
78
+ * draw on it.
74
79
  */
75
80
  function planLink({ config, input }) {
76
81
  return planLinkApplied({
@@ -116,7 +121,15 @@ function planLinkApplied({ config, input }) {
116
121
  /** Per accessor, what the declaration gains: the kind whole, or paths. */
117
122
  const gains = /* @__PURE__ */ new Map();
118
123
  let leftAnywhere = false;
124
+ const replacements = replacementTakes({
125
+ config,
126
+ slug,
127
+ source,
128
+ input
129
+ });
130
+ const landing = new Set(replacements.entries.map(({ kind, path }) => `${kind}:${path}`));
119
131
  const shadow = (kind, path) => {
132
+ if (landing.has(`${kind}:${path}`)) return true;
120
133
  const held = config.resolve(kind, path);
121
134
  if (!held || held.borrowedFrom === slug) return true;
122
135
  if (sharedWithOrigin({
@@ -261,12 +274,13 @@ function planLinkApplied({ config, input }) {
261
274
  declared = Object.keys(next).length === 0 ? current : next;
262
275
  }
263
276
  const planned = [
277
+ ...replacements.patches,
264
278
  ...takes,
265
279
  ...declares,
266
280
  ...shadows
267
281
  ];
268
282
  let result = planned.length > 0 ? applied(config, planned) : void 0;
269
- if (takes.length > 0 && result && "failed" in result) throw result.failed;
283
+ if ((takes.length > 0 || replacements.patches.length > 0) && result && "failed" in result) throw result.failed;
270
284
  const { dropped, stranded, patches: drops } = takes.length > 0 && result && "config" in result ? directivesTakenFrom({
271
285
  config,
272
286
  slug,
@@ -277,7 +291,74 @@ function planLinkApplied({ config, input }) {
277
291
  patches: []
278
292
  };
279
293
  if (drops.length > 0 && result && "config" in result) result = applied(result.config, drops);
280
- const patches = [...planned, ...drops];
294
+ const taken = [];
295
+ const leaves = [];
296
+ for (const entry of replacements.entries) {
297
+ const { kind, accessor, path, replaces, from } = entry;
298
+ if (from === void 0 || !result || !("config" in result)) {
299
+ taken.push({
300
+ kind,
301
+ path: replaces,
302
+ with: path,
303
+ ...from ? { from } : {},
304
+ excluded: false,
305
+ dropped: []
306
+ });
307
+ continue;
308
+ }
309
+ const leftOut = { [accessor]: [replaces] };
310
+ const keptFor = linkedDependents({
311
+ config: result.config,
312
+ slug: from,
313
+ data: leftOut
314
+ });
315
+ if (keptFor.length > 0) {
316
+ taken.push({
317
+ kind,
318
+ path: replaces,
319
+ with: path,
320
+ from,
321
+ excluded: false,
322
+ keptFor,
323
+ dropped: []
324
+ });
325
+ continue;
326
+ }
327
+ const exclusion = planExclude({
328
+ config: result.config,
329
+ input: {
330
+ path: from,
331
+ data: leftOut
332
+ }
333
+ });
334
+ if (exclusion.blocked !== void 0) {
335
+ taken.push({
336
+ kind,
337
+ path: replaces,
338
+ with: path,
339
+ from,
340
+ excluded: false,
341
+ keptBecause: exclusion.blocked,
342
+ dropped: []
343
+ });
344
+ continue;
345
+ }
346
+ leaves.push(...exclusion.patches);
347
+ result = applied(result.config, exclusion.patches);
348
+ taken.push({
349
+ kind,
350
+ path: replaces,
351
+ with: path,
352
+ from,
353
+ excluded: true,
354
+ dropped: exclusion.dropped
355
+ });
356
+ }
357
+ const patches = [
358
+ ...planned,
359
+ ...drops,
360
+ ...leaves
361
+ ];
281
362
  return {
282
363
  plan: {
283
364
  declared,
@@ -291,6 +372,7 @@ function planLinkApplied({ config, input }) {
291
372
  addresses,
292
373
  linked,
293
374
  extended,
375
+ replaced: taken,
294
376
  dropped,
295
377
  shared,
296
378
  patches,
@@ -303,6 +385,79 @@ function planLinkApplied({ config, input }) {
303
385
  };
304
386
  }
305
387
  /**
388
+ * The edits that take each `replace` entry: the replaced row becomes this config's own extension of
389
+ * the source's entity, still at the old path, and is then renamed to the source's path. A rename is
390
+ * the edit that moves every ref, and the one design files and code follow, so the replaced name's
391
+ * instances arrive on the taken entity the way a rename's would. The config refuses to rename a row
392
+ * held under its source's own name, so the extension comes first: what it holds is then a name this
393
+ * config gave.
394
+ */
395
+ function replacementTakes({ config, slug, source, input }) {
396
+ const entries = [];
397
+ const patches = [];
398
+ const accessors = config.accessors();
399
+ const landing = /* @__PURE__ */ new Map();
400
+ for (const [accessor, pairs] of Object.entries(input.replace ?? {})) {
401
+ const kind = accessors[accessor]?.kind;
402
+ if (!kind) rejectData(`Cannot replace anything through "${slug}" — this system has no "${accessor}".`, ["replace", accessor]);
403
+ for (const [replaces, path] of Object.entries(pairs)) {
404
+ const at = [
405
+ "replace",
406
+ accessor,
407
+ replaces
408
+ ];
409
+ if (input.data !== "*" && !covers({
410
+ declared: input.data,
411
+ accessor,
412
+ path
413
+ })) rejectData(`Cannot replace "${replaces}" with "${path}" — \`data\` doesn't link "${path}" through "${slug}". Name it there too.`, at);
414
+ if (!source.resolve(kind, path)) rejectData(`Cannot replace "${replaces}" with "${path}" — "${slug}" has no ${kind} "${path}".`, at);
415
+ if (path === replaces) rejectData(`Cannot replace "${path}" with itself — taking the source's "${path}" over it is \`onLinked: 'take'\`.`, at);
416
+ const twice = landing.get(`${kind}:${path}`);
417
+ if (twice !== void 0) rejectData(`Cannot replace both "${twice}" and "${replaces}" with "${path}".`, at);
418
+ const held = config.resolve(kind, replaces);
419
+ if (!held) rejectData(`Cannot replace "${replaces}" — this system has no ${kind} "${replaces}".`, at);
420
+ if (held.borrowedFrom === slug) rejectData(`Cannot replace "${replaces}" with "${path}" — "${replaces}" already comes from "${slug}".`, at);
421
+ const occupied = config.storedBody(kind, path);
422
+ if (occupied !== void 0 && !isMintedRow(occupied)) rejectData(`Cannot replace "${replaces}" with "${path}" — this system already has a ${kind} of its own at "${path}".`, at);
423
+ landing.set(`${kind}:${path}`, replaces);
424
+ const from = held.borrowedFrom ?? declaredElsewhere({
425
+ config,
426
+ slug,
427
+ kind,
428
+ path: replaces
429
+ });
430
+ const stored = config.storedBody(kind, replaces);
431
+ const minted = stored !== void 0 && isMintedRow(stored);
432
+ entries.push({
433
+ kind,
434
+ accessor,
435
+ path,
436
+ replaces,
437
+ ...from ? { from } : {}
438
+ });
439
+ patches.push({
440
+ kind,
441
+ operation: minted ? "create" : "replace",
442
+ path: replaces,
443
+ data: {
444
+ ...ref(kind, path, slug),
445
+ extend: {}
446
+ }
447
+ }, {
448
+ kind,
449
+ operation: "rename",
450
+ path: replaces,
451
+ to: path
452
+ });
453
+ }
454
+ }
455
+ return {
456
+ entries,
457
+ patches
458
+ };
459
+ }
460
+ /**
306
461
  * The other pins' `options` entries a take leaves applying to nothing, and the pin edits that remove
307
462
  * them.
308
463
  *
@@ -74,7 +74,7 @@ interface RecordedSource {
74
74
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
75
75
  * that replaced it (see `detectedWireVersion`).
76
76
  */
77
- declare const SERIALIZED_CONFIG_VERSION = 20261102143821;
77
+ declare const SERIALIZED_CONFIG_VERSION = 20261102193912;
78
78
  interface SerializedConfig {
79
79
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
80
80
  readonly version: number;
@@ -6391,7 +6391,7 @@ var Config = class Config {
6391
6391
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
6392
6392
  hydrateFrom(stored, options) {
6393
6393
  const claimed = schemaVersionOf(stored);
6394
- if (claimed > 20261102143821) throw new SchemaVersionTooNew(claimed);
6394
+ if (claimed > 20261102193912) throw new SchemaVersionTooNew(claimed);
6395
6395
  const wire = upgradeSerializedConfig(stored);
6396
6396
  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.`);
6397
6397
  const leftover = wire.options;
@@ -25,6 +25,11 @@ interface ComponentApiChange {
25
25
  readonly component: string;
26
26
  /** `true` when `after` has no such component: every instance of it names nothing. */
27
27
  readonly removed: boolean;
28
+ /**
29
+ * The path `after` holds the component at, when a rename moved it: its instances follow the rename,
30
+ * and `breaks` are what its props lose against the component they land on.
31
+ */
32
+ readonly renamedTo?: string;
28
33
  readonly breaks: readonly ComponentApiBreak[];
29
34
  /** Props `after` exposes that `before` didn't, in declaration order. */
30
35
  readonly added: readonly string[];
@@ -33,7 +38,15 @@ interface ComponentApiChange {
33
38
  * The components whose API `after` broke for instances stored against `before`, in `before`'s
34
39
  * order. A component that only gained props isn't listed. `components` narrows the read to those
35
40
  * paths, for a caller that already knows what a change touched.
41
+ *
42
+ * `renames` are the component renames between the two, oldest first (`documentRenamesFromPatches`
43
+ * reads them off a draft). A renamed component's instances follow the rename, so it isn't removed:
44
+ * its props are compared with the component it lands on, which is what replacing a component with
45
+ * a source's of another name breaks, and a rename that keeps every prop breaks nothing.
36
46
  */
37
- declare function componentApiChanges(before: Config, after: Config, components?: readonly string[]): ComponentApiChange[];
47
+ declare function componentApiChanges(before: Config, after: Config, components?: readonly string[], renames?: readonly {
48
+ readonly from: string;
49
+ readonly to: string;
50
+ }[]): ComponentApiChange[];
38
51
  //#endregion
39
52
  export { ComponentApiBreak, ComponentApiChange, componentApiChanges };
@@ -43,8 +43,8 @@ function breaksOf(configs, component, before, after) {
43
43
  });
44
44
  continue;
45
45
  }
46
- const wasValues = valuesOf(configs.before, component, was);
47
- const isValues = valuesOf(configs.after, component, is);
46
+ const wasValues = valuesOf(configs.before, component.before, was);
47
+ const isValues = valuesOf(configs.after, component.after, is);
48
48
  if (wasValues && isValues) {
49
49
  const kept = new Set(isValues);
50
50
  const gone = wasValues.filter((value) => !kept.has(value));
@@ -65,14 +65,26 @@ function breaksOf(configs, component, before, after) {
65
65
  * The components whose API `after` broke for instances stored against `before`, in `before`'s
66
66
  * order. A component that only gained props isn't listed. `components` narrows the read to those
67
67
  * paths, for a caller that already knows what a change touched.
68
+ *
69
+ * `renames` are the component renames between the two, oldest first (`documentRenamesFromPatches`
70
+ * reads them off a draft). A renamed component's instances follow the rename, so it isn't removed:
71
+ * its props are compared with the component it lands on, which is what replacing a component with
72
+ * a source's of another name breaks, and a rename that keeps every prop breaks nothing.
68
73
  */
69
- function componentApiChanges(before, after, components) {
74
+ function componentApiChanges(before, after, components, renames = []) {
70
75
  const paths = components ?? before.list("component").map((entity) => entity.path);
71
76
  const changes = [];
77
+ const landsAt = (path) => {
78
+ let at = path;
79
+ for (const rename of renames) if (rename.from === at) at = rename.to;
80
+ return at;
81
+ };
72
82
  for (const component of paths) {
73
83
  if (!before.resolve("component", component)) continue;
74
84
  const was = surfaceProps(before, component);
75
- if (!after.resolve("component", component)) {
85
+ const target = landsAt(component);
86
+ const renamedTo = target === component ? void 0 : target;
87
+ if (!after.resolve("component", target)) {
76
88
  changes.push({
77
89
  component,
78
90
  removed: true,
@@ -81,16 +93,20 @@ function componentApiChanges(before, after, components) {
81
93
  });
82
94
  continue;
83
95
  }
84
- const is = surfaceProps(after, component);
96
+ const is = surfaceProps(after, target);
85
97
  const breaks = breaksOf({
86
98
  before,
87
99
  after
88
- }, component, was, is);
100
+ }, {
101
+ before: component,
102
+ after: target
103
+ }, was, is);
89
104
  if (breaks.length === 0) continue;
90
105
  const had = new Set(was.map((prop) => prop.name));
91
106
  changes.push({
92
107
  component,
93
108
  removed: false,
109
+ ...renamedTo ? { renamedTo } : {},
94
110
  breaks,
95
111
  added: is.filter((prop) => !had.has(prop.name)).map((prop) => prop.name)
96
112
  });
@@ -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 = 20261102143821;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261102193912;
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 = 20261102143821;
16
+ const CURRENT_SCHEMA_VERSION = 20261102193912;
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 > 20261102143821) 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 > 20261102193912) 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 ?? 20261102143821;
105
+ const target = options?.target ?? 20261102193912;
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 ?? 20261102143821;
163
+ const target = options?.target ?? 20261102193912;
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 ?? 20261102143821;
182
+ const target = options?.target ?? 20261102193912;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];