@maroonedog/luq 2.2.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 (133) hide show
  1. package/README.md +47 -602
  2. package/dist/builder/compile-declarations.d.ts +7 -1
  3. package/dist/builder/compile-declarations.js +18 -8
  4. package/dist/builder/compile-declarations.mjs +18 -8
  5. package/dist/builder/create-builder.js +2 -3
  6. package/dist/builder/create-builder.mjs +2 -3
  7. package/dist/builder/create-field-builder.js +13 -1
  8. package/dist/builder/create-field-builder.mjs +13 -1
  9. package/dist/builder/declared-calls-store.d.ts +9 -0
  10. package/dist/builder/declared-calls-store.js +19 -0
  11. package/dist/builder/declared-calls-store.mjs +15 -0
  12. package/dist/builder/field-declared-calls.types.d.ts +6 -0
  13. package/dist/builder/field-declared-calls.types.js +2 -0
  14. package/dist/builder/field-declared-calls.types.mjs +1 -0
  15. package/dist/builder/field-entry.types.d.ts +9 -3
  16. package/dist/builder/field-options.types.d.ts +26 -0
  17. package/dist/chain/bundle-paths.types.d.ts +9 -12
  18. package/dist/chain/chain-node-store.d.ts +5 -0
  19. package/dist/chain/chain-node-store.js +15 -0
  20. package/dist/chain/chain-node-store.mjs +11 -0
  21. package/dist/chain/collect-field-rules.d.ts +13 -2
  22. package/dist/chain/collect-field-rules.js +10 -3
  23. package/dist/chain/collect-field-rules.mjs +10 -3
  24. package/dist/chain/create-chain-node.js +19 -9
  25. package/dist/chain/create-chain-node.mjs +19 -9
  26. package/dist/chain/declaration-recorder.port.d.ts +31 -0
  27. package/dist/chain/declaration-recorder.port.js +17 -0
  28. package/dist/chain/declaration-recorder.port.mjs +13 -0
  29. package/dist/chain/declared-call.types.d.ts +15 -0
  30. package/dist/chain/declared-call.types.js +2 -0
  31. package/dist/chain/declared-call.types.mjs +1 -0
  32. package/dist/chain/index.d.ts +3 -1
  33. package/dist/compile/compile-array-node.js +1 -0
  34. package/dist/compile/compile-array-node.mjs +1 -0
  35. package/dist/compile/compile-field.d.ts +1 -0
  36. package/dist/compile/compile-field.js +8 -6
  37. package/dist/compile/compile-field.mjs +8 -6
  38. package/dist/compile/compile-schema.js +4 -0
  39. package/dist/compile/compile-schema.mjs +4 -0
  40. package/dist/compile/group-array-fields.d.ts +1 -0
  41. package/dist/compile/split-rules-by-kind.js +10 -14
  42. package/dist/compile/split-rules-by-kind.mjs +10 -14
  43. package/dist/compile/validation-plan.types.d.ts +19 -6
  44. package/dist/core/type-erasure.d.ts +36 -30
  45. package/dist/core/type-erasure.js +36 -30
  46. package/dist/core/type-erasure.mjs +36 -30
  47. package/dist/json-schema/build-from-schema.js +8 -1
  48. package/dist/json-schema/build-from-schema.mjs +8 -1
  49. package/dist/json-schema/declare-additional-properties.d.ts +7 -7
  50. package/dist/json-schema/declare-additional-properties.js +7 -7
  51. package/dist/json-schema/declare-additional-properties.mjs +7 -7
  52. package/dist/json-schema/declare-object-keywords.js +4 -4
  53. package/dist/json-schema/declare-object-keywords.mjs +4 -4
  54. package/dist/json-schema/follow-json-pointer.d.ts +7 -6
  55. package/dist/json-schema/follow-json-pointer.js +24 -24
  56. package/dist/json-schema/follow-json-pointer.mjs +24 -24
  57. package/dist/json-schema/ref-resolution-error.js +3 -3
  58. package/dist/json-schema/ref-resolution-error.mjs +3 -3
  59. package/dist/json-schema/schema-registry.js +12 -11
  60. package/dist/json-schema/schema-registry.mjs +12 -11
  61. package/dist/json-schema/uri-reference.js +12 -12
  62. package/dist/json-schema/uri-reference.mjs +12 -12
  63. package/dist/path/create-value-writer.js +12 -12
  64. package/dist/path/create-value-writer.mjs +12 -12
  65. package/dist/path/reserved-segment.d.ts +16 -16
  66. package/dist/path/reserved-segment.js +17 -21
  67. package/dist/path/reserved-segment.mjs +17 -21
  68. package/dist/plugins/index.generated.js +2 -2
  69. package/dist/plugins/index.generated.mjs +2 -2
  70. package/dist/plugins/manifest.generated.js +2 -2
  71. package/dist/plugins/manifest.generated.mjs +2 -2
  72. package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +8 -8
  73. package/dist/plugins/object-additional-properties/select-additional-keys.js +16 -16
  74. package/dist/plugins/object-additional-properties/select-additional-keys.mjs +16 -16
  75. package/dist/plugins/stitch/stitch.d.ts +10 -14
  76. package/dist/plugins/stitch-with/stitch-with.d.ts +1 -1
  77. package/dist/plugins/stitch-with/stitch-with.js +22 -24
  78. package/dist/plugins/stitch-with/stitch-with.mjs +22 -24
  79. package/dist/plugins/string-min/string-min.js +6 -8
  80. package/dist/plugins/string-min/string-min.mjs +6 -8
  81. package/dist/presets/presets.d.ts +10 -11
  82. package/dist/presets/presets.js +22 -23
  83. package/dist/presets/presets.mjs +22 -23
  84. package/dist/runtime/create-field-validator.js +4 -6
  85. package/dist/runtime/create-field-validator.mjs +4 -6
  86. package/dist/runtime/create-validator.js +11 -11
  87. package/dist/runtime/create-validator.mjs +11 -11
  88. package/dist/runtime/output-writer.js +5 -1
  89. package/dist/runtime/output-writer.mjs +5 -1
  90. package/dist/runtime/run-array-node.js +6 -6
  91. package/dist/runtime/run-array-node.mjs +6 -6
  92. package/dist/runtime/run-field.js +18 -19
  93. package/dist/runtime/run-field.mjs +18 -19
  94. package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
  95. package/dist/standard-schema/assemble-json-schema.js +95 -0
  96. package/dist/standard-schema/assemble-json-schema.mjs +92 -0
  97. package/dist/standard-schema/declaration-recorder.d.ts +6 -0
  98. package/dist/standard-schema/declaration-recorder.js +30 -0
  99. package/dist/standard-schema/declaration-recorder.mjs +27 -0
  100. package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
  101. package/dist/standard-schema/declarations-unavailable-error.js +32 -0
  102. package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
  103. package/dist/standard-schema/emit-field-schema.d.ts +9 -0
  104. package/dist/standard-schema/emit-field-schema.js +68 -0
  105. package/dist/standard-schema/emit-field-schema.mjs +65 -0
  106. package/dist/standard-schema/index.d.ts +5 -0
  107. package/dist/standard-schema/index.js +9 -1
  108. package/dist/standard-schema/index.mjs +4 -0
  109. package/dist/standard-schema/json-schema-target.d.ts +6 -0
  110. package/dist/standard-schema/json-schema-target.js +44 -0
  111. package/dist/standard-schema/json-schema-target.mjs +39 -0
  112. package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
  113. package/dist/standard-schema/plugin-keyword-map.js +93 -0
  114. package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
  115. package/dist/standard-schema/split-issue-path.d.ts +6 -4
  116. package/dist/standard-schema/split-issue-path.js +15 -13
  117. package/dist/standard-schema/split-issue-path.mjs +15 -13
  118. package/dist/standard-schema/standard-schema.types.d.ts +8 -7
  119. package/dist/standard-schema/standard-schema.types.js +6 -6
  120. package/dist/standard-schema/standard-schema.types.mjs +6 -6
  121. package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
  122. package/dist/standard-schema/to-standard-json-schema.js +36 -0
  123. package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
  124. package/dist/standard-schema/to-standard-schema.d.ts +16 -15
  125. package/dist/standard-schema/to-standard-schema.js +15 -22
  126. package/dist/standard-schema/to-standard-schema.mjs +15 -22
  127. package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
  128. package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
  129. package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
  130. package/dist/types/index.d.ts +12 -12
  131. package/dist/types/index.js +7 -7
  132. package/dist/types/index.mjs +7 -7
  133. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Null until someone installs. Exposed as the binding rather than behind a
