@yahoo/uds-create-config 5.27.1 → 5.29.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.
Files changed (33) hide show
  1. package/dist/configs/react-native-system.d.ts +1 -0
  2. package/dist/configs/system.js +4 -0
  3. package/dist/entities/native/NativeSettings.d.ts +1 -0
  4. package/dist/entities/system/Font.d.ts +20 -1
  5. package/dist/entities/system/Font.js +50 -4
  6. package/dist/entities/system/LinkedSystem.d.ts +12 -0
  7. package/dist/entities/system/LinkedSystem.js +114 -18
  8. package/dist/entities/system/Settings.d.ts +3 -0
  9. package/dist/entities/system/Settings.js +7 -0
  10. package/dist/entities/system/link-declaration.d.ts +43 -2
  11. package/dist/entities/system/link-declaration.js +110 -15
  12. package/dist/entities/system/link-plan.js +109 -4
  13. package/dist/framework/Config.d.ts +6 -1
  14. package/dist/framework/Config.js +28 -6
  15. package/dist/framework/defineEntity.d.ts +4 -3
  16. package/dist/framework/defineEntity.js +1 -0
  17. package/dist/framework/registry.d.ts +5 -0
  18. package/dist/framework/registry.js +2 -1
  19. package/dist/framework/rejection.d.ts +4 -2
  20. package/dist/framework/schema-version.d.ts +1 -1
  21. package/dist/framework/schema-version.js +5 -5
  22. package/dist/framework/source-integrity.d.ts +9 -2
  23. package/dist/framework/source-integrity.js +21 -2
  24. package/dist/framework/utils/refs.d.ts +14 -2
  25. package/dist/framework/utils/refs.js +22 -3
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +4 -4
  28. package/dist/migrations/20261013182150_link_exclude_op.d.ts +13 -0
  29. package/dist/migrations/20261013182150_link_exclude_op.js +10 -0
  30. package/dist/migrations/20261013201548_font_faces.d.ts +9 -0
  31. package/dist/migrations/20261013201548_font_faces.js +10 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
@@ -1,7 +1,8 @@
1
1
  import { rejectData } from "../../framework/rejection.js";
2
2
  import { collectRefs, isMintedRow, ref, sourceOf, splitRef } from "../../framework/utils/refs.js";
3
3
  import { blockedBy } from "../../framework/plan.js";
4
- import { PIN_KIND, covers, declarationOf, narrowDeclaration } from "./link-declaration.js";
4
+ import { excludedCondition } from "../../framework/source-integrity.js";
5
+ import { PIN_KIND, covers, declarationOf, entriesFor, excludePaths, excludes, includePaths, narrowDeclaration } from "./link-declaration.js";
5
6
  import { planCopy, sourceEntries } from "./copy-plan.js";
6
7
  //#region src/entities/system/link-plan.ts
7
8
  /** The kinds `data` names, with what to take of each. An accessor this config has no kind for is a typo, refused by name. */
@@ -201,7 +202,7 @@ function planLink({ config, input }) {
201
202
  } else {
202
203
  const next = { ...current ?? {} };
203
204
  for (const [accessor, gain] of gains) {
204
- const before = next[accessor];
205
+ const before = entriesFor(current, accessor);
205
206
  if (before === "*") continue;
206
207
  if (gain === "*") {
207
208
  next[accessor] = "*";
@@ -213,7 +214,13 @@ function planLink({ config, input }) {
213
214
  path
214
215
  }));
215
216
  if (added.length === 0) continue;
216
- next[accessor] = [...before ?? [], ...new Set(added)];
217
+ next[accessor] = includePaths({
218
+ config,
219
+ slug,
220
+ kind: config.accessors()[accessor]?.kind ?? accessor,
221
+ entries: before ?? [],
222
+ paths: [...new Set(added)]
223
+ });
217
224
  }
218
225
  for (const [accessor, value] of Object.entries(next)) {
219
226
  if (current !== void 0 && current[accessor] === value) continue;
@@ -361,6 +368,104 @@ function planUnlink({ config, input }) {
361
368
  };
362
369
  }
