@maroonedog/luq 2.1.0 → 2.3.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 (161) hide show
  1. package/README.md +92 -430
  2. package/dist/builder/builder-surface.types.d.ts +1 -0
  3. package/dist/builder/compile-declarations.d.ts +7 -1
  4. package/dist/builder/compile-declarations.js +18 -8
  5. package/dist/builder/compile-declarations.mjs +18 -8
  6. package/dist/builder/create-builder.js +8 -0
  7. package/dist/builder/create-builder.mjs +8 -0
  8. package/dist/builder/create-field-builder.js +13 -1
  9. package/dist/builder/create-field-builder.mjs +13 -1
  10. package/dist/builder/declared-calls-store.d.ts +9 -0
  11. package/dist/builder/declared-calls-store.js +19 -0
  12. package/dist/builder/declared-calls-store.mjs +15 -0
  13. package/dist/builder/field-builder.types.d.ts +13 -0
  14. package/dist/builder/field-declared-calls.types.d.ts +6 -0
  15. package/dist/builder/field-declared-calls.types.js +2 -0
  16. package/dist/builder/field-declared-calls.types.mjs +1 -0
  17. package/dist/builder/field-entry.types.d.ts +9 -3
  18. package/dist/builder/field-options.types.d.ts +26 -0
  19. package/dist/chain/bundle-paths.types.d.ts +23 -0
  20. package/dist/chain/bundle-paths.types.js +2 -0
  21. package/dist/chain/bundle-paths.types.mjs +1 -0
  22. package/dist/chain/chain-method.types.d.ts +8 -3
  23. package/dist/chain/chain-node-store.d.ts +5 -0
  24. package/dist/chain/chain-node-store.js +15 -0
  25. package/dist/chain/chain-node-store.mjs +11 -0
  26. package/dist/chain/collect-field-rules.d.ts +13 -2
  27. package/dist/chain/collect-field-rules.js +10 -3
  28. package/dist/chain/collect-field-rules.mjs +10 -3
  29. package/dist/chain/create-chain-node.js +19 -9
  30. package/dist/chain/create-chain-node.mjs +19 -9
  31. package/dist/chain/declaration-recorder.port.d.ts +31 -0
  32. package/dist/chain/declaration-recorder.port.js +17 -0
  33. package/dist/chain/declaration-recorder.port.mjs +13 -0
  34. package/dist/chain/declared-call.types.d.ts +15 -0
  35. package/dist/chain/declared-call.types.js +2 -0
  36. package/dist/chain/declared-call.types.mjs +1 -0
  37. package/dist/chain/index.d.ts +3 -1
  38. package/dist/chain/resolve-args.types.d.ts +4 -2
  39. package/dist/compile/compile-array-node.d.ts +0 -7
  40. package/dist/compile/compile-array-node.js +5 -0
  41. package/dist/compile/compile-array-node.mjs +5 -0
  42. package/dist/compile/compile-field.d.ts +1 -0
  43. package/dist/compile/compile-field.js +13 -2
  44. package/dist/compile/compile-field.mjs +13 -2
  45. package/dist/compile/compile-schema.js +4 -0
  46. package/dist/compile/compile-schema.mjs +4 -0
  47. package/dist/compile/group-array-fields.d.ts +1 -0
  48. package/dist/compile/split-rules-by-kind.js +27 -7
  49. package/dist/compile/split-rules-by-kind.mjs +27 -7
  50. package/dist/compile/validation-plan.types.d.ts +36 -0
  51. package/dist/core/type-erasure.d.ts +36 -30
  52. package/dist/core/type-erasure.js +36 -30
  53. package/dist/core/type-erasure.mjs +36 -30
  54. package/dist/json-schema/build-from-schema.js +8 -1
  55. package/dist/json-schema/build-from-schema.mjs +8 -1
  56. package/dist/json-schema/declare-additional-properties.d.ts +7 -7
  57. package/dist/json-schema/declare-additional-properties.js +7 -7
  58. package/dist/json-schema/declare-additional-properties.mjs +7 -7
  59. package/dist/json-schema/declare-object-keywords.js +4 -4
  60. package/dist/json-schema/declare-object-keywords.mjs +4 -4
  61. package/dist/json-schema/flatten-schema.js +1 -1
  62. package/dist/json-schema/flatten-schema.mjs +1 -1
  63. package/dist/json-schema/follow-json-pointer.d.ts +7 -6
  64. package/dist/json-schema/follow-json-pointer.js +24 -24
  65. package/dist/json-schema/follow-json-pointer.mjs +24 -24
  66. package/dist/json-schema/ref-resolution-error.js +3 -3
  67. package/dist/json-schema/ref-resolution-error.mjs +3 -3
  68. package/dist/json-schema/schema-registry.js +12 -11
  69. package/dist/json-schema/schema-registry.mjs +12 -11
  70. package/dist/json-schema/uri-reference.js +12 -12
  71. package/dist/json-schema/uri-reference.mjs +12 -12
  72. package/dist/path/create-value-writer.js +12 -12
  73. package/dist/path/create-value-writer.mjs +12 -12
  74. package/dist/path/reserved-segment.d.ts +16 -16
  75. package/dist/path/reserved-segment.js +17 -21
  76. package/dist/path/reserved-segment.mjs +17 -21
  77. package/dist/plugin-kit/marker.types.d.ts +18 -0
  78. package/dist/plugins/index.generated.d.ts +1 -0
  79. package/dist/plugins/index.generated.js +6 -4
  80. package/dist/plugins/index.generated.mjs +3 -2
  81. package/dist/plugins/manifest.generated.js +3 -2
  82. package/dist/plugins/manifest.generated.mjs +3 -2
  83. package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +8 -8
  84. package/dist/plugins/object-additional-properties/select-additional-keys.js +16 -16
  85. package/dist/plugins/object-additional-properties/select-additional-keys.mjs +16 -16
  86. package/dist/plugins/stitch/stitch.d.ts +24 -8
  87. package/dist/plugins/stitch-with/index.d.ts +2 -0
  88. package/dist/plugins/stitch-with/index.js +5 -0
  89. package/dist/plugins/stitch-with/index.mjs +1 -0
  90. package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
  91. package/dist/plugins/stitch-with/stitch-with.js +92 -0
  92. package/dist/plugins/stitch-with/stitch-with.mjs +89 -0
  93. package/dist/plugins/stitchWith.d.ts +1 -0
  94. package/dist/plugins/stitchWith.js +2 -0
  95. package/dist/plugins/stitchWith.mjs +1 -0
  96. package/dist/plugins/string-min/string-min.js +19 -3
  97. package/dist/plugins/string-min/string-min.mjs +20 -4
  98. package/dist/presets/index.d.ts +1 -0
  99. package/dist/presets/index.js +9 -0
  100. package/dist/presets/index.mjs +1 -0
  101. package/dist/presets/presets.d.ts +164 -0
  102. package/dist/presets/presets.js +79 -0
  103. package/dist/presets/presets.mjs +76 -0
  104. package/dist/runtime/create-field-validator.js +22 -8
  105. package/dist/runtime/create-field-validator.mjs +22 -8
  106. package/dist/runtime/create-validator.js +38 -8
  107. package/dist/runtime/create-validator.mjs +38 -8
  108. package/dist/runtime/field-rule-context.d.ts +32 -0
  109. package/dist/runtime/field-rule-context.js +48 -0
  110. package/dist/runtime/field-rule-context.mjs +44 -0
  111. package/dist/runtime/index-stack.d.ts +29 -4
  112. package/dist/runtime/index-stack.js +76 -15
  113. package/dist/runtime/index-stack.mjs +76 -15
  114. package/dist/runtime/output-writer.js +5 -1
  115. package/dist/runtime/output-writer.mjs +5 -1
  116. package/dist/runtime/run-array-node.js +22 -10
  117. package/dist/runtime/run-array-node.mjs +22 -10
  118. package/dist/runtime/run-field.js +47 -18
  119. package/dist/runtime/run-field.mjs +47 -18
  120. package/dist/runtime/run-plan.js +5 -1
  121. package/dist/runtime/run-plan.mjs +5 -1
  122. package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
  123. package/dist/standard-schema/assemble-json-schema.js +95 -0
  124. package/dist/standard-schema/assemble-json-schema.mjs +92 -0
  125. package/dist/standard-schema/declaration-recorder.d.ts +6 -0
  126. package/dist/standard-schema/declaration-recorder.js +30 -0
  127. package/dist/standard-schema/declaration-recorder.mjs +27 -0
  128. package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
  129. package/dist/standard-schema/declarations-unavailable-error.js +32 -0
  130. package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
  131. package/dist/standard-schema/emit-field-schema.d.ts +9 -0
  132. package/dist/standard-schema/emit-field-schema.js +68 -0
  133. package/dist/standard-schema/emit-field-schema.mjs +65 -0
  134. package/dist/standard-schema/index.d.ts +5 -0
  135. package/dist/standard-schema/index.js +9 -1
  136. package/dist/standard-schema/index.mjs +4 -0
  137. package/dist/standard-schema/json-schema-target.d.ts +6 -0
  138. package/dist/standard-schema/json-schema-target.js +44 -0
  139. package/dist/standard-schema/json-schema-target.mjs +39 -0
  140. package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
  141. package/dist/standard-schema/plugin-keyword-map.js +93 -0
  142. package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
  143. package/dist/standard-schema/split-issue-path.d.ts +6 -4
  144. package/dist/standard-schema/split-issue-path.js +15 -13
  145. package/dist/standard-schema/split-issue-path.mjs +15 -13
  146. package/dist/standard-schema/standard-schema.types.d.ts +8 -7
  147. package/dist/standard-schema/standard-schema.types.js +6 -6
  148. package/dist/standard-schema/standard-schema.types.mjs +6 -6
  149. package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
  150. package/dist/standard-schema/to-standard-json-schema.js +36 -0
  151. package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
  152. package/dist/standard-schema/to-standard-schema.d.ts +16 -15
  153. package/dist/standard-schema/to-standard-schema.js +15 -22
  154. package/dist/standard-schema/to-standard-schema.mjs +15 -22
  155. package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
  156. package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
  157. package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
  158. package/dist/types/index.d.ts +19 -7
  159. package/dist/types/index.js +7 -7
  160. package/dist/types/index.mjs +7 -7
  161. package/package.json +35 -19
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,10 @@
1
1
  export { attachSlotMethods, CHAIN_BUILT_IN_OWNER, PluginMethodCollisionError, } from "./attach-slot-methods";
