@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,4 +1,10 @@
1
1
  import type { ValidationPlan } from "../compile/validation-plan.types";
2
2
  import type { GlobalConfig } from "../types/global-config";
3
3
  import type { FieldEntry } from "./field-entry.types";
4
- export declare function compileDeclarations(entries: readonly FieldEntry[], configOverride: GlobalConfig | undefined): ValidationPlan;
4
+ import type { FieldDeclaredCalls } from "./field-declared-calls.types";
5
+ /** What build() makes in one pass: the plan to run, and what was declared. */
6
+ export interface CompiledDeclarations {
7
+ readonly plan: ValidationPlan;
8
+ readonly declaredCalls: readonly FieldDeclaredCalls[];
9
+ }
10
+ export declare function compileDeclarations(entries: readonly FieldEntry[], configOverride: GlobalConfig | undefined): CompiledDeclarations;
@@ -26,15 +26,25 @@ const global_config_store_1 = require("./global-config-store");
26
26
  function compileDeclarations(entries, configOverride) {
27
27
  const config = (0, global_config_1.resolveGlobalConfig)(configOverride, (0, global_config_store_1.getGlobalConfig)());
28
28
  const childKeysOf = (0, declared_child_keys_1.indexDeclaredChildKeys)(entries.map((entry) => entry.path));
29
- const declarations = entries.map((entry) => ({
30
- path: entry.path,
31
- rules: entry.collectRules({
29
+ const declarations = [];
30
+ const declaredCalls = [];
31
+ for (const entry of entries) {
32
+ const outcome = entry.collectRules({
32
33
  fieldPath: entry.path,
33
34
  declaredSiblingKeys: childKeysOf(entry.path),
34
35
  config,
35
- }),
36
- defaultOf: entry.defaultOf ?? undefined,
37
- applyDefaultToNull: entry.applyDefaultToNull,
38
- }));
39
- return (0, compile_schema_1.compileSchema)(declarations, (0, run_branch_1.createBranchExecutor)());
36
+ });
37
+ declarations.push({
38
+ path: entry.path,
39
+ rules: outcome.rules,
40
+ defaultOf: entry.defaultOf ?? undefined,
41
+ applyDefaultToNull: entry.applyDefaultToNull,
42
+ normalize: entry.normalize ?? undefined,
43
+ });
44
+ declaredCalls.push({ path: entry.path, calls: outcome.calls });
45
+ }
46
+ return {
47
+ plan: (0, compile_schema_1.compileSchema)(declarations, (0, run_branch_1.createBranchExecutor)()),
48
+ declaredCalls,
49
+ };
40
50
  }
@@ -23,15 +23,25 @@ import { getGlobalConfig } from "./global-config-store.mjs";
23
23
  export function compileDeclarations(entries, configOverride) {
24
24
  const config = resolveGlobalConfig(configOverride, getGlobalConfig());
25
25
  const childKeysOf = indexDeclaredChildKeys(entries.map((entry) => entry.path));
26
- const declarations = entries.map((entry) => ({
27
- path: entry.path,
28
- rules: entry.collectRules({
26
+ const declarations = [];
27
+ const declaredCalls = [];
28
+ for (const entry of entries) {
29
+ const outcome = entry.collectRules({
29
30
  fieldPath: entry.path,
30
31
  declaredSiblingKeys: childKeysOf(entry.path),
31
32
  config,
32
- }),
33
- defaultOf: entry.defaultOf ?? undefined,
34
- applyDefaultToNull: entry.applyDefaultToNull,
35
- }));
36
- return compileSchema(declarations, createBranchExecutor());
33
+ });
34
+ declarations.push({
35
+ path: entry.path,
36
+ rules: outcome.rules,
37
+ defaultOf: entry.defaultOf ?? undefined,
38
+ applyDefaultToNull: entry.applyDefaultToNull,
39
+ normalize: entry.normalize ?? undefined,
40
+ });
41
+ declaredCalls.push({ path: entry.path, calls: outcome.calls });
42
+ }
43
+ return {
44
+ plan: compileSchema(declarations, createBranchExecutor()),
45
+ declaredCalls,
46
+ };
37
47
  }
@@ -51,9 +51,8 @@ function createBuilderSurface() {
51
51
  return surface;
52
52
  },
