@maroonedog/luq 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) 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.d.ts +10 -0
  25. package/dist/chain/create-chain-node.js +19 -9
  26. package/dist/chain/create-chain-node.mjs +19 -9
  27. package/dist/chain/create-field-slots.js +9 -1
  28. package/dist/chain/create-field-slots.mjs +9 -1
  29. package/dist/chain/declaration-recorder.port.d.ts +31 -0
  30. package/dist/chain/declaration-recorder.port.js +17 -0
  31. package/dist/chain/declaration-recorder.port.mjs +13 -0
  32. package/dist/chain/declared-call.types.d.ts +15 -0
  33. package/dist/chain/declared-call.types.js +2 -0
  34. package/dist/chain/declared-call.types.mjs +1 -0
  35. package/dist/chain/index.d.ts +3 -1
  36. package/dist/chain/slot-type-guard.d.ts +10 -0
  37. package/dist/chain/slot-type-guard.js +37 -0
  38. package/dist/chain/slot-type-guard.mjs +34 -0
  39. package/dist/compile/compile-array-node.js +1 -0
  40. package/dist/compile/compile-array-node.mjs +1 -0
  41. package/dist/compile/compile-field.d.ts +1 -0
  42. package/dist/compile/compile-field.js +8 -6
  43. package/dist/compile/compile-field.mjs +8 -6
  44. package/dist/compile/compile-schema.js +4 -0
  45. package/dist/compile/compile-schema.mjs +4 -0
  46. package/dist/compile/group-array-fields.d.ts +1 -0
  47. package/dist/compile/split-rules-by-kind.js +10 -14
  48. package/dist/compile/split-rules-by-kind.mjs +10 -14
  49. package/dist/compile/validation-plan.types.d.ts +19 -6
  50. package/dist/core/type-erasure.d.ts +36 -30
  51. package/dist/core/type-erasure.js +36 -30
  52. package/dist/core/type-erasure.mjs +36 -30
  53. package/dist/json-schema/build-from-schema.js +8 -1
  54. package/dist/json-schema/build-from-schema.mjs +8 -1
  55. package/dist/json-schema/create-structural-context.js +3 -0
  56. package/dist/json-schema/create-structural-context.mjs +3 -0
  57. package/dist/json-schema/declare-additional-properties.d.ts +7 -7
  58. package/dist/json-schema/declare-additional-properties.js +7 -7
  59. package/dist/json-schema/declare-additional-properties.mjs +7 -7
  60. package/dist/json-schema/declare-object-keywords.js +4 -4
  61. package/dist/json-schema/declare-object-keywords.mjs +4 -4
  62. package/dist/json-schema/follow-json-pointer.d.ts +7 -6
  63. package/dist/json-schema/follow-json-pointer.js +24 -24
  64. package/dist/json-schema/follow-json-pointer.mjs +24 -24
  65. package/dist/json-schema/ref-resolution-error.js +3 -3
  66. package/dist/json-schema/ref-resolution-error.mjs +3 -3
  67. package/dist/json-schema/schema-registry.js +12 -11
  68. package/dist/json-schema/schema-registry.mjs +12 -11
  69. package/dist/json-schema/uri-reference.js +12 -12
  70. package/dist/json-schema/uri-reference.mjs +12 -12
  71. package/dist/path/create-value-writer.js +12 -12
  72. package/dist/path/create-value-writer.mjs +12 -12
  73. package/dist/path/reserved-segment.d.ts +16 -16
  74. package/dist/path/reserved-segment.js +17 -21
  75. package/dist/path/reserved-segment.mjs +17 -21
  76. package/dist/plugins/index.generated.js +2 -2
  77. package/dist/plugins/index.generated.mjs +2 -2
  78. package/dist/plugins/manifest.generated.js +2 -2
  79. package/dist/plugins/manifest.generated.mjs +2 -2
  80. package/dist/plugins/object/object.d.ts +6 -0
  81. package/dist/plugins/object/object.js +9 -11
  82. package/dist/plugins/object/object.mjs +10 -12
  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 +10 -14
  87. package/dist/plugins/stitch-with/stitch-with.d.ts +1 -1
  88. package/dist/plugins/stitch-with/stitch-with.js +22 -24
  89. package/dist/plugins/stitch-with/stitch-with.mjs +22 -24
  90. package/dist/plugins/string-min/string-min.js +6 -8
  91. package/dist/plugins/string-min/string-min.mjs +6 -8
  92. package/dist/presets/presets.d.ts +10 -11
  93. package/dist/presets/presets.js +22 -23
  94. package/dist/presets/presets.mjs +22 -23
  95. package/dist/runtime/create-field-validator.js +4 -6
  96. package/dist/runtime/create-field-validator.mjs +4 -6
  97. package/dist/runtime/create-validator.js +11 -11
  98. package/dist/runtime/create-validator.mjs +11 -11
  99. package/dist/runtime/output-writer.js +5 -1
  100. package/dist/runtime/output-writer.mjs +5 -1
  101. package/dist/runtime/run-array-node.js +6 -6
  102. package/dist/runtime/run-array-node.mjs +6 -6
  103. package/dist/runtime/run-field.js +18 -19
  104. package/dist/runtime/run-field.mjs +18 -19
  105. package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
  106. package/dist/standard-schema/assemble-json-schema.js +95 -0
  107. package/dist/standard-schema/assemble-json-schema.mjs +92 -0
  108. package/dist/standard-schema/declaration-recorder.d.ts +6 -0
  109. package/dist/standard-schema/declaration-recorder.js +30 -0
  110. package/dist/standard-schema/declaration-recorder.mjs +27 -0
  111. package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
  112. package/dist/standard-schema/declarations-unavailable-error.js +32 -0
  113. package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
  114. package/dist/standard-schema/emit-field-schema.d.ts +9 -0
  115. package/dist/standard-schema/emit-field-schema.js +68 -0
  116. package/dist/standard-schema/emit-field-schema.mjs +65 -0
  117. package/dist/standard-schema/index.d.ts +5 -0
  118. package/dist/standard-schema/index.js +9 -1
  119. package/dist/standard-schema/index.mjs +4 -0
  120. package/dist/standard-schema/json-schema-target.d.ts +6 -0
  121. package/dist/standard-schema/json-schema-target.js +44 -0
  122. package/dist/standard-schema/json-schema-target.mjs +39 -0
  123. package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
  124. package/dist/standard-schema/plugin-keyword-map.js +93 -0
  125. package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
  126. package/dist/standard-schema/split-issue-path.d.ts +6 -4
  127. package/dist/standard-schema/split-issue-path.js +15 -13
  128. package/dist/standard-schema/split-issue-path.mjs +15 -13
  129. package/dist/standard-schema/standard-schema.types.d.ts +8 -7
  130. package/dist/standard-schema/standard-schema.types.js +6 -6
  131. package/dist/standard-schema/standard-schema.types.mjs +6 -6
  132. package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
  133. package/dist/standard-schema/to-standard-json-schema.js +36 -0
  134. package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
  135. package/dist/standard-schema/to-standard-schema.d.ts +16 -15
  136. package/dist/standard-schema/to-standard-schema.js +15 -22
  137. package/dist/standard-schema/to-standard-schema.mjs +15 -22
  138. package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
  139. package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
  140. package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
  141. package/dist/types/index.d.ts +12 -12
  142. package/dist/types/index.js +7 -7
  143. package/dist/types/index.mjs +7 -7
  144. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { eraseChainSurface } from "../core/type-erasure.mjs";