363
370
  /**
371
+ * Leave paths of a linked system out, with a `!` entry each in the pin's `link`. Stronger than
372
+ * `unlink`: nothing derives there however the link reaches it, a linked token naming it included,
373
+ * and the linked tokens' overrides under an excluded modifier are left out of the build. `link` with
374
+ * the same path lifts it again.
375
+ */
376
+ function planExclude({ config, input }) {
377
+ const { path: slug } = input;
378
+ const source = config.sourceFor(slug);
379
+ if (!source) rejectData(`Cannot exclude through "${slug}" — nothing has resolved that link.`);
380
+ const current = declarationOf(config, slug);
381
+ const next = current === void 0 ? {} : current === "*" ? { "*": "*" } : { ...current };
382
+ for (const { kind, accessor, paths } of wantedKinds({
383
+ config,
384
+ slug,
385
+ data: input.data,
386
+ verb: "exclude"
387
+ })) {
388
+ if (paths === "*") rejectData(`Cannot exclude every "${accessor}" through "${slug}" — name the paths to leave out, or \`unlink\` the kind.`, ["data", accessor]);
389
+ for (const path of paths) if (sourceEntries({
390
+ source,
391
+ kind,
392
+ path
393
+ }).length === 0) rejectData(`Cannot exclude "${path}" through "${slug}" — the source has no ${kind} there.`, ["data", accessor]);
394
+ next[accessor] = excludePaths(entriesFor(current, accessor), paths);
395
+ }
396
+ const declared = next;
397
+ const dropped = [];
398
+ const optionDeletes = {};
399
+ const options = config.storedBody("linked-system", slug).options ?? {};
400
+ for (const [accessor, entries] of Object.entries(options)) {
401
+ if (!entries || typeof entries !== "object") continue;
402
+ const kind = config.accessors()[accessor]?.kind ?? accessor;
403
+ for (const path of Object.keys(entries)) {
404
+ if (!excludes({
405
+ declared,
406
+ accessor,
407
+ path
408
+ })) continue;
409
+ dropped.push({
410
+ kind,
411
+ path
412
+ });
413
+ optionDeletes[`options.${accessor}.${path}`] = null;
414
+ }
415
+ }
416
+ const patches = [{
417
+ kind: PIN_KIND,
418
+ operation: "update",
419
+ path: slug,
420
+ data: {
421
+ link: declared,
422
+ ...optionDeletes
423
+ }
424
+ }];
425
+ if (Object.entries(input.data).every(([accessor, paths]) => paths.every((path) => excludes({
426
+ declared: current,
427
+ accessor,
428
+ path
429
+ })))) return {
430
+ declared: current,
431
+ removed: [],
432
+ leftOut: [],
433
+ dropped: [],
434
+ blocked: `Nothing would change: what "${slug}" declares already leaves everything named out.`,
435
+ patches: []
436
+ };
437
+ const blocked = blockedBy(() => config.applyAll(patches));
438
+ const removed = [];
439
+ const leftOut = [];
440
+ if (blocked === void 0) {
441
+ const after = config.applyAll(patches);
442
+ for (const kind of config.borrowableKinds(slug)) for (const item of config.list(kind)) {
443
+ if (item.borrowedFrom !== slug) continue;
444
+ if (after.resolve(kind, item.path)?.borrowedFrom === slug) continue;
445
+ removed.push({
446
+ kind,
447
+ path: item.path
448
+ });
449
+ }
450
+ for (const item of after.list("token")) {
451
+ if (item.borrowedFrom !== slug) continue;
452
+ const body = source.resolve("token", item.path)?.toJSON();
453
+ for (const [override, { on }] of Object.entries(body?.overrides ?? {})) if (excludedCondition(after, on)) leftOut.push({
454
+ token: item.path,
455
+ override
456
+ });
457
+ }
458
+ }
459
+ return {
460
+ declared,
461
+ removed,
462
+ leftOut,
463
+ dropped,
464
+ ...blocked === void 0 ? {} : { blocked },
465
+ patches
466
+ };
467
+ }
468
+ /**
364
469
  * The linked rows that point at what `data` names, followed to a fixpoint: a row pointing at one of
365
470
  * those, and so on. Read off both ref graphs, since a linked row's refs are the source's spelling and
366
471
  * this config's index holds them qualified.
@@ -832,4 +937,4 @@ function sourceRowStates({ config, slug }) {
832
937
  return rows;
833
938
  }
834
939
  //#endregion
835
- export { planBreak, planLink, planUnlink, sourceRowStates };
940
+ export { planBreak, planExclude, planLink, planUnlink, sourceRowStates };
@@ -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 = 20261013164300;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261013201548;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -1785,6 +1785,11 @@ declare class Config {
1785
1785
  * subject is the overlay rather than whether a row is minted at all.
1786
1786
  */