53
53
  useAll(plugins) {
54
- // 順序は Object.values の列挙順。first-wins なので、同じ名前が二度
55
- // 来ても最初のものが残る プリセットが既に登録したものを黙って
56
- // 置き換えることはない。
54
+ // In enumeration order, first-wins: a name arriving twice keeps the
55
+ // first one, so a preset never silently replaces what is registered.
57
56
  for (const plugin of Object.values(plugins)) {
58
57
  registerPlugin(registration.plugins, plugin);
59
58
  }
@@ -45,9 +45,8 @@ export function createBuilderSurface() {
45
45
  return surface;
46
46
  },
47
47
  useAll(plugins) {
48
- // 順序は Object.values の列挙順。first-wins なので、同じ名前が二度
49
- // 来ても最初のものが残る プリセットが既に登録したものを黙って
50
- // 置き換えることはない。
48
+ // In enumeration order, first-wins: a name arriving twice keeps the
49
+ // first one, so a preset never silently replaces what is registered.
51
50
  for (const plugin of Object.values(plugins)) {
52
51
  registerPlugin(registration.plugins, plugin);
53
52
  }
@@ -17,9 +17,20 @@ exports.createFieldBuilderSurface = createFieldBuilderSurface;
17
17
  const collect_field_rules_1 = require("../chain/collect-field-rules");
18
18
  const compile_declarations_1 = require("./compile-declarations");
19
19
  const create_plan_validator_1 = require("./create-plan-validator");
20
+ const declared_calls_store_1 = require("./declared-calls-store");
20
21
  const resolve_field_default_1 = require("./resolve-field-default");
21
22
  /** No declaration yet: shared and frozen, so `.for<T>()` allocates nothing. */
22
23
  const NO_ENTRIES = Object.freeze([]);
24
+ /**
25
+ * Builds the plan and attaches whatever was recorded during that same single
26
+ * pass. The attachment is external, so the validator gains no member.
27
+ */
28
+ function buildValidator(entries, configOverride) {
29
+ const compiled = (0, compile_declarations_1.compileDeclarations)(entries, configOverride);
30
+ const validator = (0, create_plan_validator_1.createPlanBackedValidator)(compiled.plan);
31
+ (0, declared_calls_store_1.rememberDeclaredCalls)(validator, compiled.declaredCalls);
32
+ return validator;
33
+ }
23
34
  function createFieldBuilderSurface(bag, configOverride, entries = NO_ENTRIES) {
24
35
  const surface = {
25
36
  v: (path, define, options) => createFieldBuilderSurface(bag, configOverride, [
@@ -27,7 +38,7 @@ function createFieldBuilderSurface(bag, configOverride, entries = NO_ENTRIES) {
27
38
  toFieldEntry(bag, path, define, options),
28
39
  ]),
29
40
  strict: () => surface,
30
- build: () => (0, create_plan_validator_1.createPlanBackedValidator)((0, compile_declarations_1.compileDeclarations)(entries, configOverride)),
41
+ build: () => buildValidator(entries, configOverride),
31
42
  };
32
43
  return Object.freeze(surface);
33
44
  }
@@ -42,6 +53,7 @@ function toFieldEntry(bag, path, define, options) {
42
53
  path,
43
54
  defaultOf: policy.defaultOf,
44
55
  applyDefaultToNull: policy.applyDefaultToNull,
56
+ normalize: options?.normalize ?? null,
45
57
  collectRules: (context) => (0, collect_field_rules_1.collectFieldRules)(bag, context, define),
46
58
  };
47
59
  }
@@ -14,9 +14,20 @@
14
14
  import { collectFieldRules } from "../chain/collect-field-rules.mjs";
15
15
  import { compileDeclarations } from "./compile-declarations.mjs";
16
16
  import { createPlanBackedValidator } from "./create-plan-validator.mjs";
17
+ import { rememberDeclaredCalls } from "./declared-calls-store.mjs";
17
18
  import { resolveFieldDefault } from "./resolve-field-default.mjs";
18
19
  /** No declaration yet: shared and frozen, so `.for<T>()` allocates nothing. */
19
20
  const NO_ENTRIES = Object.freeze([]);
21
+ /**
22
+ * Builds the plan and attaches whatever was recorded during that same single
23
+ * pass. The attachment is external, so the validator gains no member.
24
+ */
25
+ function buildValidator(entries, configOverride) {
26
+ const compiled = compileDeclarations(entries, configOverride);
27
+ const validator = createPlanBackedValidator(compiled.plan);
28
+ rememberDeclaredCalls(validator, compiled.declaredCalls);
29
+ return validator;
30
+ }
20
31
  export function createFieldBuilderSurface(bag, configOverride, entries = NO_ENTRIES) {
21
32
  const surface = {
22
33
  v: (path, define, options) => createFieldBuilderSurface(bag, configOverride, [
@@ -24,7 +35,7 @@ export function createFieldBuilderSurface(bag, configOverride, entries = NO_ENTR
24
35
  toFieldEntry(bag, path, define, options),
25
36
  ]),
26
37
  strict: () => surface,
27
- build: () => createPlanBackedValidator(compileDeclarations(entries, configOverride)),
38
+ build: () => buildValidator(entries, configOverride),
28
39
  };
29
40
  return Object.freeze(surface);
30
41
  }
@@ -39,6 +50,7 @@ function toFieldEntry(bag, path, define, options) {
39
50
  path,
40
51
  defaultOf: policy.defaultOf,
41
52
  applyDefaultToNull: policy.applyDefaultToNull,
53
+ normalize: options?.normalize ?? null,
42
54
  collectRules: (context) => collectFieldRules(bag, context, define),
43
55
  };
44
56
  }
@@ -0,0 +1,9 @@
1
+ import type { FieldDeclaredCalls } from "./field-declared-calls.types";
2
+ /** Called only by build(), attaching to an already frozen validator. */
3
+ export declare function rememberDeclaredCalls(validator: object, calls: readonly FieldDeclaredCalls[]): void;
4
+ /**
5
+ * undefined means this validator did not come from build(). Keeping it
6
+ * distinct from the empty list is the point: it lets a writer tell "no
7
+ * constraints were declared" from "what was declared is not known".
8
+ */
9
+ export declare function readDeclaredCalls(validator: unknown): readonly FieldDeclaredCalls[] | undefined;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rememberDeclaredCalls = rememberDeclaredCalls;
4
+ exports.readDeclaredCalls = readDeclaredCalls;
5
+ const declaredCallsByValidator = new WeakMap();
6
+ /** Called only by build(), attaching to an already frozen validator. */
7
+ function rememberDeclaredCalls(validator, calls) {
8
+ declaredCallsByValidator.set(validator, calls);
9
+ }
10
+ /**
11
+ * undefined means this validator did not come from build(). Keeping it
12
+ * distinct from the empty list is the point: it lets a writer tell "no
13
+ * constraints were declared" from "what was declared is not known".
14
+ */
15
+ function readDeclaredCalls(validator) {
16
+ if (typeof validator !== "object" || validator === null)
17
+ return undefined;
18
+ return declaredCallsByValidator.get(validator);
19
+ }
@@ -0,0 +1,15 @@
1
+ const declaredCallsByValidator = new WeakMap();
2
+ /** Called only by build(), attaching to an already frozen validator. */
3
+ export function rememberDeclaredCalls(validator, calls) {
4
+ declaredCallsByValidator.set(validator, calls);
5
+ }
6
+ /**
7
+ * undefined means this validator did not come from build(). Keeping it
8
+ * distinct from the empty list is the point: it lets a writer tell "no
9
+ * constraints were declared" from "what was declared is not known".
10
+ */
11
+ export function readDeclaredCalls(validator) {
12
+ if (typeof validator !== "object" || validator === null)
13
+ return undefined;
14
+ return declaredCallsByValidator.get(validator);
15
+ }
@@ -0,0 +1,6 @@
1
+ import type { DeclaredCall } from "../chain/declared-call.types";
2
+ export interface FieldDeclaredCalls {
3
+ readonly path: string;
4
+ /** null means no record was kept; the empty list means nothing was declared. */
5
+ readonly calls: readonly DeclaredCall[] | null;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,16 @@
1
1
  import type { ChainBuildContext } from "../chain/create-chain-node";
2
- import type { Rule } from "../plugin-kit/compiled-rule";
2
+ import type { FieldChainOutcome } from "../chain/collect-field-rules";
3
+ import type { FieldNormalizer } from "./field-options.types";
3
4
  export interface FieldEntry {
4
5
  readonly path: string;
5
6
  /** null unless `.v()`'s third argument declared a default. */
6
7
  readonly defaultOf: ((root: unknown) => unknown) | null;
7
8
  readonly applyDefaultToNull: boolean;
8
- /** Runs the user's chain callback ONCE and returns its ordered rules. */
9
- collectRules(context: ChainBuildContext): readonly Rule[];
9
+ /** null unless `.v()`'s third argument declared a normalizer. */
10
+ readonly normalize: FieldNormalizer | null;
11
+ /**
12
+ * Runs the user's chain callback ONCE and returns its ordered rules,
13
+ * together with what was declared to produce them.
14
+ */
15
+ collectRules(context: ChainBuildContext): FieldChainOutcome;
10
16
  }
@@ -1,5 +1,14 @@
1
1
  /** The lazy form of a default. A zero-argument function is assignable to it. */
2
2
  export type DefaultFactory<TValue> = (root: unknown) => TValue;
3
+ /**
4
+ * Tidies a value before anything judges it. See `normalize` below.
5
+ *
6
+ * Both sides are `unknown` on purpose: the input has not been validated yet,
7
+ * and a form puts a string in a numeric field, so `"42"` → `42` is the main
8
+ * use of this layer. Typing it `(value: TValue) => TValue` would be a lie.
9
+ * What comes back out is judged by the rules, not by the type.
10
+ */
11
+ export type FieldNormalizer = (value: unknown) => unknown;
3
12
  export interface FieldOptions<TValue> {
4
13
  /**
5
14
  * Substituted before ANY rule looks at the value, so validate() and parse()
@@ -8,4 +17,21 @@ export interface FieldOptions<TValue> {
8
17
  readonly default?: TValue | DefaultFactory<TValue>;
9
18
  /** Defaults to true — a declared null is replaced, matching 1.x. */
10
19
  readonly applyDefaultToNull?: boolean;
20
+ /**
21
+ * Tidies the value before anything judges it. Runs straight after
22
+ * `default` and before presence is decided.
23
+ *
24
+ * Same promise as `default`: validate() and parse() judge the same value,
25
+ * and only parse() writes it back, so the two can never disagree.
26
+ *
27
+ * **Never called for undefined or null.** Otherwise
28
+ * `(v) => String(v).trim()` would turn a missing field into the string
29
+ * `"undefined"` and let it past `.required()`. Absence is `default`'s
30
+ * business; this one only ever sees a value that is there, which is why a
31
+ * normalizer needs no null check of its own.
32
+ *
33
+ * That ordering is what makes the common case work: `" "` → trim → `""`
34
+ * → presence reads the empty string as missing → required reports it.
35
+ */
36
+ readonly normalize?: FieldNormalizer;
11
37
  }
@@ -3,24 +3,21 @@ import type { ValueAtPath } from "../path/value-at-path.types";
3
3
  import type { AnyChain } from "./field-chain.types";
4
4
  import type { FieldSlots } from "./field-slots.types";
5
5
  import type { PluginBag } from "./plugin-bag.types";
6
- /** 別名 -> ルートのパス。パスはルートに実在するものしか書けない。 */
6
+ /** Alias to a path from the root; only paths that exist there are writable. */
7
7
  export type BundlePaths<TRoot> = Readonly<Record<string, FieldPath<TRoot> & string>>;
8
- /** 対応表から組み上がる束。別名がキーで、値はそのパスの値。 */
8
+ /** The bundle built from the table: aliases as keys, the path's value as value. */
9
9
  export type BundleOf<TRoot, M extends BundlePaths<TRoot>> = {
10
10
  readonly [A in keyof M & string]: ValueAtPath<TRoot, M[A]>;
11
11
  };
12
12
  /**
13
- * 束ひとつに対するサブチェーン。
13
+ * The sub-chain for one bundle.
14
14
  *
15
- * stitch の核は「複数のフィールドを **1つの判定** にまとめる」ことなので、
16
- * 主体は束そのものであって別名ごとではない。別名ごとにルールを並べる形も
17
- * 書けるが、それは `total === price * quantity` のような判定が書けず、
18
- * stitch ではなくなる。
15
+ * The subject is the bundle itself, not each alias: bringing several fields
16
+ * into ONE judgement is the point, and a per-alias form cannot express
17
+ * `total === price * quantity`.
19
18
  *
20
- * 主体が束なので、束の中を `.v()` 的に見るのではなく、`b.object` などの
21
- * スロットがそのまま開く。型は BundleOf なので、束のメンバーは補完も効くし
22
- * 取り違えればコンパイルエラーになる stitch
23
- * `Readonly<Record<string, unknown>>` を渡していたのに対する、ここの一点だけ
24
- * が違いである。
19
+ * Because the subject is the bundle, the slots (`b.object` and friends) open
20
+ * on it directly rather than descending into it. The bundle is typed, so its
21
+ * members complete and mistaking one fails to compile.
25
22
  */
26
23
  export type BundleChain<TRoot, M extends BundlePaths<TRoot>, B extends PluginBag> = (b: FieldSlots<BundleOf<TRoot, M>, B, BundleOf<TRoot, M>>) => AnyChain;
@@ -0,0 +1,5 @@
1
+ import type { Rule } from "../plugin-kit/compiled-rule";
2
+ /** Called only by whoever made the node, once, just before freezing it. */
3
+ export declare function rememberChainNode(node: object, rules: readonly Rule[]): void;
4
+ /** The one way back out of a chain: undefined for anything that is not a node. */
5
+ export declare function readChainNode(candidate: unknown): readonly Rule[] | undefined;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rememberChainNode = rememberChainNode;
4
+ exports.readChainNode = readChainNode;
5
+ const rulesByNode = new WeakMap();
6
+ /** Called only by whoever made the node, once, just before freezing it. */
7
+ function rememberChainNode(node, rules) {
8
+ rulesByNode.set(node, rules);
9
+ }
10
+ /** The one way back out of a chain: undefined for anything that is not a node. */
11
+ function readChainNode(candidate) {
12
+ if (typeof candidate !== "object" || candidate === null)
13
+ return undefined;
14
+ return rulesByNode.get(candidate);
15
+ }
@@ -0,0 +1,11 @@
1
+ const rulesByNode = new WeakMap();
2
+ /** Called only by whoever made the node, once, just before freezing it. */
3
+ export function rememberChainNode(node, rules) {
4
+ rulesByNode.set(node, rules);
5
+ }
6
+ /** The one way back out of a chain: undefined for anything that is not a node. */
7
+ export function readChainNode(candidate) {
8
+ if (typeof candidate !== "object" || candidate === null)
9
+ return undefined;
10
+ return rulesByNode.get(candidate);
11
+ }
@@ -2,9 +2,20 @@ import type { Rule } from "../plugin-kit/compiled-rule";
2
2
  import type { PluginBag } from "./plugin-bag.types";
3
3
  import type { AnyChain } from "./field-chain.types";
4
4
  import type { FieldSlots } from "./field-slots.types";
5
- import { type ChainBuildContext } from "./create-chain-node";
5
+ import type { ChainBuildContext } from "./create-chain-node";
6
+ import type { DeclaredCall } from "./declared-call.types";
7
+ /** What the single run produced: rules for the runtime, calls for a writer. */
8
+ export interface FieldChainOutcome {
9
+ readonly rules: readonly Rule[];
10
+ /**
11
+ * null means no record was kept, which is not the empty list's "nothing was
12
+ * declared". Collapsing the two lets a writer return a schema with no
13
+ * constraints and no idea that it is missing them.
14
+ */
15
+ readonly calls: readonly DeclaredCall[] | null;
16
+ }
6
17
  export declare class FieldChainResultError extends Error {
7
18
  readonly fieldPath: string;
8
19
  constructor(fieldPath: string);
9
20
  }
10
- export declare function collectFieldRules<TRoot, B extends PluginBag, TField>(bag: B, context: ChainBuildContext, define: (b: FieldSlots<TRoot, B, TField>) => AnyChain): readonly Rule[];
21
+ export declare function collectFieldRules<TRoot, B extends PluginBag, TField>(bag: B, context: ChainBuildContext, define: (b: FieldSlots<TRoot, B, TField>) => AnyChain): FieldChainOutcome;
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FieldChainResultError = void 0;
4
4
  exports.collectFieldRules = collectFieldRules;
5
- const create_chain_node_1 = require("./create-chain-node");
5
+ const chain_node_store_1 = require("./chain-node-store");
6
+ const declaration_recorder_port_1 = require("./declaration-recorder.port");
6
7
  const create_field_slots_1 = require("./create-field-slots");
7
8
  class FieldChainResultError extends Error {
8
9
  constructor(fieldPath) {
@@ -15,8 +16,14 @@ class FieldChainResultError extends Error {
15
16
  exports.FieldChainResultError = FieldChainResultError;
16
17
  function collectFieldRules(bag, context, define) {
17
18
  const chain = define((0, create_field_slots_1.createFieldSlots)(bag, context));
18
- const rules = (0, create_chain_node_1.readChainRules)(chain);
19
+ const rules = (0, chain_node_store_1.readChainNode)(chain);
19
20
  if (rules === undefined)
20
21
  throw new FieldChainResultError(context.fieldPath);
21
- return Object.freeze(rules.slice());
22
+ const recorder = declaration_recorder_port_1.declarationRecorder;
23
+ return Object.freeze({
24
+ rules: Object.freeze(rules.slice()),
25
+ calls: recorder === null
26
+ ? null
27
+ : Object.freeze((recorder.read(chain) ?? []).slice()),
28
+ });
22
29
  }
@@ -1,4 +1,5 @@
1
- import { readChainRules } from "./create-chain-node.mjs";
1
+ import { readChainNode } from "./chain-node-store.mjs";
2
+ import { declarationRecorder } from "./declaration-recorder.port.mjs";
2
3
  import { createFieldSlots } from "./create-field-slots.mjs";
3
4
  export class FieldChainResultError extends Error {
4
5
  constructor(fieldPath) {
@@ -10,8 +11,14 @@ export class FieldChainResultError extends Error {
10
11
  }
11
12
  export function collectFieldRules(bag, context, define) {
12
13
  const chain = define(createFieldSlots(bag, context));
13
- const rules = readChainRules(chain);
14
+ const rules = readChainNode(chain);
14
15
  if (rules === undefined)
15
16
  throw new FieldChainResultError(context.fieldPath);
16
- return Object.freeze(rules.slice());
17
+ const recorder = declarationRecorder;
18
+ return Object.freeze({
19
+ rules: Object.freeze(rules.slice()),
20
+ calls: recorder === null
21
+ ? null
22
+ : Object.freeze((recorder.read(chain) ?? []).slice()),
23
+ });
17
24
  }
@@ -9,6 +9,16 @@ export interface ChainBuildContext {
9
9
  readonly fieldPath: string;
10
10
  readonly declaredSiblingKeys: readonly string[];
11
11
  readonly config: ResolvedGlobalConfig;
12
+ /**
13
+ * True when something other than the slot already decides the runtime type.
14
+ *
15
+ * Set by the JSON Schema converter and nowhere else: there the DOCUMENT owns
16
+ * the type, its `type` keyword reports under the code `type`, and its
17
+ * vocabulary is wider than a slot's — `integer`, a list of types, and `null`
18
+ * as a type in its own right. A slot guard on that path would report a
19
+ * second issue for one bad value, under a code no schema asked for.
20
+ */
21
+ readonly typeDecidedElsewhere?: boolean;
12
22
  }
13
23
  /**
14
24
  * The parts a node cannot make for itself. `resolveArguments` is injected so
@@ -20,12 +20,18 @@ exports.readChainRules = readChainRules;
20
20
  // The rule list is held in a WeakMap rather than on the node, so the node
21
21
  // carries exactly the members its type declares and reading the rules back
22
22
  // needs no assertion and no runtime shape check.
23
+ //
24
+ // A node does NOT carry what was called with what. This file builds no record
25
+ // of the call; it only notifies whoever asked to be told, and with nobody
26
+ // asking that is one null check per chain step. See
27
+ // declaration-recorder.port.ts.
23
28
  // ===========================================================================
24
29
  const types_1 = require("../types");
25
30
  const create_rule_1 = require("../plugin-kit/create-rule");
26
31
  const refine_methods_types_1 = require("./refine-methods.types");
27
32
  const attach_slot_methods_1 = require("./attach-slot-methods");
28
- const chainRulesByNode = new WeakMap();
33
+ const declaration_recorder_port_1 = require("./declaration-recorder.port");
34
+ const chain_node_store_1 = require("./chain-node-store");
29
35
  const EMPTY_RECORD = Object.freeze({});
30
36
  exports.EMPTY_RULES = Object.freeze([]);
31
37
  function isIssueSeverity(value) {
@@ -70,7 +76,7 @@ function nullIsAValue(severity) {
70
76
  buildMessageContext: () => ({}),
71
77
  });
72
78
  }
73
- function createSlotMethod(wiring, slot, rules, plugin) {
79
+ function createSlotMethod(wiring, parent, slot, rules, plugin) {
74
80
  const arity = Math.max(plugin.build.length - 1, 0);
75
81
  return (...args) => {
76
82
  const rawOptions = args.length > arity ? args[arity] : undefined;
@@ -79,25 +85,29 @@ function createSlotMethod(wiring, slot, rules, plugin) {
79
85
  const added = plugin.judgesNull === true
80
86
  ? [nullIsAValue(wiring.context.config.defaultSeverity), rule]
81
87
  : [rule];
82
- return createChainNode(wiring, slot, [...rules, ...added]);
88
+ const child = createChainNode(wiring, slot, [...rules, ...added]);
89
+ declaration_recorder_port_1.declarationRecorder?.record(parent, child, plugin, slot, resolved);
90
+ return child;
83
91
  };
84
92
  }
85
93
  function attachRefineMethods(target, wiring, rules) {
86
94
  for (const [methodName, slot] of Object.entries(refine_methods_types_1.REFINE_METHOD_SLOTS)) {
87
- target[methodName] = () => createChainNode(wiring, slot, rules);
95
+ target[methodName] = () => {
96
+ const child = createChainNode(wiring, slot, rules);
97
+ declaration_recorder_port_1.declarationRecorder?.inherit(target, child);
98
+ return child;
99
+ };
88
100
  }
89
101
  }
90
102
  /** The runtime value behind FieldChain. Erased to its type by its callers. */
91
103
  function createChainNode(wiring, slot, rules) {
92
104
  const node = {};
93
105
  attachRefineMethods(node, wiring, rules);
94
- (0, attach_slot_methods_1.attachSlotMethods)(node, wiring.bag, slot, (plugin) => createSlotMethod(wiring, slot, rules, plugin));
95
- chainRulesByNode.set(node, rules);
106
+ (0, attach_slot_methods_1.attachSlotMethods)(node, wiring.bag, slot, (plugin) => createSlotMethod(wiring, node, slot, rules, plugin));
107
+ (0, chain_node_store_1.rememberChainNode)(node, rules);
96
108
  return Object.freeze(node);
97
109
  }
98
110
  /** The one way back out of a chain: undefined when the value is not a node. */
99
111
  function readChainRules(candidate) {
100
- if (typeof candidate !== "object" || candidate === null)
101
- return undefined;
102
- return chainRulesByNode.get(candidate);
112
+ return (0, chain_node_store_1.readChainNode)(candidate);
103
113
  }
@@ -15,12 +15,18 @@
15
15
  // The rule list is held in a WeakMap rather than on the node, so the node
16
16
  // carries exactly the members its type declares and reading the rules back
17
17
  // needs no assertion and no runtime shape check.
18
+ //
19
+ // A node does NOT carry what was called with what. This file builds no record
20
+ // of the call; it only notifies whoever asked to be told, and with nobody
21
+ // asking that is one null check per chain step. See
22
+ // declaration-recorder.port.ts.
18
23
  // ===========================================================================
19
24
  import { isPlainObject, isString } from "../types/index.mjs";
20
25
  import { presence } from "../plugin-kit/create-rule.mjs";
21
26
  import { REFINE_METHOD_SLOTS } from "./refine-methods.types.mjs";
22
27
  import { attachSlotMethods } from "./attach-slot-methods.mjs";
23
- const chainRulesByNode = new WeakMap();
28
+ import { declarationRecorder } from "./declaration-recorder.port.mjs";
29
+ import { readChainNode, rememberChainNode } from "./chain-node-store.mjs";
24
30
  const EMPTY_RECORD = Object.freeze({});
25
31
  export const EMPTY_RULES = Object.freeze([]);
26
32
  function isIssueSeverity(value) {
@@ -65,7 +71,7 @@ function nullIsAValue(severity) {
65
71
  buildMessageContext: () => ({}),
66
72
  });
67
73
  }
68
- function createSlotMethod(wiring, slot, rules, plugin) {
74
+ function createSlotMethod(wiring, parent, slot, rules, plugin) {
69
75
  const arity = Math.max(plugin.build.length - 1, 0);
70
76
  return (...args) => {
71
77
  const rawOptions = args.length > arity ? args[arity] : undefined;
@@ -74,25 +80,29 @@ function createSlotMethod(wiring, slot, rules, plugin) {
74
80
  const added = plugin.judgesNull === true
75
81
  ? [nullIsAValue(wiring.context.config.defaultSeverity), rule]
76
82
  : [rule];
77
- return createChainNode(wiring, slot, [...rules, ...added]);
83
+ const child = createChainNode(wiring, slot, [...rules, ...added]);
84
+ declarationRecorder?.record(parent, child, plugin, slot, resolved);
85
+ return child;
78
86
  };
79
87
  }
80
88
  function attachRefineMethods(target, wiring, rules) {
81
89
  for (const [methodName, slot] of Object.entries(REFINE_METHOD_SLOTS)) {
82
- target[methodName] = () => createChainNode(wiring, slot, rules);
90
+ target[methodName] = () => {
91
+ const child = createChainNode(wiring, slot, rules);
92
+ declarationRecorder?.inherit(target, child);
93
+ return child;
94
+ };
83
95
  }
84
96
  }
85
97
  /** The runtime value behind FieldChain. Erased to its type by its callers. */
86
98
  export function createChainNode(wiring, slot, rules) {
87
99
  const node = {};
88
100
  attachRefineMethods(node, wiring, rules);
89
- attachSlotMethods(node, wiring.bag, slot, (plugin) => createSlotMethod(wiring, slot, rules, plugin));
90
- chainRulesByNode.set(node, rules);
101
+ attachSlotMethods(node, wiring.bag, slot, (plugin) => createSlotMethod(wiring, node, slot, rules, plugin));
102
+ rememberChainNode(node, rules);
91
103
  return Object.freeze(node);
92
104
  }
93
105
  /** The one way back out of a chain: undefined when the value is not a node. */
94
106
  export function readChainRules(candidate) {
95
- if (typeof candidate !== "object" || candidate === null)
96
- return undefined;
97
- return chainRulesByNode.get(candidate);
107
+ return readChainNode(candidate);
98
108
  }
@@ -5,6 +5,7 @@ exports.createFieldSlots = createFieldSlots;
5
5
  const type_erasure_1 = require("../core/type-erasure");
6
6
  const create_chain_node_1 = require("./create-chain-node");
7
7
  const collect_branch_rules_1 = require("./collect-branch-rules");
8
+ const slot_type_guard_1 = require("./slot-type-guard");
8
9
  const SLOT_NAMES = Object.freeze([
9
10
  "string",
10
11
  "number",
@@ -25,9 +26,16 @@ function buildSlotSurface(bag, context) {
25
26
  resolveArguments: (plugin, declared) => (0, collect_branch_rules_1.resolvePluginArguments)(bag, context, buildSlotSurface, plugin, declared),
26
27
  };
27
28
  for (const slot of SLOT_NAMES) {
29
+ // The slot's own type check leads the chain, so it runs before any value
30
+ // rule. Those rules pass a wrong-typed value through on purpose; this is
31
+ // what reports it. See slot-type-guard.ts.
32
+ const guard = context.typeDecidedElsewhere === true
33
+ ? null
34
+ : (0, slot_type_guard_1.slotTypeGuard)(slot, context.config.defaultSeverity);
35
+ const seed = guard === null ? create_chain_node_1.EMPTY_RULES : Object.freeze([guard]);
28
36
  Object.defineProperty(surface, slot, {
29
37
  enumerable: true,
30
- get: () => (0, create_chain_node_1.createChainNode)(wiring, slot, create_chain_node_1.EMPTY_RULES),
38
+ get: () => (0, create_chain_node_1.createChainNode)(wiring, slot, seed),
31
39
  });
32
40
  }
33
41
  return surface;