@yahoo/uds-create-config 5.26.0 → 5.27.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.
@@ -69,10 +69,19 @@ declare const ReactNativeSystemDefinition: ConfigClass<{
69
69
  tokens: EntityClass<_$zod.ZodObject<Omit<{
70
70
  value: _$zod.ZodType<string | number | Ref | ComposedValue, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
71
71
  type: _$zod.ZodOptional<_$zod.ZodString>;
72
- overrides: _$zod.ZodOptional<SubEntityClass<_$zod.ZodObject<{
72
+ overrides: _$zod.ZodOptional<_$zod.ZodUnion<readonly [_$zod.ZodPipe<_$zod.ZodArray<_$zod.ZodObject<{
73
73
  on: _$zod.ZodType<OverrideCondition, unknown, _$zod_v4_core0.$ZodTypeInternals<OverrideCondition, unknown>>;
74
74
  value: _$zod.ZodType<string | number | Ref | ComposedValue, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
75
- }, _$zod_v4_core0.$strip>>>;
75
+ }, _$zod_v4_core0.$strip>>, _$zod.ZodTransform<Record<string, {
76
+ on: OverrideCondition;
77
+ value: string | number | Ref | ComposedValue;
78
+ }>, {
79
+ on: OverrideCondition;
80
+ value: string | number | Ref | ComposedValue;
81
+ }[]>>, SubEntityClass<_$zod.ZodObject<{
82
+ on: _$zod.ZodType<OverrideCondition, unknown, _$zod_v4_core0.$ZodTypeInternals<OverrideCondition, unknown>>;
83
+ value: _$zod.ZodType<string | number | Ref | ComposedValue, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
84
+ }, _$zod_v4_core0.$strip>>]>>;
76
85
  utility: _$zod.ZodOptional<_$zod.ZodBoolean>;
77
86
  property: _$zod.ZodOptional<_$zod.ZodObject<{
78
87
  syntax: _$zod.ZodOptional<_$zod.ZodString>;
@@ -81,10 +90,19 @@ declare const ReactNativeSystemDefinition: ConfigClass<{
81
90
  cssVariable: _$zod.ZodOptional<_$zod.ZodUnion<readonly [_$zod.ZodString, _$zod.ZodLiteral<false>]>>;
82
91
  }, "value" | "property" | "overrides" | "cssVariable"> & {
83
92
  value: _$zod.ZodType<string | number | boolean | Ref | DerivedColor, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
84
- overrides: _$zod.ZodOptional<SubEntityClass<_$zod.ZodObject<{
93
+ overrides: _$zod.ZodOptional<_$zod.ZodUnion<readonly [_$zod.ZodPipe<_$zod.ZodArray<_$zod.ZodObject<{
94
+ on: _$zod.ZodType<OverrideCondition, unknown, _$zod_v4_core0.$ZodTypeInternals<OverrideCondition, unknown>>;
95
+ value: _$zod.ZodType<string | number | boolean | Ref | DerivedColor, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
96
+ }, _$zod_v4_core0.$strip>>, _$zod.ZodTransform<Record<string, {
97
+ on: OverrideCondition;
98
+ value: string | number | boolean | Ref | DerivedColor;
99
+ }>, {
100
+ on: OverrideCondition;
101
+ value: string | number | boolean | Ref | DerivedColor;
102
+ }[]>>, SubEntityClass<_$zod.ZodObject<{
85
103
  on: _$zod.ZodType<OverrideCondition, unknown, _$zod_v4_core0.$ZodTypeInternals<OverrideCondition, unknown>>;
86
104
  value: _$zod.ZodType<string | number | boolean | Ref | DerivedColor, unknown, _$zod_v4_core0.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
87
- }, _$zod_v4_core0.$strip>>>;
105
+ }, _$zod_v4_core0.$strip>>]>>;
88
106
  }, _$zod_v4_core0.$strip>, _$zod.ZodObject<Omit<{
89
107
  cssPrefix: _$zod.ZodOptional<_$zod.ZodString>;
90
108
  cssVariables: _$zod.ZodOptional<_$zod.ZodBoolean>;
@@ -67,6 +67,7 @@ import { cssMotionStrictMigration } from "../migrations/20261009181635_css_motio
67
67
  import { linkOverrideRefMigration } from "../migrations/20261009193004_link_override_ref.js";
68
68
  import { linkedComponentModulesMigration } from "../migrations/20261012200606_linked_component_modules.js";
69
69
  import { componentStatesMigration } from "../migrations/20261013154037_component_states.js";
70
+ import { tokenOverridesListMigration } from "../migrations/20261013164300_token_overrides_list.js";
70
71
  const System = defineConfig({
71
72
  name: WEB_CONFIG_NAME,
72
73
  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.",
@@ -145,5 +146,6 @@ if (!registeredSchemaMigrations().has(20261009181635)) registerSchemaMigrations(
145
146
  if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(linkOverrideRefMigration);
146
147
  if (!registeredSchemaMigrations().has(20261012200606)) registerSchemaMigrations(linkedComponentModulesMigration);
147
148
  if (!registeredSchemaMigrations().has(20261013154037)) registerSchemaMigrations(componentStatesMigration);
149
+ if (!registeredSchemaMigrations().has(20261013164300)) registerSchemaMigrations(tokenOverridesListMigration);
148
150
  //#endregion
149
151
  export { System };
@@ -12,10 +12,19 @@ declare const nativeTokenValue: z.ZodType<number | boolean | string | Ref$1 | De
12
12
  declare const NativeToken: EntityClass<z.ZodObject<Omit<{
13
13
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
14
14
  type: z.ZodOptional<z.ZodString>;
15
- overrides: z.ZodOptional<SubEntityClass<z.ZodObject<{
15
+ overrides: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodObject<{
16
16
  on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
17
17
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
18
- }, z.core.$strip>>>;
18
+ }, z.core.$strip>>, z.ZodTransform<Record<string, {
19
+ on: OverrideCondition;
20
+ value: string | number | Ref | ComposedValue;
21
+ }>, {
22
+ on: OverrideCondition;
23
+ value: string | number | Ref | ComposedValue;
24
+ }[]>>, SubEntityClass<z.ZodObject<{
25
+ on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
26
+ value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
27
+ }, z.core.$strip>>]>>;
19
28
  utility: z.ZodOptional<z.ZodBoolean>;
20
29
  property: z.ZodOptional<z.ZodObject<{
21
30
  syntax: z.ZodOptional<z.ZodString>;
@@ -24,10 +33,19 @@ declare const NativeToken: EntityClass<z.ZodObject<Omit<{
24
33
  cssVariable: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
25
34
  }, "value" | "property" | "overrides" | "cssVariable"> & {
26
35
  value: z.ZodType<string | number | boolean | Ref | DerivedColor, unknown, z.core.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
27
- overrides: z.ZodOptional<SubEntityClass<z.ZodObject<{
36
+ overrides: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodObject<{
37
+ on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
38
+ value: z.ZodType<string | number | boolean | Ref | DerivedColor, unknown, z.core.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
39
+ }, z.core.$strip>>, z.ZodTransform<Record<string, {
40
+ on: OverrideCondition;
41
+ value: string | number | boolean | Ref | DerivedColor;
42
+ }>, {
43
+ on: OverrideCondition;
44
+ value: string | number | boolean | Ref | DerivedColor;
45
+ }[]>>, SubEntityClass<z.ZodObject<{
28
46
  on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
29
47
  value: z.ZodType<string | number | boolean | Ref | DerivedColor, unknown, z.core.$ZodTypeInternals<string | number | boolean | Ref | DerivedColor, unknown>>;
30
- }, z.core.$strip>>>;
48
+ }, z.core.$strip>>]>>;
31
49
  }, z.core.$strip>, z.ZodObject<Omit<{
32
50
  cssPrefix: z.ZodOptional<z.ZodString>;
33
51
  cssVariables: z.ZodOptional<z.ZodBoolean>;
@@ -1,5 +1,6 @@
1
1
  import { refSchema } from "../../framework/utils/refs.js";
2
2
  import { oklchColor } from "../system/color.js";
3
+ import { orList } from "../../framework/defineSubEntity.js";
3
4
  import { Font } from "../system/Font.js";
4
5
  import { NATIVE_CONFIG_NAME } from "../../configs/platform.js";
5
6
  import { prepareLinkedTokenBody } from "../system/linked-token-values.js";
@@ -66,7 +67,7 @@ const NativeToken = Token.extend({
66
67
  entityName: "NativeToken",
67
68
  fields: {
68
69
  value: nativeTokenValue,
69
- overrides: NativeOverride.optional(),
70
+ overrides: orList(NativeOverride).optional(),
70
71
  cssVariable: null,
71
72
  property: null
72
73
  },
@@ -753,8 +753,9 @@ displayName: ({ member, path }) => typeof member.name === "string" && member.nam
753
753
  bulkOverlayWrite: ({ accessor, path, overlay }) => ({ [`options.${accessor}.${path}`]: overlay })
754
754
  });
755
755
  /** An `override` or `extend` body with its `styles` as a list, in the stored form: the record keyed by
756
- * each rule's condition, as `Component.item` stores one. Only a component's `styles` is a rule list —
757
- * every other kind's is a record — so a list here is one whatever accessor it sits under. */
756
+ * each rule's condition, as `Component.item` stores one. Only a component has `styles`, so a list
757
+ * there is a rule list whatever accessor it sits under. A token's `overrides` list is stored as
758
+ * written and keyed where the overlay is read. */
758
759
  function storedPinBody(body) {
759
760
  if (!isPlainObject(body) || !Array.isArray(body.styles)) return body;
760
761
  return {
@@ -44,10 +44,19 @@ declare function defineOverride<TValue extends z.ZodTypeAny>(value: TValue): Sub
44
44
  declare const tokenFields: z.ZodObject<{
45
45
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
46
46
  type: z.ZodOptional<z.ZodString>;
47
- overrides: z.ZodOptional<SubEntityClass<z.ZodObject<{
47
+ overrides: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodObject<{
48
48
  on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
49
49
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
50
- }, z.core.$strip>>>;
50
+ }, z.core.$strip>>, z.ZodTransform<Record<string, {
51
+ on: OverrideCondition;
52
+ value: string | number | Ref | ComposedValue;
53
+ }>, {
54
+ on: OverrideCondition;
55
+ value: string | number | Ref | ComposedValue;
56
+ }[]>>, SubEntityClass<z.ZodObject<{
57
+ on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
58
+ value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
59
+ }, z.core.$strip>>]>>;
51
60
  utility: z.ZodOptional<z.ZodBoolean>;
52
61
  property: z.ZodOptional<z.ZodObject<{
53
62
  syntax: z.ZodOptional<z.ZodString>;
@@ -61,10 +70,19 @@ type TokenBody = z.infer<typeof tokenFields>;
61
70
  declare const TokenEntity: EntityClass<z.ZodObject<{
62
71
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
63
72
  type: z.ZodOptional<z.ZodString>;
64
- overrides: z.ZodOptional<SubEntityClass<z.ZodObject<{
73
+ overrides: z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<z.ZodObject<{
74
+ on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
75
+ value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
76
+ }, z.core.$strip>>, z.ZodTransform<Record<string, {
77
+ on: OverrideCondition;
78
+ value: string | number | Ref | ComposedValue;
79
+ }>, {
80
+ on: OverrideCondition;
81
+ value: string | number | Ref | ComposedValue;
82
+ }[]>>, SubEntityClass<z.ZodObject<{
65
83
  on: z.ZodType<OverrideCondition, unknown, z.core.$ZodTypeInternals<OverrideCondition, unknown>>;
66
84
  value: z.ZodType<string | number | Ref | ComposedValue, unknown, z.core.$ZodTypeInternals<string | number | Ref | ComposedValue, unknown>>;
67
- }, z.core.$strip>>>;
85
+ }, z.core.$strip>>]>>;
68
86
  utility: z.ZodOptional<z.ZodBoolean>;
69
87
  property: z.ZodOptional<z.ZodObject<{
70
88
  syntax: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import { isRef, kindOf, pathOf, ref, refKey, sourceOf } from "../../framework/utils/refs.js";
2
2
  import { composedValueSchemas } from "./composed-values.js";
3
3
  import { defineEntity } from "../../framework/defineEntity.js";
4
- import { defineSubEntity } from "../../framework/defineSubEntity.js";
4
+ import { defineSubEntity, orList } from "../../framework/defineSubEntity.js";
5
5
  import { Font } from "./Font.js";
6
6
  import { Modifier } from "./Modifier.js";
7
7
  import { spellTokenRead } from "./token-values.js";
@@ -112,7 +112,8 @@ function defineOverride(value) {
112
112
  on: config ? declaredCondition(config) : overrideConditionSchema,
113
113
  value
114
114
  }),
115
- key: (override) => overrideKey(override.on)
115
+ key: (override) => overrideKey(override.on),
116
+ listed: true
116
117
  });
117
118
  }
118
119
  const Override = defineOverride(tokenValue);
@@ -128,7 +129,7 @@ const TokenEntity = defineEntity({
128
129
  fields: z.object({
129
130
  value: tokenValue.describe("The base value: a literal, a ref to another token, or a composed value such as a gradient."),
130
131
  type: z.string().optional().describe("The CSS value type, when the value alone does not say it."),
131
- overrides: Override.optional().describe("Per-mode values, keyed by the condition they apply under."),
132
+ overrides: orList(Override).optional().describe("Per-mode values, a list of `{ on, value }`. Stored keyed by the condition each applies under."),
132
133
  utility: z.boolean().optional().describe("`false` keeps the token out of its group's values, so a style prop that takes the group writes no class for it — `spacing/scale` powers `--spacing` without becoming a `p-scale`. A ref naming the token still resolves."),
133
134
  property: z.strictObject({
134
135
  syntax: z.string().optional().describe("The `@property` syntax, as `<length>`. Defaults to `*`."),
@@ -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 = 20261013154037;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261013164300;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -2179,9 +2179,8 @@ declare class Config {
2179
2179
  * only the reached ones would leave those rows absent from the artifact. Reachability stays the
2180
2180
  * floor either way, so a ref always brings its target whether or not a declaration claims it.
2181
2181
  *
2182
- * Shared rather than computed per caller: the CSS emitter needs it to know what to declare, and the
2183
- * build's validation needs the same set to know what to check — two answers to that question would
2184
- * drift, and the drift would look like a token that emits but isn't validated.
2182
+ * Preserves the original source definitions for saved configs and Web emission. Native emission
2183
+ * and condition validation use runtimeSourceClosure to follow the definitions after overrides.
2185
2184
  *
2186
2185
  * `kind` narrows it for a caller that emits or validates one kind, which is the caller's business
2187
2186
  * to name; the walk itself follows refs wherever they go, so borrowing something other than a token
@@ -2190,6 +2189,14 @@ declare class Config {
2190
2189
  * Sorted, so anything built from it is deterministic.
2191
2190
  */
2192
2191
  sourceClosure(slug: string, kind?: string): string[];
2192
+ /** References used after local overrides; sourceClosure also preserves the original bases. */
2193
+ runtimeSourceClosure({
2194
+ slug,
2195
+ kind
2196
+ }: {
2197
+ slug: string;
2198
+ kind?: string;
2199
+ }): string[];
2193
2200
  /**
2194
2201
  * The walk behind {@link sourceClosure}, run once per snapshot and source set.
2195
2202
  *
@@ -272,7 +272,8 @@ function cachesFor(snapshot, sources) {
272
272
  membersIndexed: /* @__PURE__ */ new Set(),
273
273
  leaves: /* @__PURE__ */ new Map(),
274
274
  computed: /* @__PURE__ */ new Map(),
275
- closures: /* @__PURE__ */ new Map()
275
+ closures: /* @__PURE__ */ new Map(),
276
+ runtimeClosures: /* @__PURE__ */ new Map()
276
277
  };
277
278
  buckets.push(bucket);
278
279
  return bucket;
@@ -318,6 +319,37 @@ function leafOf(field) {
318
319
  function wholeBodySchema(entry, ctx = {}) {
319
320
  return dataSchema(entry.schemas, "create", ctx) ?? dataSchema(entry.schemas, "replace", ctx);
320
321
  }
322
+ /**
323
+ * An overlay whose bodies hold a list for a collection keyed by its members (`defineSubEntity({ key })`),
324
+ * with each list written as the record that collection is stored as. A pin stores its bodies as
325
+ * authored, and the overlay is laid onto the source key by key, so a list left as written would
326
+ * replace the source's record rather than add to it. A later member wins over an earlier one with
327
+ * the same key.
328
+ */
329
+ function keyedOverlay(entry, overlay) {
330
+ if (!entry) return overlay;
331
+ const keyed = (body) => {
332
+ if (!body) return body;
333
+ let out;
334
+ for (const [field, sub] of Object.entries(entry.subEntities)) {
335
+ const members = body[field];
336
+ if (!sub.key || !Array.isArray(members)) continue;
337
+ const record = {};
338
+ for (const member of members) record[sub.key(member)] = member;
339
+ out ??= { ...body };
340
+ out[field] = record;
341
+ }
342
+ return out ?? body;
343
+ };
344
+ const override = keyed(overlay.override);
345
+ const extend = keyed(overlay.extend);
346
+ if (override === overlay.override && extend === overlay.extend) return overlay;
347
+ return {
348
+ ...overlay,
349
+ override,
350
+ extend
351
+ };
352
+ }
321
353
  /** The sub-entity a field path names, at either depth. */
322
354
  function subEntityAt(entry, field) {
323
355
  const slash = field.indexOf("/");
@@ -3764,7 +3796,7 @@ var Config = class Config {
3764
3796
  const overlay = entry.bulkOverlay?.(args);
3765
3797
  if (overlay !== void 0) return {
3766
3798
  claimed,
3767
- overlay
3799
+ overlay: keyedOverlay(this.entityOf(kind), overlay)
3768
3800
  };
3769
3801
  if (claimed) return { claimed: true };
3770
3802
  }
@@ -4352,9 +4384,8 @@ var Config = class Config {
4352
4384
  * only the reached ones would leave those rows absent from the artifact. Reachability stays the
4353
4385
  * floor either way, so a ref always brings its target whether or not a declaration claims it.
4354
4386
  *
4355
- * Shared rather than computed per caller: the CSS emitter needs it to know what to declare, and the
4356
- * build's validation needs the same set to know what to check — two answers to that question would
4357
- * drift, and the drift would look like a token that emits but isn't validated.
4387
+ * Preserves the original source definitions for saved configs and Web emission. Native emission
4388
+ * and condition validation use runtimeSourceClosure to follow the definitions after overrides.
4358
4389
  *
4359
4390
  * `kind` narrows it for a caller that emits or validates one kind, which is the caller's business
4360
4391
  * to name; the walk itself follows refs wherever they go, so borrowing something other than a token
@@ -4366,10 +4397,25 @@ var Config = class Config {
4366
4397
  const cache = this.caches.closures;
4367
4398
  const hit = cache.get(slug);
4368
4399
  if (hit) return kind === void 0 ? hit : hit.filter((target) => kindOf(target) === kind);
4369
- const targets = this.computeSourceClosure(slug);
4400
+ const targets = this.computeSourceClosure({
4401
+ slug,
4402
+ includeExtensionBases: true
4403
+ });
4370
4404
  cache.set(slug, targets);
4371
4405
  return kind === void 0 ? targets : targets.filter((target) => kindOf(target) === kind);
4372
4406
  }
4407
+ /** References used after local overrides; sourceClosure also preserves the original bases. */
4408
+ runtimeSourceClosure({ slug, kind }) {
4409
+ let targets = this.caches.runtimeClosures.get(slug);
4410
+ if (!targets) {
4411
+ targets = this.computeSourceClosure({
4412
+ slug,
4413
+ includeExtensionBases: false
4414
+ });
4415
+ this.caches.runtimeClosures.set(slug, targets);
4416
+ }
4417
+ return kind === void 0 ? targets : targets.filter((target) => kindOf(target) === kind);
4418
+ }
4373
4419
  /**
4374
4420
  * The walk behind {@link sourceClosure}, run once per snapshot and source set.
4375
4421
  *
@@ -4378,7 +4424,7 @@ var Config = class Config {
4378
4424
  * over a config that is routinely megabytes, for one answer that cannot change while the snapshot
4379
4425
  * and its sources are fixed.
4380
4426
  */
4381
- computeSourceClosure(slug) {
4427
+ computeSourceClosure({ slug, includeExtensionBases }) {
4382
4428
  const source = this.sourceFor(slug);
4383
4429
  if (!source) return [];
4384
4430
  const queue = [];
@@ -4386,6 +4432,7 @@ var Config = class Config {
4386
4432
  for (const item of this.state.snapshot.slice(owned).items.values()) {
4387
4433
  const stored = item.toJSON();
4388
4434
  if (!isExtendedRef(stored)) continue;
4435
+ if (!includeExtensionBases && this.resolve(owned, item.path)) continue;
4389
4436
  for (const target of collectRefs(stored)) {
4390
4437
  if (sourceOf(target) !== slug) continue;
4391
4438
  queue.push(`${kindOf(target)}:${pathOf(target)}`);
@@ -4396,7 +4443,11 @@ var Config = class Config {
4396
4443
  queue.push(`${kindOf(target)}:${pathOf(target)}`);
4397
4444
  }
4398
4445
  }
4399
- for (const target of this.adoptedFrom(slug)) queue.push(target);
4446
+ for (const target of this.adoptedFrom(slug)) {
4447
+ const local = this.storedBody(kindOf(target), pathOf(target));
4448
+ if (!includeExtensionBases && isExtendedRef(local) && !isMintedRow(local) && this.resolve(kindOf(target), pathOf(target))) continue;
4449
+ queue.push(target);
4450
+ }
4400
4451
  const reached = /* @__PURE__ */ new Set();
4401
4452
  while (queue.length > 0) {
4402
4453
  const target = queue.pop();
@@ -4979,6 +5030,8 @@ var Config = class Config {
4979
5030
  for (const { bag } of sites(body)) for (const prop of Object.keys(bag)) {
4980
5031
  const target = `style-property:${prop}`;
4981
5032
  if (members.has(target)) continue;
5033
+ const local = this.storedBody("style-property", prop);
5034
+ if (local && !isMintedRow(local)) continue;
4982
5035
  const declared = source.resolve("style-property", prop);
4983
5036
  if (!declared) continue;
4984
5037
  members.add(target);
@@ -5027,15 +5080,37 @@ var Config = class Config {
5027
5080
  }
5028
5081
  }
5029
5082
  };
5083
+ const visitExtension = ({ kind, extension, linked = false }) => {
5084
+ const base = source.resolve(kind, pathOf(extension));
5085
+ if (!base) return;
5086
+ const body = this.laidLinkedBody({
5087
+ kind,
5088
+ path: pathOf(extension),
5089
+ body: qualifyRefs({
5090
+ value: base.toJSON(),
5091
+ source: slug,
5092
+ owns: (name) => source.ownedKinds().includes(name)
5093
+ }),
5094
+ extend: extension.extend
5095
+ });
5096
+ visitEntity(kind, body, false, linked);
5097
+ visitBags(kind, body, linked);
5098
+ };
5030
5099
  for (const kind of this.ownedKinds()) {
5031
5100
  const slice = this.state.snapshot.slice(kind);
5032
5101
  for (const [, stored] of slice.items) {
5033
5102
  const body = stored.toJSON();
5034
5103
  if (isMintedRow(body)) continue;
5104
+ if (isExtendedRef(body) && sourceOf(body) === slug) {
5105
+ visitExtension({
5106
+ kind,
5107
+ extension: body
5108
+ });
5109
+ continue;
5110
+ }
5035
5111
  if (isRef(body) && sourceOf(body) === slug) {
5036
5112
  const theirs = source.resolve(kindOf(body), pathOf(body))?.toJSON();
5037
5113
  if (theirs) visitEntity(kind, theirs, true);
5038
- if (isExtendedRef(body)) visit(body.extend);
5039
5114
  continue;
5040
5115
  }
5041
5116
  visitEntity(kind, body);
@@ -5047,6 +5122,15 @@ var Config = class Config {
5047
5122
  }
5048
5123
  }
5049
5124
  for (const target of this.adoptedFrom(slug)) {
5125
+ const local = this.storedBody(kindOf(target), pathOf(target));
5126
+ if (isExtendedRef(local) && !isMintedRow(local) && sourceOf(local) === slug) {
5127
+ visitExtension({
5128
+ kind: kindOf(target),
5129
+ extension: local,
5130
+ linked: true
5131
+ });
5132
+ continue;
5133
+ }
5050
5134
  const body = source.resolve(kindOf(target), pathOf(target))?.toJSON();
5051
5135
  if (body) visitEntity(kindOf(target), body, true, true);
5052
5136
  }
@@ -5749,7 +5833,7 @@ var Config = class Config {
5749
5833
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5750
5834
  hydrateFrom(stored, options) {
5751
5835
  const claimed = schemaVersionOf(stored);
5752
- if (claimed > 20261013154037) throw new SchemaVersionTooNew(claimed);
5836
+ if (claimed > 20261013164300) throw new SchemaVersionTooNew(claimed);
5753
5837
  const wire = upgradeSerializedConfig(stored);
5754
5838
  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\`.`);
5755
5839
  const leftover = wire.options;
@@ -16,7 +16,7 @@ interface KindSignature {
16
16
  readonly collections: Record<string, string>;
17
17
  /** The group node's own fields, when the kind carries any. */
18
18
  readonly group?: string;
19
- /** A real entry from this config, in stored form. */
19
+ /** A real entry from this config, as it is authored. */
20
20
  readonly example?: unknown;
21
21
  }
22
22
  interface AuthoringSignatureOptions {
@@ -75,6 +75,19 @@ function authoringSignature(schema, options) {
75
75
  const json = jsonSchema(schema);
76
76
  return json === void 0 ? void 0 : renderSignature(withoutMetadata(json), options);
77
77
  }
78
+ /**
79
+ * A real entry, spelled the way it is authored. A collection whose schema offers only a list (see
80
+ * `withDeprecated`) is stored as the record keyed by its members, so the example writes its members
81
+ * as that list, and a reader copying it never learns the deprecated record.
82
+ */
83
+ function authoredExample({ item, fields }) {
84
+ const body = item.toJSON();
85
+ const properties = fields?.properties;
86
+ if (!properties) return { [item.path]: body };
87
+ const authored = { ...body };
88
+ for (const [field, value] of Object.entries(body)) if (properties[field]?.type === "array" && value && typeof value === "object" && !Array.isArray(value)) authored[field] = Object.values(value);
89
+ return { [item.path]: authored };
90
+ }
78
91
  /** Every owned kind's authoring shape as signatures, with a real example where the config has one. */
79
92
  function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
80
93
  return config.ownedKinds().flatMap((kind) => {
@@ -87,14 +100,18 @@ function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
87
100
  if (signature) collections[field] = singleton ? `one section, no key (\`sub-update/${field}\`): ${signature}` : signature;
88
101
  }
89
102
  const first = config.list(kind)[0];
103
+ const create = dataSchemaOf(config, entity.schemas.create);
90
104
  return [{
91
105
  kind,
92
106
  label: entity.label,
93
107
  refHelper: entity.entityName,
94
- create: authoringSignature(dataSchemaOf(config, entity.schemas.create), { depth }),
108
+ create: authoringSignature(create, { depth }),
95
109
  collections,
96
110
  group: authoringSignature(entity.groupCreateSchema, { depth }),
97
- example: first ? { [first.path]: first.toJSON() } : void 0
111
+ example: first ? authoredExample({
112
+ item: first,
113
+ fields: jsonSchema(create)
114
+ }) : void 0
98
115
  }];
99
116
  });
100
117
  }
@@ -137,14 +154,18 @@ function authoringReference(config) {
137
154
  collections[field] = jsonSchema(dataSchemaOf(config, entity.schemas[`sub-create/${field}`]));
138
155
  }
139
156
  const first = config.list(kind)[0];
157
+ const fields = jsonSchema(dataSchemaOf(config, entity.schemas.create));
140
158
  return [{
141
159
  kind,
142
160
  label: entity.label,
143
161
  refHelper: entity.entityName,
144
- fields: jsonSchema(dataSchemaOf(config, entity.schemas.create)),
162
+ fields,
145
163
  collections,
146
164
  groupFields: jsonSchema(entity.groupCreateSchema),
147
- example: first ? { [first.path]: first.toJSON() } : void 0
165
+ example: first ? authoredExample({
166
+ item: first,
167
+ fields
168
+ }) : void 0
148
169
  }];
149
170
  }) };
150
171
  }
@@ -36,6 +36,9 @@ type SubEntityClass<F extends z.ZodTypeAny = z.ZodTypeAny> = CollectionSchema<F>
36
36
  * caller, so key and value can't drift, two members with the same identity can't both exist, and
37
37
  * an edit that changes the identity moves the entry. Omit when the key is arbitrary. */
38
38
  readonly key?: (member: unknown) => string;
39
+ /** The collection is authored as a list of its members, which carry their own `key`. The keyed
40
+ * record it is stored as is still read but no longer offered: see {@link orList}. */
41
+ readonly listed?: true;
39
42
  /** Whether a member DECLARES nothing — a style rule whose every layer bag is empty. Asked only
40
43
  * when a \`sub-update\` would bring the member into being: a delta made entirely of removals has
41
44
  * nothing to remove from a member that is not there, so creating one files an entry that emits
@@ -143,6 +146,7 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
143
146
  */
144
147
  base?: z.ZodTypeAny;
145
148
  key?: (member: unknown) => string;
149
+ listed?: true;
146
150
  vacant?: (member: unknown) => boolean;
147
151
  schemas?: UserSchemas;
148
152
  namespaces?: string;
@@ -161,6 +165,9 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
161
165
  * Two members with the same key are one member, the later one, as the key derivation on write
162
166
  * already treats a record whose keys disagree with their members — unless `merge` says how the two
163
167
  * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
168
+ *
169
+ * A collection declared `listed` still takes the keyed record but offers only the list, see
170
+ * {@link withDeprecated}.
164
171
  */
165
172
  declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>, {
166
173
  merge
@@ -1,6 +1,7 @@
1
1
  import { kindOf, memberOf, memberRef, pathOf } from "./utils/refs.js";
2
2
  import { resolveSchema } from "./schemas.js";
3
3
  import { titleCase } from "./registry.js";
4
+ import { withDeprecated } from "./utils/deprecated.js";
4
5
  import { z } from "zod";
5
6
  //#region src/framework/defineSubEntity.ts
6
7
  /**
@@ -78,6 +79,7 @@ function defineSubEntity(spec) {
78
79
  fields: spec.fields,
79
80
  base: spec.base,
80
81
  key: spec.key,
82
+ ...spec.listed ? { listed: true } : {},
81
83
  vacant: spec.vacant,
82
84
  schemas: spec.schemas,
83
85
  namespaces: spec.namespaces,
@@ -126,6 +128,9 @@ function defineSubEntity(spec) {
126
128
  * Two members with the same key are one member, the later one, as the key derivation on write
127
129
  * already treats a record whose keys disagree with their members — unless `merge` says how the two
128
130
  * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
131
+ *
132
+ * A collection declared `listed` still takes the keyed record but offers only the list, see
133
+ * {@link withDeprecated}.
129
134
  */
130
135
  function orList(collection, { merge } = {}) {
131
136
  const key = collection.key;
@@ -142,6 +147,10 @@ function orList(collection, { merge } = {}) {
142
147
  }
143
148
  return keyed;
144
149
  });
150
+ if (collection.listed) return withDeprecated({
151
+ offered: listed,
152
+ deprecated: collection
153
+ });
145
154
  return z.union([listed, collection]);
146
155
  }
147
156
  //#endregion
@@ -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 = 20261013154037;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261013164300;
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 = 20261013154037;
16
+ const CURRENT_SCHEMA_VERSION = 20261013164300;
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 > 20261013154037) 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 > 20261013164300) 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 ?? 20261013154037;
105
+ const target = options?.target ?? 20261013164300;
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 ?? 20261013154037;
163
+ const target = options?.target ?? 20261013164300;
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 ?? 20261013154037;
182
+ const target = options?.target ?? 20261013164300;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -430,7 +430,10 @@ function unstatableConditions(config) {
430
430
  for (const slug of config.sourceSlugs()) {
431
431
  const source = config.sourceFor(slug);
432
432
  if (!source) continue;
433
- for (const target of config.sourceClosure(slug, "token")) {
433
+ for (const target of config.runtimeSourceClosure({
434
+ slug,
435
+ kind: "token"
436
+ })) {
434
437
  const token = pathOf(target);
435
438
  const { overrides } = source.resolve("token", token)?.toJSON() ?? {};
436
439
  if (!overrides) continue;