1787
1787
  sourceOverlay(slug: string, kind: string, path: string): SourceOverlay | undefined;
1788
+ /**
1789
+ * Whether the declared source at `slug` excludes one of its paths ({@link EntityKind.bulkExcludes}):
1790
+ * left out on purpose, so no row derives there. Matched on the path the source spells.
1791
+ */
1792
+ sourceExcludes(slug: string, kind: string, path: string): boolean;
1788
1793
  /**
1789
1794
  * The field-path delta that records `overlay` as the pin's directive for one path, or `null` when
1790
1795
  * there is nothing to write.
@@ -1,6 +1,6 @@
1
1
  import "./config-operation-kind.js";
2
2
  import { ConfigFormatError, ConfigRejection, isConfigRejection, issuePath, refusalIssues, reject, rejectData, rejectInvalid, rejectReferrers } from "./rejection.js";
3
- import { ENTITY_PATH_MESSAGE, MINTED, collectRefs, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, pathOf, qualifyRefs, ref, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, singletonRef, sourceHops, sourceOf, splitRef } from "./utils/refs.js";
3
+ import { MINTED, collectRefs, entityPathProblem, entityRefOf, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, pathOf, qualifyRefs, ref, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, singletonRef, sourceHops, sourceOf, splitRef } from "./utils/refs.js";
4
4
  import { describePatch } from "./plan.js";
5
5
  import { applyPathDelta, isPlainObject, namedFields, pathSegments, sectionDelta, touchedFields } from "./utils/field-path.js";
6
6
  import { METADATA_KEYS, declaredField } from "./utils/field-schema.js";
@@ -2023,7 +2023,8 @@ var Config = class Config {
2023
2023
  if (!entry.schemas[verb]) reject("UNSUPPORTED", `${patch.kind} has no ${verb} operation`);
2024
2024
  if (entry.singleton && "path" in patch && patch.path !== void 0 && patch.path !== entry.name) reject("INVALID_PATH", `${patch.kind} has one item, at "${entry.name}" — leave \`path\` out. A field is written by its path in \`data\` (\`{ "${patch.path}": … }\`) or through sub-update/<section>.`);
2025
2025
  if (patch.operation === "create") {
2026
- if (!isEntityPath(patch.path)) reject("INVALID_PATH", `Cannot create ${patch.kind} "${patch.path}" — ${ENTITY_PATH_MESSAGE}`);
2026
+ const problem = entityPathProblem(patch.path);
2027
+ if (problem !== void 0) reject("INVALID_PATH", `Cannot create ${patch.kind} "${patch.path}" — ${problem}`);
2027
2028
  const occupant = snapshot.get(patch.kind, patch.path);
2028
2029
  if (occupant && !isMintedRow(occupant.toJSON())) reject("ALREADY_EXISTS", `${patch.kind} "${patch.path}" already exists`);
2029
2030
  return snapshot.withChanges([this.wholeBodyChange({
@@ -2113,7 +2114,8 @@ var Config = class Config {
2113
2114
  }]);
2114
2115
  }
2115
2116
  if (patch.operation === "rename") {
2116
- if (!isEntityPath(patch.to)) reject("INVALID_PATH", `Cannot rename ${patch.kind} "${patch.path}" to "${patch.to}" — ${ENTITY_PATH_MESSAGE}`);
2117
+ const problem = entityPathProblem(patch.to);
2118
+ if (problem !== void 0) reject("INVALID_PATH", `Cannot rename ${patch.kind} "${patch.path}" to "${patch.to}" — ${problem}`);
2117
2119
  const occupant = patch.to === patch.path ? void 0 : snapshot.get(patch.kind, patch.to);
2118
2120
  if (occupant && !isMintedRow(occupant.toJSON())) reject("ALREADY_EXISTS", `Cannot rename ${patch.kind} "${patch.path}" to "${patch.to}" — a ${patch.kind} already exists there.`);
2119
2121
  const storedBody = stored;
@@ -3612,6 +3614,23 @@ var Config = class Config {
3612
3614
  return this.sourceDirective(slug, kind, path).overlay;
3613
3615
  }
3614
3616
  /**
3617
+ * Whether the declared source at `slug` excludes one of its paths ({@link EntityKind.bulkExcludes}):
3618
+ * left out on purpose, so no row derives there. Matched on the path the source spells.
3619
+ */
3620
+ sourceExcludes(slug, kind, path) {
3621
+ const accessor = this.accessorOf(kind);
3622
+ return this.sourceKinds().some((sourceKind) => {
3623
+ const excludes = this.entityOf(sourceKind)?.bulkExcludes;
3624
+ const pin = this.storedBody(sourceKind, slug);
3625
+ return excludes !== void 0 && pin !== void 0 && excludes({
3626
+ body: pin,
3627
+ kind,
3628
+ accessor,
3629
+ path
3630
+ });
3631
+ });
3632
+ }
3633
+ /**
3615
3634
  * The field-path delta that records `overlay` as the pin's directive for one path, or `null` when
3616
3635
  * there is nothing to write.
3617
3636
  *
@@ -4791,6 +4810,7 @@ var Config = class Config {
4791
4810
  config: this,
4792
4811
  source
4793
4812
  })) continue;
4813
+ if (this.sourceExcludes(slug, kind, path)) continue;
4794
4814
  const current = this.state.snapshot.get(kind, path);
4795
4815
  if (current && !isMintedRow(current.toJSON())) continue;
4796
4816
  if (placed.has(target)) continue;
@@ -5833,7 +5853,7 @@ var Config = class Config {
5833
5853
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5834
5854
  hydrateFrom(stored, options) {
5835
5855
  const claimed = schemaVersionOf(stored);
5836
- if (claimed > 20261013164300) throw new SchemaVersionTooNew(claimed);
5856
+ if (claimed > 20261013201548) throw new SchemaVersionTooNew(claimed);
5837
5857
  const wire = upgradeSerializedConfig(stored);
5838
5858
  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\`.`);
5839
5859
  const leftover = wire.options;
@@ -5952,7 +5972,8 @@ function isPathSegment(meta, kind, raw) {
5952
5972
  function loadItemChange(meta, kind, path, raw, phase = "author") {
5953
5973
  const entry = meta.entities.get(kind);
5954
5974
  if (!entry) throw new Error(`Unknown entity kind: ${kind}`);
5955
- if (phase === "author" && !isEntityPath(path)) reject("INVALID_PATH", `${kind} "${path}" — ${ENTITY_PATH_MESSAGE}`);
5975
+ const problem = phase === "author" ? entityPathProblem(path) : void 0;
5976
+ if (problem !== void 0) reject("INVALID_PATH", `${kind} "${path}" — ${problem}`);
5956
5977
  if (phase === "author") refusePhantomsIn({
5957
5978
  kind,
5958
5979
  path,
@@ -6200,7 +6221,8 @@ function itemChange(entry, kind, path, raw, validate = false) {
6200
6221
  function loadGroupChange(meta, kind, path, raw, phase = "author") {
6201
6222
  const entry = meta.entities.get(kind);
6202
6223
  if (!entry) throw new Error(`Unknown entity kind: ${kind}`);
6203
- if (phase === "author" && !isEntityPath(path)) reject("INVALID_PATH", `${kind} group "${path}" — ${ENTITY_PATH_MESSAGE}`);
6224
+ const problem = phase === "author" ? entityPathProblem(path) : void 0;
6225
+ if (problem !== void 0) reject("INVALID_PATH", `${kind} group "${path}" — ${problem}`);
6204
6226
  if (phase === "author") refusePhantomsIn({
6205
6227
  kind,
6206
6228
  path,
@@ -338,10 +338,11 @@ declare function defineEntity<F extends FieldsSchema, G extends FieldsSchema = z
338
338
  */