3
+ * getter because that measured smaller: with no installer reachable, what
4
+ * survives minification is one `let` and an optional chain.
5
+ */
6
+ export let declarationRecorder = null;
7
+ /**
8
+ * Called once, when the implementation is loaded. A second call overwrites,
9
+ * which is harmless while there is one implementation.
10
+ */
11
+ export function installDeclarationRecorder(recorder) {
12
+ declarationRecorder = recorder;
13
+ }
@@ -0,0 +1,15 @@
1
+ import type { TypeName } from "../types";
2
+ /** One call of one chain method. */
3
+ export interface DeclaredCall {
4
+ readonly pluginName: string;
5
+ readonly method: string;
6
+ readonly slot: TypeName;
7
+ /**
8
+ * The arguments as declared, after argument resolution and nothing else.
9
+ *
10
+ * They are not converted: a RegExp stays a RegExp. How a given output format
11
+ * spells a value is that writer's business, and converting here would make
12
+ * everyone pay for a conversion only some of them want.
13
+ */
14
+ readonly args: readonly unknown[];
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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";
@@ -27,6 +27,7 @@ function compileRelativeDeclaration(declaration, context) {
27
27
  fieldPath: declaration.fieldPath,
28
28
  defaultOf: declaration.defaultOf,
29
29
  applyDefaultToNull: declaration.applyDefaultToNull,
30
+ normalize: declaration.normalize,
30
31
  planRef: context.planRef,
31
32
  eraseComposite: context.eraseComposite,
32
33
  });