2
2
  export { createChainNode, readChainRules, EMPTY_RULES, type ChainBuildContext, type ChainNodeWiring, } from "./create-chain-node";
3
+ export type { DeclaredCall } from "./declared-call.types";
4
+ export type { DeclarationRecorder } from "./declaration-recorder.port";
3
5
  export { collectBranchRules, resolvePluginArguments, SubChainResultError, type SlotSurfaceFactory, type SubChainArgumentDeclaration, type SubChainDefine, } from "./collect-branch-rules";
4
6
  export { buildSlotSurface, createFieldSlots } from "./create-field-slots";
5
- export { collectFieldRules, FieldChainResultError, } from "./collect-field-rules";
7
+ export { collectFieldRules, FieldChainResultError, type FieldChainOutcome, } from "./collect-field-rules";
6
8
  export type { PluginBag, BagEntry, SlotPlugins } from "./plugin-bag.types";
7
9
  export type { AllowNull, ChainState, CoverWith, ExcludeMissing, ExcludeNull, ExcludeUndefined, OpenState, UncoveredMembers, UnionGuardCoverageError, } from "./chain-state.types";
8
10
  export type { ChainMethod } from "./chain-method.types";
@@ -1,5 +1,5 @@
1
1
  import type { ArrayItemContext, Present } from "../types";