339
339
  bulkAdopts?: (args: BulkQuery) => boolean;
340
340
  /**
341
- * Whether the pin declines this path — the counterpart to {@link EntityKind.bulkAdopts}, and the
342
- * tombstone `link`'s own note asks for. A declined path derives no row, so the name is free for
343
- * this config to author, and the source's entity stays reachable qualified.
341
+ * Whether the pin leaves this path out on purpose — the counterpart to {@link EntityKind.bulkAdopts}.
342
+ * An excluded path derives no row however the link reaches it, so the name is free for this config
343
+ * to author, and the source's entity stays reachable qualified.
344
344
  */
345
+ bulkExcludes?: (args: BulkQuery) => boolean;
345
346
  /**
346
347
  * What the declared source lays over the entity at this path, when it declares anything — the
347
348
  * bodies a minted row carries in place of an empty `extend`, so a bulk adjustment and a hand-written
@@ -120,6 +120,7 @@ function defineEntity(spec) {
120
120
  bareMember: spec.bareMember,
121
121
  sourceResolution: spec.sourceResolution,
122
122
  bulkAdopts: spec.bulkAdopts,
123
+ bulkExcludes: spec.bulkExcludes,
123
124
  bulkOverlay: spec.bulkOverlay,
124
125
  bulkDirectives: spec.bulkDirectives,
125
126
  bulkOverlayWrite: spec.bulkOverlayWrite,
@@ -260,6 +260,11 @@ interface EntityKind {
260
260
  * nothing, and no delete is ever refused for it.
261
261
  */