2
2
  import { createChainNode, EMPTY_RULES, } from "./create-chain-node.mjs";
3
3
  import { resolvePluginArguments } from "./collect-branch-rules.mjs";
4
+ import { slotTypeGuard } from "./slot-type-guard.mjs";
4
5
  const SLOT_NAMES = Object.freeze([
5
6
  "string",
6
7
  "number",
@@ -21,9 +22,16 @@ export function buildSlotSurface(bag, context) {
21
22
  resolveArguments: (plugin, declared) => resolvePluginArguments(bag, context, buildSlotSurface, plugin, declared),
22
23
  };
23
24
  for (const slot of SLOT_NAMES) {
25
+ // The slot's own type check leads the chain, so it runs before any value
26
+ // rule. Those rules pass a wrong-typed value through on purpose; this is
27
+ // what reports it. See slot-type-guard.ts.
28
+ const guard = context.typeDecidedElsewhere === true
29
+ ? null
30
+ : slotTypeGuard(slot, context.config.defaultSeverity);
31
+ const seed = guard === null ? EMPTY_RULES : Object.freeze([guard]);
24
32
  Object.defineProperty(surface, slot, {
25
33
  enumerable: true,
26
- get: () => createChainNode(wiring, slot, EMPTY_RULES),
34
+ get: () => createChainNode(wiring, slot, seed),
27
35
  });
28
36
  }
29
37
  return surface;
@@ -0,0 +1,31 @@
1
+ import type { TypeName } from "../types";
2
+ import type { AnyPlugin } from "../plugin-kit/plugin-definition";
3
+ import type { DeclaredCall } from "./declared-call.types";
4
+ /**
5
+ * Only the parent-to-child relation between nodes crosses this line; how the
6
+ * record is held is the implementation's business. Nothing passed here is
7
+ * built for the occasion — the plugin, the slot and the arguments are all
8
+ * already in hand for building the rule.
9
+ */
10
+ export interface DeclarationRecorder {
11
+ /**
12
+ * `child`'s record is `parent`'s with one call appended. Called once, by
13
+ * whoever made the node, immediately after freezing it.
14
+ */
15
+ record(parent: object, child: object, plugin: AnyPlugin, slot: TypeName, args: readonly unknown[]): void;
16
+ /** A refine step adds no call. It carries the record across unchanged. */
17
+ inherit(parent: object, child: object): void;
18
+ /** The calls declared up to that node, or undefined when there are none. */
19
+ read(node: object): readonly DeclaredCall[] | undefined;
20
+ }
21
+ /**
22
+ * Null until someone installs. Exposed as the binding rather than behind a
23
+ * getter because that measured smaller: with no installer reachable, what
24
+ * survives minification is one `let` and an optional chain.
25
+ */
26
+ export declare let declarationRecorder: DeclarationRecorder | null;
27
+ /**
28
+ * Called once, when the implementation is loaded. A second call overwrites,
29
+ * which is harmless while there is one implementation.
30
+ */
31
+ export declare function installDeclarationRecorder(recorder: DeclarationRecorder | null): void;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.declarationRecorder = void 0;
4
+ exports.installDeclarationRecorder = installDeclarationRecorder;
5
+ /**
6
+ * Null until someone installs. Exposed as the binding rather than behind a
7
+ * getter because that measured smaller: with no installer reachable, what
8
+ * survives minification is one `let` and an optional chain.
9
+ */
10
+ exports.declarationRecorder = null;
11
+ /**
12
+ * Called once, when the implementation is loaded. A second call overwrites,
13
+ * which is harmless while there is one implementation.
14
+ */
15
+ function installDeclarationRecorder(recorder) {
16
+ exports.declarationRecorder = recorder;
17
+ }
@@ -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";
@@ -0,0 +1,10 @@
1
+ import type { IssueSeverity, TypeName } from "../types";
2
+ import type { Rule } from "../plugin-kit/compiled-rule";
3
+ /**
4
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
5
+ * nothing about the runtime type.
6
+ *
7
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
8
+ * plugin name it would have had, matching every other code in the library.
9
+ */
10
+ export declare function slotTypeGuard(slot: TypeName, severity: IssueSeverity): Rule | null;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slotTypeGuard = slotTypeGuard;
4
+ const types_1 = require("../types");
5
+ const create_rule_1 = require("../plugin-kit/create-rule");
6
+ const SLOT_TYPES = Object.freeze({
7
+ string: { accepts: types_1.isString, noun: "a string" },
8
+ number: { accepts: types_1.isNumber, noun: "a number" },
9
+ boolean: {
10
+ accepts: (value) => typeof value === "boolean",
11
+ noun: "a boolean",
12
+ },
13
+ date: { accepts: (value) => value instanceof Date, noun: "a Date" },
14
+ array: { accepts: types_1.isArray, noun: "an array" },
15
+ object: { accepts: types_1.isPlainObject, noun: "an object" },
16
+ });
17
+ /**
18
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
19
+ * nothing about the runtime type.
20
+ *
21
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
22
+ * plugin name it would have had, matching every other code in the library.
23
+ */
24
+ function slotTypeGuard(slot, severity) {
25
+ const slotType = SLOT_TYPES[slot];
26
+ if (slotType === undefined)
27
+ return null;
28
+ return (0, create_rule_1.check)({
29
+ code: `${slot}Type`,
30
+ severity,
31
+ run: (value) => value === undefined || value === null || slotType.accepts(value)
32
+ ? types_1.PASS
33
+ : (0, types_1.fail)({ expected: slotType.noun, actual: value }),
34
+ describe: () => `Value must be ${slotType.noun}`,
35
+ buildMessageContext: () => ({}),
36
+ });
37
+ }
@@ -0,0 +1,34 @@
1
+ import { PASS, fail, isArray, isNumber, isPlainObject, isString, } from "../types/index.mjs";
2
+ import { check } from "../plugin-kit/create-rule.mjs";
3
+ const SLOT_TYPES = Object.freeze({
4
+ string: { accepts: isString, noun: "a string" },
5
+ number: { accepts: isNumber, noun: "a number" },
6
+ boolean: {
7
+ accepts: (value) => typeof value === "boolean",
8
+ noun: "a boolean",
9
+ },
10
+ date: { accepts: (value) => value instanceof Date, noun: "a Date" },
11
+ array: { accepts: isArray, noun: "an array" },
12
+ object: { accepts: isPlainObject, noun: "an object" },
13
+ });
14
+ /**
15
+ * The rule `b.<slot>` starts its chain with, or null for a slot that claims
16
+ * nothing about the runtime type.
17
+ *
18
+ * The code is `<slot>Type` — `stringType`, `numberType` — which reads as the
19
+ * plugin name it would have had, matching every other code in the library.
20
+ */
21
+ export function slotTypeGuard(slot, severity) {
22
+ const slotType = SLOT_TYPES[slot];
23
+ if (slotType === undefined)
24
+ return null;
25
+ return check({
26
+ code: `${slot}Type`,
27
+ severity,
28
+ run: (value) => value === undefined || value === null || slotType.accepts(value)
29
+ ? PASS
30
+ : fail({ expected: slotType.noun, actual: value }),
31
+ describe: () => `Value must be ${slotType.noun}`,
32
+ buildMessageContext: () => ({}),
33
+ });
34
+ }
@@ -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;