@@ -23,6 +23,7 @@ export function compileRelativeDeclaration(declaration, context) {
23
23
  fieldPath: declaration.fieldPath,
24
24
  defaultOf: declaration.defaultOf,
25
25
  applyDefaultToNull: declaration.applyDefaultToNull,
26
+ normalize: declaration.normalize,
26
27
  planRef: context.planRef,
27
28
  eraseComposite: context.eraseComposite,
28
29
  });
@@ -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
  }
@@ -13,17 +13,17 @@ const resolve_recursion_1 = require("./resolve-recursion");
13
13
  const split_rules_by_kind_1 = require("./split-rules-by-kind");
14
14
  /** Whether a field with no explicit setting replaces a null with its default. */
15
15
  exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
16
- /** 添字はここでは入らない。開いている添字は実行時に接頭辞が持つ。 */
16
+ /** No indices here: an open index is carried by the run-time prefix. */
17
17
  const NO_INDICES = Object.freeze([]);
18
18
  function compileField(request) {
19
19
  const byKind = (0, split_rules_by_kind_1.splitRulesByKind)(request.rules);
20
20
  const hasDefault = request.defaultOf !== null;
21
- 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;
22
23
  const template = Object.freeze(request.template);
23
- // read を先に作る。ワイルドカードを含むテンプレートを拒むのは
24
- // createValueReader の役目で、それより先に formatIssuePath を呼ぶと
25
- // 「添字が足りない」という RangeError が、本来の PathSyntaxError
26
- // 追い越して出てしまう (テストがそれを捕まえた)。
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
27
  const read = (0, create_value_reader_1.createValueReader)(template);
28
28
  const field = {
29
29
  template,
@@ -32,6 +32,7 @@ function compileField(request) {
32
32
  write: needsWriter ? (0, create_value_writer_1.createValueWriter)(template) : null,
33
33
  defaultOf: request.defaultOf,
34
34
  applyDefaultToNull: request.applyDefaultToNull,
35
+ normalize: request.normalize,
35
36
  presence: (0, resolve_presence_1.resolvePresence)(byKind.presences),
36
37
  presenceOverrides: (0, resolve_conditional_presence_1.resolveConditionalPresence)(byKind.conditionalPresences),
37
38
  gates: byKind.gates,
@@ -54,6 +55,7 @@ function compileFieldDeclaration(declaration, planRef, eraseComposite) {
54
55
  fieldPath: declaration.path,
55
56
  defaultOf: declaration.defaultOf ?? null,
56
57
  applyDefaultToNull: declaration.applyDefaultToNull ?? exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
58
+ normalize: declaration.normalize ?? null,
57
59
  planRef,
58
60
  eraseComposite,
59
61
  });
@@ -8,17 +8,17 @@ import { resolveRecursion } from "./resolve-recursion.mjs";
8
8
  import { splitRulesByKind } from "./split-rules-by-kind.mjs";
9
9
  /** Whether a field with no explicit setting replaces a null with its default. */
10
10
  export const APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
11
- /** 添字はここでは入らない。開いている添字は実行時に接頭辞が持つ。 */
11
+ /** No indices here: an open index is carried by the run-time prefix. */
12
12
  const NO_INDICES = Object.freeze([]);
13
13
  export function compileField(request) {
14
14
  const byKind = splitRulesByKind(request.rules);
15
15
  const hasDefault = request.defaultOf !== null;
16
- 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;
17
18
  const template = Object.freeze(request.template);
18
- // read を先に作る。ワイルドカードを含むテンプレートを拒むのは
19
- // createValueReader の役目で、それより先に formatIssuePath を呼ぶと
20
- // 「添字が足りない」という RangeError が、本来の PathSyntaxError
21
- // 追い越して出てしまう (テストがそれを捕まえた)。
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
22
  const read = createValueReader(template);
23
23
  const field = {
24
24
  template,
@@ -27,6 +27,7 @@ export function compileField(request) {
27
27
  write: needsWriter ? createValueWriter(template) : null,
28
28
  defaultOf: request.defaultOf,
29
29
  applyDefaultToNull: request.applyDefaultToNull,
30
+ normalize: request.normalize,
30
31
  presence: resolvePresence(byKind.presences),
31
32
  presenceOverrides: resolveConditionalPresence(byKind.conditionalPresences),
32
33
  gates: byKind.gates,
@@ -49,6 +50,7 @@ export function compileFieldDeclaration(declaration, planRef, eraseComposite) {
49
50
  fieldPath: declaration.path,
50
51
  defaultOf: declaration.defaultOf ?? null,
51
52
  applyDefaultToNull: declaration.applyDefaultToNull ?? APPLIES_DEFAULT_TO_NULL_BY_DEFAULT,
53
+ normalize: declaration.normalize ?? null,
52
54
  planRef,
53
55
  eraseComposite,
54
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 {
@@ -12,25 +12,21 @@ class UnknownRuleKindError extends Error {
12
12
  }
13
13
  exports.UnknownRuleKindError = UnknownRuleKindError;
14
14
  /**
15
- * 空の束は一つを共有する。
15
+ * Every empty bucket is the same object.
16
16
  *
17
- * 七つの種のうち、どのフィールドもたいてい二つか三つしか使わない。残りは
18
- * 空配列だが、フィールドごとに新しく Object.freeze([]) を作っていたので、
19
- * 空であること自体は同じなのにマップの同一性だけが全部違っていた。実行時の
20
- * `field.gates.length` `field.transforms` の読み出しは、フィールドを
21
- * またぐたびに別の受け手を見ることになる。
22
- *
23
- * この置き換えは src/compile/resolve-conditional-presence.ts の
24
- * NO_PRESENCE_OVERRIDES と src/runtime/output-writer.ts の NO_WRITE_TARGETS が
25
- * 既にやっていることを、残りの種にも広げただけである。
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.
26
21
  */
27
22
  const NO_RULES_OF_THIS_KIND = Object.freeze([]);
28
23
  /**
29
- * 空なら共有の一つを返す。中身があるならその配列を凍結して返す。
24
+ * Returns the shared empty for an empty bucket, and the frozen array
25
+ * otherwise.
30
26
  *
31
- * `readonly never[]` はどの `readonly R[]` にも代入できるので、共有する
32
- * ためにアサーションを書く必要はない。型アサーションを書いてよいのは
33
- * src/core/type-erasure.ts だけである。
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.
34
30
  */
35
31
  function freezeRules(rules) {
36
32
  return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
@@ -7,25 +7,21 @@ export class UnknownRuleKindError extends Error {
7
7
  }
8
8
  }
9
9
  /**
10
- * 空の束は一つを共有する。
10
+ * Every empty bucket is the same object.
11
11
  *
12
- * 七つの種のうち、どのフィールドもたいてい二つか三つしか使わない。残りは
13
- * 空配列だが、フィールドごとに新しく Object.freeze([]) を作っていたので、
14
- * 空であること自体は同じなのにマップの同一性だけが全部違っていた。実行時の
15
- * `field.gates.length` `field.transforms` の読み出しは、フィールドを
16
- * またぐたびに別の受け手を見ることになる。
17
- *
18
- * この置き換えは src/compile/resolve-conditional-presence.ts の
19
- * NO_PRESENCE_OVERRIDES と src/runtime/output-writer.ts の NO_WRITE_TARGETS が
20
- * 既にやっていることを、残りの種にも広げただけである。
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.
21
16
  */
22
17
  const NO_RULES_OF_THIS_KIND = Object.freeze([]);
23
18
  /**
24
- * 空なら共有の一つを返す。中身があるならその配列を凍結して返す。
19
+ * Returns the shared empty for an empty bucket, and the frozen array
20
+ * otherwise.
25
21
  *
26
- * `readonly never[]` はどの `readonly R[]` にも代入できるので、共有する
27
- * ためにアサーションを書く必要はない。型アサーションを書いてよいのは
28
- * src/core/type-erasure.ts だけである。
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.
29
25
  */
30
26
  function freezeRules(rules) {
31
27
  return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
@@ -66,14 +66,15 @@ export interface RecursionPolicy {
66
66
  export interface CompiledField {
67
67
  readonly template: readonly PathSegment[];
68
68
  /**
69
- * この フィールド自身のパスを、配列の添字を除いて描画したもの。
69
+ * This field's own path, rendered without array indices.
70
70
  *
71
- * コンパイル時に一度だけ作る。実行時に組み直していたのは、テンプレートが
72
- * 固定なので **毎回同じ文字列を作る** ことを意味していた: 50要素・3
73
- * フィールドの配列なら、1回の validate 150 回、issue が1件も出なくても
74
- * である。実測でそれが要素あたりの費用の 35% を占めていた。
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
75
  *
76
- * 実行時に残るのは、開いている添字の接頭辞と繋ぐ連結ひとつだけになる。
76
+ * What remains at validation time is one concatenation with the prefix
77
+ * holding the open indices.
77
78
  */
78
79
  readonly renderedPath: string;
79
80
  readonly read: (subject: unknown) => unknown;
@@ -88,6 +89,11 @@ export interface CompiledField {
88
89
  readonly write: ValueWriter | null;
89
90
  readonly defaultOf: ((root: unknown) => unknown) | null;
90
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;
91
97
  readonly presence: PresencePolicy;
92
98
  /**
93
99
  * The conditional overrides of `presence`, in declaration order, and the
@@ -127,6 +133,12 @@ export interface ValidationPlan {
127
133
  readonly arrays: readonly ArrayNode[];
128
134
  readonly hasTransforms: boolean;
129
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;
130
142
  }
131
143
  /**
132
144
  * What the chain collector hands compile: one path, its ordered rules, and the
@@ -143,4 +155,5 @@ export interface FieldDeclaration {
143
155
  readonly rules: readonly Rule[];
144
156
  readonly defaultOf?: (root: unknown) => unknown;
145
157
  readonly applyDefaultToNull?: boolean;
158
+ readonly normalize?: (value: unknown) => unknown;
146
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;
@@ -6,51 +6,57 @@
6
6
  // the whole escape hatch in one file.
7
7
  // ===========================================================================
8
8
  /**
9
- * 理由: 動的にキーを積み上げて組み立てたレコードは、実行時にはキーが揃って
10
- * いても静的には Record<string, unknown> 止まりになる。呼び出し側のジェネリク
11
- * ス(AsyncContextBuilder.set `C & { [P in K]: V }`)が、そのキーと値の対応
12
- * を型として保証している。
9
+ * Why: a record built up key by key has all its keys at run time but stays
10
+ * Record<string, unknown> statically. The generic on the calling side —
11
+ * AsyncContextBuilder.set's `C & { [P in K]: V }` — is what holds the
12
+ * key-to-value correspondence as a type.
13
13
  */
14
14
  export function eraseAssembledRecord(assembled) {
15
15
  return assembled;
16
16
  }
17
17
  /**
18
- * 理由: チェーンの実体は「バッグに入っているプラグインの数だけメソッドを生やした
19
- * レコード」で、実行時にはキーが揃っていても静的には Record<string, unknown> 止まり
20
- * になる。対応する型 FieldSlots / FieldChain SlotPlugins によるマップ型なので、
21
- * 「どのキーが生えるか」は型引数 B S からしか決まらず、値の組み立て側では書けない。
22
- * 正しさは attachSlotMethods が SlotPlugins と同じ規則(plugin.slots に S を含む
23
- * プラグインの plugin.method だけを生やす)で組み立てていることに依存する。
18
+ * Why: a chain is a record carrying one method per plugin in the bag. The
19
+ * keys are all there at run time but the value stays Record<string, unknown>
20
+ * statically, while the matching types are mapped types — which keys appear
21
+ * follows from the type arguments alone and cannot be written where the value
22
+ * is assembled.
23
+ *
24
+ * Soundness depends on the assembly following the same rule the mapped type
25
+ * does: attach a method for a plugin exactly when that plugin declares the
26
+ * slot.
24
27
  */
25
28
  export function eraseChainSurface(assembled) {
26
29
  return assembled;
27
30
  }
28
31
  /**
29
- * 理由: ビルダー連鎖の実体は「use で積んだプラグイン」「v で積んだ宣言」を持つ
30
- * 1 つのレコードで、実行時には段が進んでも同じ形のまま変わらない。一方その静的
31
- * な型は、段ごとの型引数(バッグの交差 B & BagEntry<P>、宣言済みパスの和
32
- * TDeclared | K、union guard の網羅で分岐する条件型)でしか書けず、組み立て側に
33
- * はその型を書く手段が無い。同じ理由で、L5 createValidator はプランしか知らな
34
- * いので ValidationResult<unknown> しか返せず、宣言された T を戻せるのはこの
35
- * 境界だけである。
36
- * 正しさは、erased 側(src/builder/builder-surface.types.ts)が宣言型と同じ
37
- * メンバー集合を型として持ち、実装がそれに構造的に適合していることに依存する。
38
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/builder/create-builder.ts)だけ。
32
+ * Why: a builder chain is one record holding the plugins added by use() and
33
+ * the declarations added by v(), and its shape does not change as the chain
34
+ * advances. Its static type does — each step is expressed with type arguments
35
+ * (the bag intersection, the union of declared paths, the conditional that
36
+ * branches on union guard exhaustiveness) that the assembling code has no way
37
+ * to write. For the same reason the runtime knows only the plan and can only
38
+ * answer ValidationResult<unknown>; this boundary is the one place the
39
+ * declared T can be put back.
40
+ *
41
+ * Soundness depends on the erased surface declaring the same member set as
42
+ * the public type, with the implementation structurally conforming to it.
43
+ * Exactly one call site is allowed.
39
44
  */
40
45
  export function eraseBuilderSurface(assembled) {
41
46
  return assembled;
42
47
  }
43
48
  /**
44
- * 理由: fromJsonSchema<T>() は「実行時に読み込んだ JSON Schema」から検証器を作る。
45
- * ドキュメントは実行時の値なので、宣言された T との対応をコンパイラが検査できる
46
- * 材料は原理的に存在しない。L5 PlanBackedValidator はプランしか知らず
47
- * ValidationResult<unknown> しか返せないので、呼び出し側が明示した T を戻せるのは
48
- * この境界だけである eraseBuilderSurface と同じ性質の消去であり、同じ場所に置く。
49
- * 正しさは、PlanBackedValidator(src/builder/builder-surface.types.ts)が
50
- * Validator<T> と同じメンバー集合を持ち、実装がそれに構造的に適合していることに
51
- * 依存する。T が実際のドキュメントと食い違っていた場合、型は嘘をつくが実行時の
52
- * 検証結果は正しい: build-from-schema.ts の見出しがその逃げ道を明記している。
53
- * src/ 全体でこの関数の呼び出しは 1 箇所(src/json-schema/build-from-schema.ts)だけ。
49
+ * Why: building a validator from a JSON Schema read at run time gives the
50
+ * compiler nothing to check the declared T against — the document is a value,
51
+ * not a type. The plan-backed validator can only answer
52
+ * ValidationResult<unknown>, so this boundary is the one place the caller's
53
+ * explicit T can be put back. Same kind of erasure as the builder surface,
54
+ * hence the same home.
55
+ *
56
+ * Soundness depends on the plan-backed validator declaring the same member
57
+ * set as Validator<T>, with the implementation structurally conforming to it.
58
+ * When T disagrees with the actual document the type lies, but the runtime
59
+ * result is still correct. Exactly one call site is allowed.
54
60
  */
55
61
  export function eraseSchemaValidator(planBacked) {
56
62
  return planBacked;