262
262
  readonly bulkAdopts?: (args: BulkQuery) => boolean;
263
+ /**
264
+ * Whether the declaration leaves this path out on purpose. An excluded path derives no row, and an
265
+ * excluded modifier is one the source's tokens may vary under without this config stating it.
266
+ */
267
+ readonly bulkExcludes?: (args: BulkQuery) => boolean;
263
268
  /** What the declared source lays over the entity at this path, when it declares anything. */
264
269
  readonly bulkOverlay?: (args: BulkQuery) => SourceOverlay | undefined;
265
270
  /**
@@ -32,7 +32,7 @@ function titleCase(kind) {
32
32
  /** Derive a kind's immutable metadata from its field schemas — the value a `defineEntity`/
33
33
  * `defineDerivedEntity` attaches to its class and a `Config.register` stores in `#entities`. */
34
34
  function buildKind(spec) {
35
- const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants, structuralBody } = spec;
35
+ const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkExcludes, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants, structuralBody } = spec;
36
36
  const allComputed = {
37
37
  sources: ({ member, config }) => sourceSlugs(member, config),
38
38
  borrowedFrom: ({ config, path }) => config.borrowedFrom(name, path),
@@ -73,6 +73,7 @@ function buildKind(spec) {
73
73
  ...bareMember ? { bareMember } : {},
74
74
  ...sourceResolution ? { sourceResolution } : {},
75
75
  ...bulkAdopts ? { bulkAdopts } : {},
76
+ ...bulkExcludes ? { bulkExcludes } : {},
76
77
  ...bulkOverlay ? { bulkOverlay } : {},
77
78
  ...bulkDirectives ? { bulkDirectives } : {},
78
79
  ...bulkOverlayWrite ? { bulkOverlayWrite } : {},
@@ -8,8 +8,10 @@ import { z } from "zod";
8
8
  */
9
9
  type ConfigIssueCode = /** Nothing at that path / no such member — an update, delete or rename with no target. */'NOT_FOUND' /** A create over an existing path, or a rename onto a taken one. */ | 'ALREADY_EXISTS'
10
10
  /**
11
- * The address cannot be an identity — a path carrying `@` other than as a leading npm scope, which
12
- * `isEntityPath` refuses because it cannot be told apart from a borrowed address.
11
+ * The address cannot be an identity — a path `entityPathProblem` refuses: one carrying `@` other
12
+ * than as a leading npm scope, or `#`, or with an empty segment, or a segment starting with `!` or
13
+ * that is `*`. Each misreads as another address: a borrowed one, a member, a link's exclusion or
14
+ * its wildcard.
13
15
  *
14
16
  * A distinct code rather than `INVALID_DATA`: nothing about the data is wrong, and the surface that
15
17
  * reports it is a name field rather than a form. This is a distinction `apply` genuinely makes.
@@ -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 = 20261013164300;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261013201548;
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 = 20261013164300;
16
+ const CURRENT_SCHEMA_VERSION = 20261013201548;
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 > 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.`);
48
+ if (migration.version > 20261013201548) 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 ?? 20261013164300;
105
+ const target = options?.target ?? 20261013201548;
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 ?? 20261013164300;
163
+ const target = options?.target ?? 20261013201548;
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 ?? 20261013164300;
182
+ const target = options?.target ?? 20261013201548;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -232,7 +232,14 @@ interface UnstatableCondition {
232
232
  consumer: ModifierCategory | 'plain';
233
233
  };
234
234
  }
235
- /** Borrowed conditions need a consumer modifier with the same name and category. */
235
+ /**
236
+ * Whether an override's condition names a modifier this config doesn't hold because a pin excludes
237
+ * it ({@link Config.sourceExcludes}). Such an override is left out, as the stylesheet leaves out a
238
+ * condition it cannot state, and the build doesn't refuse it.
239
+ */
240
+ declare function excludedCondition(config: Config, on: unknown): boolean;
241
+ /** Borrowed conditions need a consumer modifier with the same name and category, unless the pin
242
+ * excludes that modifier. */
236
243
  declare function unstatableConditions(config: Config): UnstatableCondition[];
237
244
  /** A token of ours whose conditional value names another system's modifier. */
238
245
  interface UnstatableOverride {
@@ -254,4 +261,4 @@ interface UnstatableOverride {
254
261
  */
255
262
  declare function unstatableOverrides(config: Config): UnstatableOverride[];
256
263
  //#endregion
257
- export { DanglingDirective, DanglingRef, LeafAddress, MisdeclaredOverlay, OverlayVerb, RedundantQualifier, UnadoptedDirective, UnreadableBorrow, UnstatableCondition, UnstatableOverride, classifyOverlay, danglingDirectives, danglingSourcedRefs, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, unadoptedDirectives, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };
264
+ export { DanglingDirective, DanglingRef, LeafAddress, MisdeclaredOverlay, OverlayVerb, RedundantQualifier, UnadoptedDirective, UnreadableBorrow, UnstatableCondition, UnstatableOverride, classifyOverlay, danglingDirectives, danglingSourcedRefs, excludedCondition, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, unadoptedDirectives, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };
@@ -424,7 +424,24 @@ function danglingDirectives(config) {
424
424
  }
425
425
  return found;
426
426
  }
427
- /** Borrowed conditions need a consumer modifier with the same name and category. */
427
+ /**
428
+ * Whether an override's condition names a modifier this config doesn't hold because a pin excludes
429
+ * it ({@link Config.sourceExcludes}). Such an override is left out, as the stylesheet leaves out a
430
+ * condition it cannot state, and the build doesn't refuse it.
431
+ */
432
+ function excludedCondition(config, on) {
433
+ return (Array.isArray(on) ? on : [on]).some((ref) => {
434
+ if (!isRef(ref)) return false;
435
+ const modifier = pathOf(ref);
436
+ if (config.resolve("modifier", modifier)) return false;
437
+ return config.sourceSlugs().some((slug) => {
438
+ const spelled = config.sourceFor(slug)?.resolve("modifier", modifier)?.path ?? modifier;
439
+ return config.sourceExcludes(slug, "modifier", spelled);
440
+ });
441
+ });
442
+ }
443
+ /** Borrowed conditions need a consumer modifier with the same name and category, unless the pin
444
+ * excludes that modifier. */
428
445
  function unstatableConditions(config) {
429
446
  const found = [];
430
447
  for (const slug of config.sourceSlugs()) {
@@ -443,6 +460,8 @@ function unstatableConditions(config) {
443
460
  if (!isRef(ref)) continue;
444
461
  const modifier = pathOf(ref);
445
462
  if (!config.resolve("modifier", modifier)) {
463
+ const spelled = source.resolve("modifier", modifier)?.path ?? modifier;
464
+ if (config.sourceExcludes(slug, "modifier", spelled)) continue;
446
465
  found.push({
447
466
  slug,
448
467
  token,
@@ -575,4 +594,4 @@ function borrowedSlug(body) {
575
594
  return isRef(body) ? sourceOf(body) : void 0;
576
595
  }
577
596
  //#endregion
578
- export { borrowedSlug, classifyOverlay, danglingDirectives, danglingSourcedRefs, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, sourceSlugs, unadoptedDirectives, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };
597
+ export { borrowedSlug, classifyOverlay, danglingDirectives, danglingSourcedRefs, excludedCondition, leafAddresses, leafVerb, misdeclaredOverlays, overlayVerb, redundantQualifiers, sourceSlugs, unadoptedDirectives, unreadableBorrows, unstatableConditions, unstatableOverrides, valueAt };
@@ -57,12 +57,24 @@ interface ExtendedRef<Delta extends Record<string, unknown> = Record<string, unk
57
57
  * `if (!source) return ''` escape and the declaration vanishes), reports a false dangling ref naming a
58
58
  * source that does not exist, and marks the item borrowed on every provenance surface. Nothing throws.
59
59
  *
60
+ * The rest of the grammar a path is read in is refused the same way, since each misreads as silently:
61
+ * - `#` anywhere: the ref grammar splits on the first `#` before anything else, so `token:size/a#b`
62
+ * is a member ref (`b`) into `size/a`.
63
+ * - An empty segment (`''`, `a//b`, `/a`, `a/`): `/` is what makes a folder, and a pin's `link` covers
64
+ * a folder's members as the paths under `<folder>/`.
65
+ * - A segment starting with `!`: a `link` list entry `'!text'` leaves `text` out, so a path of that
66
+ * name couldn't be linked by name.
67
+ * - A segment that is `*`: in a `link` list, `'*'` is every path.
68
+ *
60
69
  * Checked where a path becomes an identity — a create, a rename's target, an authored entry — and
61
70
  * deliberately not on reads, so an existing bad path stays addressable and, more to the point,
62
71
  * deletable.
63
72
  */
73
+ declare function entityPathProblem(path: string): string | undefined;
74
+ /** Whether `path` can be an entity's own path: {@link entityPathProblem} finds nothing wrong with it. */
64
75
  declare function isEntityPath(path: string): boolean;
65
- /** What a refused path should say, in one place, so a create, a rename and a register word it alike. */
76
+ /** What a path carrying a misplaced `@` should say, so a create, a rename and a register word it
77
+ * alike. {@link entityPathProblem} says it for every rule. */
66
78
  declare const ENTITY_PATH_MESSAGE = "An entity path may not contain \"@\" except as a leading npm scope \u2014 \"@\" marks the source a ref is drawn from, so a path carrying one cannot be told apart from a borrowed address.";
67
79
  /**
68
80
  * A reference to another entity: `ref('token', 'color/brand')`. Resolution is prefix-or-leaf against
@@ -200,4 +212,4 @@ declare function rewriteRefNamespace(value: unknown, kind: string, from: string,
200
212
  */
201
213
  declare function rewriteRefSource(value: unknown, from: string, to: string | undefined): unknown;
202
214
  //#endregion
203
- export { ENTITY_PATH_MESSAGE, ExtendableRef, ExtendedRef, MINTED, Ref, collectRefs, declarationText, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, ref, refKey, refLeaf, refOfKey, refSchema, refTo, replaceRefMarkers, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, sourceHops, sourceOf, splitRef, splitRefMarkers };
215
+ export { ENTITY_PATH_MESSAGE, ExtendableRef, ExtendedRef, MINTED, Ref, collectRefs, declarationText, entityPathProblem, entityRefOf, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, ref, refKey, refLeaf, refOfKey, refSchema, refTo, replaceRefMarkers, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, sourceHops, sourceOf, splitRef, splitRefMarkers };
@@ -59,14 +59,33 @@ function memberOf(target) {
59
59
  * `if (!source) return ''` escape and the declaration vanishes), reports a false dangling ref naming a
60
60
  * source that does not exist, and marks the item borrowed on every provenance surface. Nothing throws.
61
61
  *
62
+ * The rest of the grammar a path is read in is refused the same way, since each misreads as silently:
63
+ * - `#` anywhere: the ref grammar splits on the first `#` before anything else, so `token:size/a#b`
64
+ * is a member ref (`b`) into `size/a`.
65
+ * - An empty segment (`''`, `a//b`, `/a`, `a/`): `/` is what makes a folder, and a pin's `link` covers
66
+ * a folder's members as the paths under `<folder>/`.
67
+ * - A segment starting with `!`: a `link` list entry `'!text'` leaves `text` out, so a path of that
68
+ * name couldn't be linked by name.
69
+ * - A segment that is `*`: in a `link` list, `'*'` is every path.
70
+ *
62
71
  * Checked where a path becomes an identity — a create, a rename's target, an authored entry — and
63
72
  * deliberately not on reads, so an existing bad path stays addressable and, more to the point,
64
73
  * deletable.
65
74
  */
75
+ function entityPathProblem(path) {
76
+ if (path.lastIndexOf("@") > 0) return ENTITY_PATH_MESSAGE;
77
+ if (path.includes("#")) return "An entity path may not contain \"#\" — \"#\" marks the member a ref points into, so a path carrying one reads as a member of a shorter path.";
78
+ const segments = path.split("/");
79
+ if (segments.some((segment) => segment === "")) return "An entity path may not have an empty segment — \"/\" separates a folder from its members, so \"\", \"a//b\", \"/a\" and \"a/\" name no folder a link or a ref can address.";
80
+ if (segments.some((segment) => segment.startsWith("!"))) return "An entity path segment may not start with \"!\" — a link's \"!path\" entry leaves that path out, so a path named that way could not be linked by name.";
81
+ if (segments.some((segment) => segment === "*")) return "An entity path segment may not be \"*\" — in a link, \"*\" stands for every path.";
82
+ }
83
+ /** Whether `path` can be an entity's own path: {@link entityPathProblem} finds nothing wrong with it. */
66
84
  function isEntityPath(path) {
67
- return path.lastIndexOf("@") <= 0;
85
+ return entityPathProblem(path) === void 0;
68
86
  }
69
- /** What a refused path should say, in one place, so a create, a rename and a register word it alike. */
87
+ /** What a path carrying a misplaced `@` should say, so a create, a rename and a register word it
88
+ * alike. {@link entityPathProblem} says it for every rule. */
70
89
  const ENTITY_PATH_MESSAGE = "An entity path may not contain \"@\" except as a leading npm scope — \"@\" marks the source a ref is drawn from, so a path carrying one cannot be told apart from a borrowed address.";
71
90
  /**
72
91
  * A reference to another entity: `ref('token', 'color/brand')`. Resolution is prefix-or-leaf against
@@ -455,4 +474,4 @@ function canonicalCondition(when) {
455
474
  return entries.length === 0 ? "*" : entries.map(([k, v]) => `${k}=${String(refLeaf(v))}`).join("&");
456
475
  }
457
476
  //#endregion
458
- export { ENTITY_PATH_MESSAGE, MINTED, canonicalCondition, collectRefs, declarationText, entityRefOf, hasRefMarker, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refKey, refLeaf, refOfKey, refSchema, refTo, replaceRefMarkers, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, singletonRef, sourceHops, sourceOf, splitRef, splitRefMarkers };
477
+ export { ENTITY_PATH_MESSAGE, MINTED, canonicalCondition, collectRefs, declarationText, entityPathProblem, entityRefOf, hasRefMarker, isEntityPath, isExtendedRef, isMintedRow, isRef, kindOf, memberOf, memberRef, orRef, pathOf, qualifyRefs, ref, refKey, refLeaf, refOfKey, refSchema, refTo, replaceRefMarkers, rewriteRefNamespace, rewriteRefSource, rewriteRefs, routedSource, singletonRef, sourceHops, sourceOf, splitRef, splitRefMarkers };