2
- import type { ElementChain, FieldRef, FieldRefs, GuardOut, MarkerRecord, NarrowedChain, PresenceShift, PropertyKeyChain, PropertyValueChain, RootPredicate, RootReader, SelfGuard, SelfReader, SelfValue, TransformOut, Unchanged } from "../plugin-kit/marker.types";
2
+ import type { ElementChain, FieldRef, FieldRefs, GuardOut, MarkerRecord, NarrowedChain, PresenceShift, PropertyKeyChain, PropertyValueChain, RootPredicate, RootReader, SelfGuard, SelfReader, SelfValue, BundleOut, StitchOut, TransformOut, Unchanged } from "../plugin-kit/marker.types";
3
3
  import type { ElementOf } from "../path/element-of.types";
4
4
  import type { PropertyValueOf } from "../path/property-value-of.types";
5
5
  import type { FieldPath } from "../path/field-path.types";
@@ -24,5 +24,7 @@ export type ResolveArgs<A extends readonly unknown[], B extends PluginBag, TRoot
24
24
  /** Presence shifts go through the NAMED operators over ChainState. */
25
25
  export type ResolveOut<O, TValue, TState extends ChainState> = [O] extends [
26
26
  Unchanged
27
- ] ? [TValue, TState] : [O] extends [TransformOut] ? [unknown, TState] : [O] extends [GuardOut] ? [TValue, TState] : [O] extends [PresenceShift<"excludeMissing">] ? [TValue, ExcludeMissing<TState>] : [O] extends [PresenceShift<"excludeUndefined">] ? [TValue, ExcludeUndefined<TState>] : [O] extends [PresenceShift<"excludeNull">] ? [TValue, ExcludeNull<TState>] : [O] extends [PresenceShift<"allowNull">] ? [TValue | null, AllowNull<TState>] : [O, TState];
27
+ ] ? [TValue, TState] : [O] extends [TransformOut] ? [unknown, TState] : [O] extends [GuardOut] ? [TValue, TState] : [
28
+ O
29
+ ] extends [StitchOut] ? [TValue, TState] : [O] extends [BundleOut] ? [TValue, TState] : [O] extends [PresenceShift<"excludeMissing">] ? [TValue, ExcludeMissing<TState>] : [O] extends [PresenceShift<"excludeUndefined">] ? [TValue, ExcludeUndefined<TState>] : [O] extends [PresenceShift<"excludeNull">] ? [TValue, ExcludeNull<TState>] : [O] extends [PresenceShift<"allowNull">] ? [TValue | null, AllowNull<TState>] : [O, TState];
28
30
  export {};
@@ -9,11 +9,4 @@ export interface NodeCompileContext {
9
9
  }
10
10
  /** The one adapter from a grouped declaration to compileField's request. */
11
11
  export declare function compileRelativeDeclaration(declaration: RelativeDeclaration, context: NodeCompileContext): CompiledField;
12
- /**
13
- * Nesting is a re-grouping of the members, not a second traversal strategy:
14
- * `items[*].sub[*].x` arrives here as the group `items` whose one member is
15
- * still `sub[*].x`, and that member groups again into the nested node `sub`.
16
- * `matrix[*][*]` reaches the same shape through an EMPTY nested template —
17
- * the element of the outer array is itself the inner array.
18
- */
19
12
  export declare function compileArrayNode(group: ArrayFieldGroup, context: NodeCompileContext): ArrayNode;
@@ -16,6 +16,7 @@ exports.compileArrayNode = compileArrayNode;
16
16
  // silently unvalidated.
17
17
  // ===========================================================================
18
18
  const create_array_reader_1 = require("../path/create-array-reader");
19
+ const format_issue_path_1 = require("../path/format-issue-path");
19
20
  const compile_field_1 = require("./compile-field");
20
21
  const group_array_fields_1 = require("./group-array-fields");
21
22
  /** The one adapter from a grouped declaration to compileField's request. */
@@ -26,6 +27,7 @@ function compileRelativeDeclaration(declaration, context) {
26
27
  fieldPath: declaration.fieldPath,
27
28
  defaultOf: declaration.defaultOf,
28
29
  applyDefaultToNull: declaration.applyDefaultToNull,
30
+ normalize: declaration.normalize,
29
31
  planRef: context.planRef,
30
32
  eraseComposite: context.eraseComposite,
31
33
  });
@@ -37,11 +39,14 @@ function compileRelativeDeclaration(declaration, context) {
37
39
  * `matrix[*][*]` reaches the same shape through an EMPTY nested template —
38
40
  * the element of the outer array is itself the inner array.
39
41
  */
42
+ /** A grouped node template never keeps a wildcard, so rendering needs no index. */
43
+ const NO_INDICES = Object.freeze([]);
40
44
  function compileArrayNode(group, context) {
41
45
  const grouped = (0, group_array_fields_1.groupArrayFields)(group.members);
42
46
  const template = Object.freeze(group.template);
43
47
  const node = {
44
48
  template,
49
+ renderedPath: (0, format_issue_path_1.formatIssuePath)(template, NO_INDICES),
45
50
  read: (0, create_array_reader_1.createArrayReader)(template),
46
51
  elementFields: Object.freeze(grouped.direct.map((declaration) => compileRelativeDeclaration(declaration, context))),
47
52
  nested: Object.freeze(grouped.arrays.map((nested) => compileArrayNode(nested, context))),
@@ -12,6 +12,7 @@
12
12
  // silently unvalidated.
13
13
  // ===========================================================================
14
14
  import { createArrayReader } from "../path/create-array-reader.mjs";
15
+ import { formatIssuePath } from "../path/format-issue-path.mjs";
15
16
  import { compileField } from "./compile-field.mjs";
16
17
  import { groupArrayFields, } from "./group-array-fields.mjs";
17
18
  /** The one adapter from a grouped declaration to compileField's request. */
@@ -22,6 +23,7 @@ export function compileRelativeDeclaration(declaration, context) {
22
23
  fieldPath: declaration.fieldPath,
23
24
  defaultOf: declaration.defaultOf,
24
25
  applyDefaultToNull: declaration.applyDefaultToNull,
26
+ normalize: declaration.normalize,
25
27
  planRef: context.planRef,
26
28
  eraseComposite: context.eraseComposite,
27
29
  });
@@ -33,11 +35,14 @@ export function compileRelativeDeclaration(declaration, context) {
33
35
  * `matrix[*][*]` reaches the same shape through an EMPTY nested template —
34
36
  * the element of the outer array is itself the inner array.
35
37
  */
38
+ /** A grouped node template never keeps a wildcard, so rendering needs no index. */
39
+ const NO_INDICES = Object.freeze([]);
36
40
  export function compileArrayNode(group, context) {
37
41
  const grouped = groupArrayFields(group.members);
38
42
  const template = Object.freeze(group.template);
39
43
  const node = {
40
44
  template,
45
+ renderedPath: formatIssuePath(template, NO_INDICES),
41
46
  read: createArrayReader(template),
42
47
  elementFields: Object.freeze(grouped.direct.map((declaration) => compileRelativeDeclaration(declaration, context))),
43
48
  nested: Object.freeze(grouped.arrays.map((nested) => compileArrayNode(nested, context))),
@@ -18,6 +18,7 @@ export interface FieldCompileRequest {
18
18
  readonly fieldPath: string;
19
19
  readonly defaultOf: ((root: unknown) => unknown) | null;
20
20
  readonly applyDefaultToNull: boolean;
21
+ readonly normalize: ((value: unknown) => unknown) | null;
21
22
  readonly planRef: PlanRef;
22
23
  readonly eraseComposite: CompositeEraser;
23
24
  }
@@ -4,6 +4,7 @@ exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = void 0;
4
4
  exports.compileField = compileField;
5
5
  exports.compileFieldDeclaration = compileFieldDeclaration;
6
6
  const create_value_reader_1 = require("../path/create-value-reader");
7
+ const format_issue_path_1 = require("../path/format-issue-path");
7
8
  const create_value_writer_1 = require("../path/create-value-writer");
8
9
  const parse_field_path_1 = require("../path/parse-field-path");
9
10
  const resolve_conditional_presence_1 = require("./resolve-conditional-presence");
@@ -12,17 +13,26 @@ const resolve_recursion_1 = require("./resolve-recursion");
12
13
  const split_rules_by_kind_1 = require("./split-rules-by-kind");
13
14
  /** Whether a field with no explicit setting replaces a null with its default. */
14
15
  exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
16
+ /** No indices here: an open index is carried by the run-time prefix. */
17
+ const NO_INDICES = Object.freeze([]);
15
18
  function compileField(request) {
16
19
  const byKind = (0, split_rules_by_kind_1.splitRulesByKind)(request.rules);
17
20
  const hasDefault = request.defaultOf !== null;
18
- const needsWriter = byKind.transforms.length > 0 || hasDefault;
21
+ // Normalizing replaces the value too, so it needs a writer just as default does.
22
+ const needsWriter = byKind.transforms.length > 0 || hasDefault || request.normalize !== null;
19
23
  const template = Object.freeze(request.template);
24
+ // The reader is made first. Refusing a template that still holds a wildcard
25
+ // is the reader's job; formatting the path before that raises a "not enough
26
+ // indices" RangeError which overtakes the PathSyntaxError that should win.
27
+ const read = (0, create_value_reader_1.createValueReader)(template);
20
28
  const field = {
21
29
  template,
22
- read: (0, create_value_reader_1.createValueReader)(template),
30
+ renderedPath: (0, format_issue_path_1.formatIssuePath)(template, NO_INDICES),
31
+ read,
23
32
  write: needsWriter ? (0, create_value_writer_1.createValueWriter)(template) : null,
24
33
  defaultOf: request.defaultOf,
25
34
  applyDefaultToNull: request.applyDefaultToNull,
35
+ normalize: request.normalize,
26
36
  presence: (0, resolve_presence_1.resolvePresence)(byKind.presences),
27
37
  presenceOverrides: (0, resolve_conditional_presence_1.resolveConditionalPresence)(byKind.conditionalPresences),
28
38
  gates: byKind.gates,
@@ -45,6 +55,7 @@ function compileFieldDeclaration(declaration, planRef, eraseComposite) {
45
55
  fieldPath: declaration.path,
46
56
  defaultOf: declaration.defaultOf ?? null,
47
57
  applyDefaultToNull: declaration.applyDefaultToNull ?? exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
58
+ normalize: declaration.normalize ?? null,
48
59
  planRef,
49
60
  eraseComposite,
50
61
  });
@@ -1,4 +1,5 @@
1
1
  import { createValueReader } from "../path/create-value-reader.mjs";
2
+ import { formatIssuePath } from "../path/format-issue-path.mjs";
2
3
  import { createValueWriter } from "../path/create-value-writer.mjs";
3
4
  import { parseFieldPath } from "../path/parse-field-path.mjs";
4
5
  import { resolveConditionalPresence } from "./resolve-conditional-presence.mjs";
@@ -7,17 +8,26 @@ import { resolveRecursion } from "./resolve-recursion.mjs";
7
8
  import { splitRulesByKind } from "./split-rules-by-kind.mjs";
8
9
  /** Whether a field with no explicit setting replaces a null with its default. */
9
10
  export const APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
11
+ /** No indices here: an open index is carried by the run-time prefix. */
12
+ const NO_INDICES = Object.freeze([]);
10
13
  export function compileField(request) {
11
14
  const byKind = splitRulesByKind(request.rules);
12
15
  const hasDefault = request.defaultOf !== null;
13
- const needsWriter = byKind.transforms.length > 0 || hasDefault;
16
+ // Normalizing replaces the value too, so it needs a writer just as default does.
17
+ const needsWriter = byKind.transforms.length > 0 || hasDefault || request.normalize !== null;
14
18
  const template = Object.freeze(request.template);
19
+ // The reader is made first. Refusing a template that still holds a wildcard
20
+ // is the reader's job; formatting the path before that raises a "not enough
21
+ // indices" RangeError which overtakes the PathSyntaxError that should win.
22
+ const read = createValueReader(template);
15
23
  const field = {
16
24
  template,
17
- read: createValueReader(template),
25
+ renderedPath: formatIssuePath(template, NO_INDICES),
26
+ read,
18
27
  write: needsWriter ? createValueWriter(template) : null,
19
28
  defaultOf: request.defaultOf,
20
29
  applyDefaultToNull: request.applyDefaultToNull,
30
+ normalize: request.normalize,
21
31
  presence: resolvePresence(byKind.presences),
22
32
  presenceOverrides: resolveConditionalPresence(byKind.conditionalPresences),
23
33
  gates: byKind.gates,
@@ -40,6 +50,7 @@ export function compileFieldDeclaration(declaration, planRef, eraseComposite) {
40
50
  fieldPath: declaration.path,
41
51
  defaultOf: declaration.defaultOf ?? null,
42
52
  applyDefaultToNull: declaration.applyDefaultToNull ?? APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
53
+ normalize: declaration.normalize ?? null,
43
54
  planRef,
44
55
  eraseComposite,
45
56
  });
@@ -64,6 +64,7 @@ function compilePlan(relatives, executor) {
64
64
  arrays: Object.freeze(grouped.arrays.map((group) => (0, compile_array_node_1.compileArrayNode)(group, context))),
65
65
  hasTransforms: relatives.some((declaration) => declaration.rules.some((rule) => rule.kind === "transform")),
66
66
  hasDefaults: relatives.some((declaration) => declaration.defaultOf !== null),
67
+ hasNormalizers: relatives.some((declaration) => declaration.normalize !== null),
67
68
  });
68
69
  return compiled;
69
70
  }
@@ -94,6 +95,7 @@ function parseRelativeDeclaration(declaration) {
94
95
  fieldPath: declaration.path,
95
96
  defaultOf: declaration.defaultOf ?? null,
96
97
  applyDefaultToNull: declaration.applyDefaultToNull ?? compile_field_1.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
98
+ normalize: declaration.normalize ?? null,
97
99
  };
98
100
  }
99
101
  /**
@@ -111,6 +113,7 @@ function collectBranchDeclarations(branch) {
111
113
  fieldPath: branch.label,
112
114
  defaultOf: null,
113
115
  applyDefaultToNull: compile_field_1.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
116
+ normalize: null,
114
117
  });
115
118
  }
116
119
  for (const field of branch.fields) {
@@ -120,6 +123,7 @@ function collectBranchDeclarations(branch) {
120
123
  fieldPath: field.path,
121
124
  defaultOf: null,
122
125
  applyDefaultToNull: compile_field_1.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
126
+ normalize: null,
123
127
  });
124
128
  }
125
129
  return declarations;
@@ -59,6 +59,7 @@ function compilePlan(relatives, executor) {
59
59
  arrays: Object.freeze(grouped.arrays.map((group) => compileArrayNode(group, context))),
60
60
  hasTransforms: relatives.some((declaration) => declaration.rules.some((rule) => rule.kind === "transform")),
61
61
  hasDefaults: relatives.some((declaration) => declaration.defaultOf !== null),
62
+ hasNormalizers: relatives.some((declaration) => declaration.normalize !== null),
62
63
  });
63
64
  return compiled;
64
65
  }
@@ -89,6 +90,7 @@ function parseRelativeDeclaration(declaration) {
89
90
  fieldPath: declaration.path,
90
91
  defaultOf: declaration.defaultOf ?? null,
91
92
  applyDefaultToNull: declaration.applyDefaultToNull ?? APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
93
+ normalize: declaration.normalize ?? null,
92
94
  };
93
95
  }
94
96
  /**
@@ -106,6 +108,7 @@ function collectBranchDeclarations(branch) {
106
108
  fieldPath: branch.label,
107
109
  defaultOf: null,
108
110
  applyDefaultToNull: APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
111
+ normalize: null,
109
112
  });
110
113
  }
111
114
  for (const field of branch.fields) {
@@ -115,6 +118,7 @@ function collectBranchDeclarations(branch) {
115
118
  fieldPath: field.path,
116
119
  defaultOf: null,
117
120
  applyDefaultToNull: APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
121
+ normalize: null,
118
122
  });
119
123
  }
120
124
  return declarations;
@@ -14,6 +14,7 @@ export interface RelativeDeclaration {
14
14
  readonly fieldPath: string;
15
15
  readonly defaultOf: ((root: unknown) => unknown) | null;
16
16
  readonly applyDefaultToNull: boolean;
17
+ readonly normalize: ((value: unknown) => unknown) | null;
17
18
  }
18
19
  /** Every declaration that reaches through the same array, plus that array. */
19
20
  export interface ArrayFieldGroup {
@@ -11,6 +11,26 @@ class UnknownRuleKindError extends Error {
11
11
  }
12
12
  }
13
13
  exports.UnknownRuleKindError = UnknownRuleKindError;
14
+ /**
15
+ * Every empty bucket is the same object.
16
+ *
17
+ * A field typically uses two or three of the kinds and leaves the rest empty.
18
+ * Freezing a fresh [] per field per kind made every one of those empties a
19
+ * distinct object, so reading the same empty list across fields kept landing
20
+ * on a different receiver.
21
+ */
22
+ const NO_RULES_OF_THIS_KIND = Object.freeze([]);
23
+ /**
24
+ * Returns the shared empty for an empty bucket, and the frozen array
25
+ * otherwise.
26
+ *
27
+ * `readonly never[]` is assignable to every `readonly R[]`, so sharing needs
28
+ * no type assertion — which matters, because this is not a file allowed to
29
+ * write one.
30
+ */
31
+ function freezeRules(rules) {
32
+ return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
33
+ }
14
34
  function splitRulesByKind(rules) {
15
35
  const checks = [];
16
36
  const presences = [];
@@ -38,13 +58,13 @@ function splitRulesByKind(rules) {
38
58
  rejectUnknownRuleKind(rule);
39
59
  }
40
60
  return Object.freeze({
41
- checks: Object.freeze(checks),
42
- presences: Object.freeze(presences),
43
- conditionalPresences: Object.freeze(conditionalPresences),
44
- gates: Object.freeze(gates),
45
- transforms: Object.freeze(transforms),
46
- composites: Object.freeze(composites),
47
- recursions: Object.freeze(recursions),
61
+ checks: freezeRules(checks),
62
+ presences: freezeRules(presences),
63
+ conditionalPresences: freezeRules(conditionalPresences),
64
+ gates: freezeRules(gates),
65
+ transforms: freezeRules(transforms),
66
+ composites: freezeRules(composites),
67
+ recursions: freezeRules(recursions),
48
68
  });
49
69
  }
50
70
  /** The exhaustiveness proof: the parameter is `never`, so an eighth kind is a
@@ -6,6 +6,26 @@ export class UnknownRuleKindError extends Error {
6
6
  this.name = "UnknownRuleKindError";
7
7
  }
8
8
  }
9
+ /**
10
+ * Every empty bucket is the same object.
11
+ *
12
+ * A field typically uses two or three of the kinds and leaves the rest empty.
13
+ * Freezing a fresh [] per field per kind made every one of those empties a
14
+ * distinct object, so reading the same empty list across fields kept landing
15
+ * on a different receiver.
16
+ */
17
+ const NO_RULES_OF_THIS_KIND = Object.freeze([]);
18
+ /**
19
+ * Returns the shared empty for an empty bucket, and the frozen array
20
+ * otherwise.
21
+ *
22
+ * `readonly never[]` is assignable to every `readonly R[]`, so sharing needs
23
+ * no type assertion — which matters, because this is not a file allowed to
24
+ * write one.
25
+ */
26
+ function freezeRules(rules) {
27
+ return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
28
+ }
9
29
  export function splitRulesByKind(rules) {
10
30
  const checks = [];
11
31
  const presences = [];
@@ -33,13 +53,13 @@ export function splitRulesByKind(rules) {
33
53
  rejectUnknownRuleKind(rule);
34
54
  }
35
55
  return Object.freeze({
36
- checks: Object.freeze(checks),
37
- presences: Object.freeze(presences),
38
- conditionalPresences: Object.freeze(conditionalPresences),
39
- gates: Object.freeze(gates),
40
- transforms: Object.freeze(transforms),
41
- composites: Object.freeze(composites),
42
- recursions: Object.freeze(recursions),
56
+ checks: freezeRules(checks),
57
+ presences: freezeRules(presences),
58
+ conditionalPresences: freezeRules(conditionalPresences),
59
+ gates: freezeRules(gates),
60
+ transforms: freezeRules(transforms),
61
+ composites: freezeRules(composites),
62
+ recursions: freezeRules(recursions),
43
63
  });
44
64
  }
45
65
  /** The exhaustiveness proof: the parameter is `never`, so an eighth kind is a
@@ -65,6 +65,18 @@ export interface RecursionPolicy {
65
65
  }
66
66
  export interface CompiledField {
67
67
  readonly template: readonly PathSegment[];
68
+ /**
69
+ * This field's own path, rendered without array indices.
70
+ *
71
+ * Rendered once, at compile time. The template is fixed, so rebuilding it
72
+ * per call meant **producing the same string every time** — once per field
73
+ * per element, whether or not a single issue was reported, and it was a
74
+ * large share of the per-element cost.
75
+ *
76
+ * What remains at validation time is one concatenation with the prefix
77
+ * holding the open indices.
78
+ */
79
+ readonly renderedPath: string;
68
80
  readonly read: (subject: unknown) => unknown;
69
81
  /**
70
82
  * null unless the field declared a transform or a default — the runtime then
@@ -77,6 +89,11 @@ export interface CompiledField {
77
89
  readonly write: ValueWriter | null;
78
90
  readonly defaultOf: ((root: unknown) => unknown) | null;
79
91
  readonly applyDefaultToNull: boolean;
92
+ /**
93
+ * null unless the field declared one. Runs after the default and BEFORE
94
+ * presence, on a value that is neither undefined nor null.
95
+ */
96
+ readonly normalize: ((value: unknown) => unknown) | null;
80
97
  readonly presence: PresencePolicy;
81
98
  /**
82
99
  * The conditional overrides of `presence`, in declaration order, and the
@@ -95,6 +112,18 @@ export interface CompiledField {
95
112
  /** Loop interchange: one array is read once however many element fields exist. */
96
113
  export interface ArrayNode {
97
114
  readonly template: readonly PathSegment[];
115
+ /**
116
+ * The node's own path, rendered once at build time — `lines`, never
117
+ * `lines[*]`, because the grouping already cut the wildcard off.
118
+ *
119
+ * CompiledField has carried its `renderedPath` since rebuilding it per
120
+ * element was measured at 35% of the per-element price. The array NODE was
121
+ * left behind and went on rendering the same string on every validate().
122
+ * Rendering can throw on a template with an unconsumed wildcard, so moving
123
+ * it here also moves that throw from validate() to build(), which is the
124
+ * direction this layer is supposed to push everything.
125
+ */
126
+ readonly renderedPath: string;
98
127
  readonly read: (subject: unknown) => unknown;
99
128
  readonly elementFields: readonly CompiledField[];
100
129
  readonly nested: readonly ArrayNode[];
@@ -104,6 +133,12 @@ export interface ValidationPlan {
104
133
  readonly arrays: readonly ArrayNode[];
105
134
  readonly hasTransforms: boolean;
106
135
  readonly hasDefaults: boolean;
136
+ /**
137
+ * A normalizer substitutes a value the same way a default does, so the
138
+ * writer must exist for it too. Kept as its own flag rather than folded
139
+ * into hasDefaults, which would make that name say something it does not.
140
+ */
141
+ readonly hasNormalizers: boolean;
107
142
  }
108
143
  /**
109
144
  * What the chain collector hands compile: one path, its ordered rules, and the
@@ -120,4 +155,5 @@ export interface FieldDeclaration {
120
155
  readonly rules: readonly Rule[];
121
156
  readonly defaultOf?: (root: unknown) => unknown;
122
157
  readonly applyDefaultToNull?: boolean;
158
+ readonly normalize?: (value: unknown) => unknown;
123
159
  }
@@ -1,42 +1,48 @@
1
1
  /**
2
- * 理由: 動的にキーを積み上げて組み立てたレコードは、実行時にはキーが揃って
3
- * いても静的には Record<string, unknown> 止まりになる。呼び出し側のジェネリク
4
- * ス(AsyncContextBuilder.set `C & { [P in K]: V }`)が、そのキーと値の対応
5
- * を型として保証している。
2
+ * Why: a record built up key by key has all its keys at run time but stays
3
+ * Record<string, unknown> statically. The generic on the calling side —
4
+ * AsyncContextBuilder.set's `C & { [P in K]: V }` — is what holds the
5
+ * key-to-value correspondence as a type.
6
6
  */
7
7
  export declare function eraseAssembledRecord<T extends object>(assembled: Readonly<Record<string, unknown>>): T;
8
8
  /**
9
- * 理由: チェーンの実体は「バッグに入っているプラグインの数だけメソッドを生やした
10
- * レコード」で、実行時にはキーが揃っていても静的には Record<string, unknown> 止まり
11
- * になる。対応する型 FieldSlots / FieldChain SlotPlugins によるマップ型なので、
12
- * 「どのキーが生えるか」は型引数 B S からしか決まらず、値の組み立て側では書けない。
13
- * 正しさは attachSlotMethods が SlotPlugins と同じ規則(plugin.slots に S を含む
14
- * プラグインの plugin.method だけを生やす)で組み立てていることに依存する。
9
+ * Why: a chain is a record carrying one method per plugin in the bag. The
10
+ * keys are all there at run time but the value stays Record<string, unknown>
11
+ * statically, while the matching types are mapped types — which keys appear
12
+ * follows from the type arguments alone and cannot be written where the value
13
+ * is assembled.
14
+ *
15
+ * Soundness depends on the assembly following the same rule the mapped type
16
+ * does: attach a method for a plugin exactly when that plugin declares the
17
+ * slot.
15
18
  */
16
19
  export declare function eraseChainSurface<T extends object>(assembled: Readonly<Record<string, unknown>>): T;
17
20
  /**
18
- * 理由: ビルダー連鎖の実体は「use で積んだプラグイン」「v で積んだ宣言」を持つ
19
- * 1 つのレコードで、実行時には段が進んでも同じ形のまま変わらない。一方その静的
20
- * な型は、段ごとの型引数(バッグの交差 B & BagEntry<P>、宣言済みパスの和
21
- * TDeclared | K、union guard の網羅で分岐する条件型)でしか書けず、組み立て側に
22
- * はその型を書く手段が無い。同じ理由で、L5 createValidator はプランしか知らな
23
- * いので ValidationResult<unknown> しか返せず、宣言された T を戻せるのはこの
24
- * 境界だけである。
25
- * 正しさは、erased 側(src/builder/builder-surface.types.ts)が宣言型と同じ
26
- * メンバー集合を型として持ち、実装がそれに構造的に適合していることに依存する。
27
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/builder/create-builder.ts)だけ。
21
+ * Why: a builder chain is one record holding the plugins added by use() and
22
+ * the declarations added by v(), and its shape does not change as the chain
23
+ * advances. Its static type does — each step is expressed with type arguments
24
+ * (the bag intersection, the union of declared paths, the conditional that
25
+ * branches on union guard exhaustiveness) that the assembling code has no way
26
+ * to write. For the same reason the runtime knows only the plan and can only
27
+ * answer ValidationResult<unknown>; this boundary is the one place the
28
+ * declared T can be put back.
29
+ *
30
+ * Soundness depends on the erased surface declaring the same member set as
31
+ * the public type, with the implementation structurally conforming to it.
32
+ * Exactly one call site is allowed.
28
33
  */
29
34
  export declare function eraseBuilderSurface<T extends object>(assembled: object): T;
30
35
  /**
31
- * 理由: fromJsonSchema<T>() は「実行時に読み込んだ JSON Schema」から検証器を作る。
32
- * ドキュメントは実行時の値なので、宣言された T との対応をコンパイラが検査できる
33
- * 材料は原理的に存在しない。L5 PlanBackedValidator はプランしか知らず
34
- * ValidationResult<unknown> しか返せないので、呼び出し側が明示した T を戻せるのは
35
- * この境界だけである eraseBuilderSurface と同じ性質の消去であり、同じ場所に置く。
36
- * 正しさは、PlanBackedValidator(src/builder/builder-surface.types.ts)が
37
- * Validator<T> と同じメンバー集合を持ち、実装がそれに構造的に適合していることに
38
- * 依存する。T が実際のドキュメントと食い違っていた場合、型は嘘をつくが実行時の
39
- * 検証結果は正しい: build-from-schema.ts の見出しがその逃げ道を明記している。
40
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/json-schema/build-from-schema.ts)だけ。
36
+ * Why: building a validator from a JSON Schema read at run time gives the
37
+ * compiler nothing to check the declared T against — the document is a value,
38
+ * not a type. The plan-backed validator can only answer
39
+ * ValidationResult<unknown>, so this boundary is the one place the caller's
40
+ * explicit T can be put back. Same kind of erasure as the builder surface,
41
+ * hence the same home.
42
+ *
43
+ * Soundness depends on the plan-backed validator declaring the same member
44
+ * set as Validator<T>, with the implementation structurally conforming to it.
45
+ * When T disagrees with the actual document the type lies, but the runtime
46
+ * result is still correct. Exactly one call site is allowed.
41
47
  */
42
48
  export declare function eraseSchemaValidator<T>(planBacked: object): T;
@@ -12,51 +12,57 @@ exports.eraseChainSurface = eraseChainSurface;
12
12
  exports.eraseBuilderSurface = eraseBuilderSurface;
13
13
  exports.eraseSchemaValidator = eraseSchemaValidator;
14
14
  /**
15
- * 理由: 動的にキーを積み上げて組み立てたレコードは、実行時にはキーが揃って
16
- * いても静的には Record<string, unknown> 止まりになる。呼び出し側のジェネリク
17
- * ス(AsyncContextBuilder.set `C & { [P in K]: V }`)が、そのキーと値の対応
18
- * を型として保証している。
15
+ * Why: a record built up key by key has all its keys at run time but stays
16
+ * Record<string, unknown> statically. The generic on the calling side —
17
+ * AsyncContextBuilder.set's `C & { [P in K]: V }` — is what holds the
18
+ * key-to-value correspondence as a type.
19
19
  */
20
20
  function eraseAssembledRecord(assembled) {
21
21
  return assembled;
22
22
  }
23
23
  /**
24
- * 理由: チェーンの実体は「バッグに入っているプラグインの数だけメソッドを生やした
25
- * レコード」で、実行時にはキーが揃っていても静的には Record<string, unknown> 止まり
26
- * になる。対応する型 FieldSlots / FieldChain SlotPlugins によるマップ型なので、
27
- * 「どのキーが生えるか」は型引数 B S からしか決まらず、値の組み立て側では書けない。
28
- * 正しさは attachSlotMethods が SlotPlugins と同じ規則(plugin.slots に S を含む
29
- * プラグインの plugin.method だけを生やす)で組み立てていることに依存する。
24
+ * Why: a chain is a record carrying one method per plugin in the bag. The
25
+ * keys are all there at run time but the value stays Record<string, unknown>
26
+ * statically, while the matching types are mapped types — which keys appear
27
+ * follows from the type arguments alone and cannot be written where the value
28
+ * is assembled.
29
+ *
30
+ * Soundness depends on the assembly following the same rule the mapped type
31
+ * does: attach a method for a plugin exactly when that plugin declares the
32
+ * slot.
30
33
  */
31
34
  function eraseChainSurface(assembled) {
32
35
  return assembled;
33
36
  }
34
37
  /**
35
- * 理由: ビルダー連鎖の実体は「use で積んだプラグイン」「v で積んだ宣言」を持つ
36
- * 1 つのレコードで、実行時には段が進んでも同じ形のまま変わらない。一方その静的
37
- * な型は、段ごとの型引数(バッグの交差 B & BagEntry<P>、宣言済みパスの和
38
- * TDeclared | K、union guard の網羅で分岐する条件型)でしか書けず、組み立て側に
39
- * はその型を書く手段が無い。同じ理由で、L5 createValidator はプランしか知らな
40
- * いので ValidationResult<unknown> しか返せず、宣言された T を戻せるのはこの
41
- * 境界だけである。
42
- * 正しさは、erased 側(src/builder/builder-surface.types.ts)が宣言型と同じ
43
- * メンバー集合を型として持ち、実装がそれに構造的に適合していることに依存する。
44
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/builder/create-builder.ts)だけ。
38
+ * Why: a builder chain is one record holding the plugins added by use() and
39
+ * the declarations added by v(), and its shape does not change as the chain
40
+ * advances. Its static type does — each step is expressed with type arguments
41
+ * (the bag intersection, the union of declared paths, the conditional that
42
+ * branches on union guard exhaustiveness) that the assembling code has no way
43
+ * to write. For the same reason the runtime knows only the plan and can only
44
+ * answer ValidationResult<unknown>; this boundary is the one place the
45
+ * declared T can be put back.
46
+ *
47
+ * Soundness depends on the erased surface declaring the same member set as
48
+ * the public type, with the implementation structurally conforming to it.
49
+ * Exactly one call site is allowed.
45
50
  */
46
51
  function eraseBuilderSurface(assembled) {
47
52
  return assembled;
48
53
  }
49
54
  /**
50
- * 理由: fromJsonSchema<T>() は「実行時に読み込んだ JSON Schema」から検証器を作る。
51
- * ドキュメントは実行時の値なので、宣言された T との対応をコンパイラが検査できる
52
- * 材料は原理的に存在しない。L5 PlanBackedValidator はプランしか知らず
53
- * ValidationResult<unknown> しか返せないので、呼び出し側が明示した T を戻せるのは
54
- * この境界だけである eraseBuilderSurface と同じ性質の消去であり、同じ場所に置く。
55
- * 正しさは、PlanBackedValidator(src/builder/builder-surface.types.ts)が
56
- * Validator<T> と同じメンバー集合を持ち、実装がそれに構造的に適合していることに
57
- * 依存する。T が実際のドキュメントと食い違っていた場合、型は嘘をつくが実行時の
58
- * 検証結果は正しい: build-from-schema.ts の見出しがその逃げ道を明記している。
59
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/json-schema/build-from-schema.ts)だけ。
55
+ * Why: building a validator from a JSON Schema read at run time gives the
56
+ * compiler nothing to check the declared T against — the document is a value,
57
+ * not a type. The plan-backed validator can only answer
58
+ * ValidationResult<unknown>, so this boundary is the one place the caller's
59
+ * explicit T can be put back. Same kind of erasure as the builder surface,
60
+ * hence the same home.
61
+ *
62
+ * Soundness depends on the plan-backed validator declaring the same member
63
+ * set as Validator<T>, with the implementation structurally conforming to it.
64
+ * When T disagrees with the actual document the type lies, but the runtime
65
+ * result is still correct. Exactly one call site is allowed.
60
66
  */
61
67
  function eraseSchemaValidator(planBacked) {
62
68
  return planBacked;