@maroonedog/luq 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +92 -430
  2. package/dist/builder/builder-surface.types.d.ts +1 -0
  3. package/dist/builder/compile-declarations.d.ts +7 -1
  4. package/dist/builder/compile-declarations.js +18 -8
  5. package/dist/builder/compile-declarations.mjs +18 -8
  6. package/dist/builder/create-builder.js +8 -0
  7. package/dist/builder/create-builder.mjs +8 -0
  8. package/dist/builder/create-field-builder.js +13 -1
  9. package/dist/builder/create-field-builder.mjs +13 -1
  10. package/dist/builder/declared-calls-store.d.ts +9 -0
  11. package/dist/builder/declared-calls-store.js +19 -0
  12. package/dist/builder/declared-calls-store.mjs +15 -0
  13. package/dist/builder/field-builder.types.d.ts +13 -0
  14. package/dist/builder/field-declared-calls.types.d.ts +6 -0
  15. package/dist/builder/field-declared-calls.types.js +2 -0
  16. package/dist/builder/field-declared-calls.types.mjs +1 -0
  17. package/dist/builder/field-entry.types.d.ts +9 -3
  18. package/dist/builder/field-options.types.d.ts +26 -0
  19. package/dist/chain/bundle-paths.types.d.ts +23 -0
  20. package/dist/chain/bundle-paths.types.js +2 -0
  21. package/dist/chain/bundle-paths.types.mjs +1 -0
  22. package/dist/chain/chain-method.types.d.ts +8 -3
  23. package/dist/chain/chain-node-store.d.ts +5 -0
  24. package/dist/chain/chain-node-store.js +15 -0
  25. package/dist/chain/chain-node-store.mjs +11 -0
  26. package/dist/chain/collect-field-rules.d.ts +13 -2
  27. package/dist/chain/collect-field-rules.js +10 -3
  28. package/dist/chain/collect-field-rules.mjs +10 -3
  29. package/dist/chain/create-chain-node.js +19 -9
  30. package/dist/chain/create-chain-node.mjs +19 -9
  31. package/dist/chain/declaration-recorder.port.d.ts +31 -0
  32. package/dist/chain/declaration-recorder.port.js +17 -0
  33. package/dist/chain/declaration-recorder.port.mjs +13 -0
  34. package/dist/chain/declared-call.types.d.ts +15 -0
  35. package/dist/chain/declared-call.types.js +2 -0
  36. package/dist/chain/declared-call.types.mjs +1 -0
  37. package/dist/chain/index.d.ts +3 -1
  38. package/dist/chain/resolve-args.types.d.ts +4 -2
  39. package/dist/compile/compile-array-node.d.ts +0 -7
  40. package/dist/compile/compile-array-node.js +5 -0
  41. package/dist/compile/compile-array-node.mjs +5 -0
  42. package/dist/compile/compile-field.d.ts +1 -0
  43. package/dist/compile/compile-field.js +13 -2
  44. package/dist/compile/compile-field.mjs +13 -2
  45. package/dist/compile/compile-schema.js +4 -0
  46. package/dist/compile/compile-schema.mjs +4 -0
  47. package/dist/compile/group-array-fields.d.ts +1 -0
  48. package/dist/compile/split-rules-by-kind.js +27 -7
  49. package/dist/compile/split-rules-by-kind.mjs +27 -7
  50. package/dist/compile/validation-plan.types.d.ts +36 -0
  51. package/dist/core/type-erasure.d.ts +36 -30
  52. package/dist/core/type-erasure.js +36 -30
  53. package/dist/core/type-erasure.mjs +36 -30
  54. package/dist/json-schema/build-from-schema.js +8 -1
  55. package/dist/json-schema/build-from-schema.mjs +8 -1
  56. package/dist/json-schema/declare-additional-properties.d.ts +7 -7
  57. package/dist/json-schema/declare-additional-properties.js +7 -7
  58. package/dist/json-schema/declare-additional-properties.mjs +7 -7
  59. package/dist/json-schema/declare-object-keywords.js +4 -4
  60. package/dist/json-schema/declare-object-keywords.mjs +4 -4
  61. package/dist/json-schema/flatten-schema.js +1 -1
  62. package/dist/json-schema/flatten-schema.mjs +1 -1
  63. package/dist/json-schema/follow-json-pointer.d.ts +7 -6
  64. package/dist/json-schema/follow-json-pointer.js +24 -24
  65. package/dist/json-schema/follow-json-pointer.mjs +24 -24
  66. package/dist/json-schema/ref-resolution-error.js +3 -3
  67. package/dist/json-schema/ref-resolution-error.mjs +3 -3
  68. package/dist/json-schema/schema-registry.js +12 -11
  69. package/dist/json-schema/schema-registry.mjs +12 -11
  70. package/dist/json-schema/uri-reference.js +12 -12
  71. package/dist/json-schema/uri-reference.mjs +12 -12
  72. package/dist/path/create-value-writer.js +12 -12
  73. package/dist/path/create-value-writer.mjs +12 -12
  74. package/dist/path/reserved-segment.d.ts +16 -16
  75. package/dist/path/reserved-segment.js +17 -21
  76. package/dist/path/reserved-segment.mjs +17 -21
  77. package/dist/plugin-kit/marker.types.d.ts +18 -0
  78. package/dist/plugins/index.generated.d.ts +1 -0
  79. package/dist/plugins/index.generated.js +6 -4
  80. package/dist/plugins/index.generated.mjs +3 -2
  81. package/dist/plugins/manifest.generated.js +3 -2
  82. package/dist/plugins/manifest.generated.mjs +3 -2
  83. package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +8 -8
  84. package/dist/plugins/object-additional-properties/select-additional-keys.js +16 -16
  85. package/dist/plugins/object-additional-properties/select-additional-keys.mjs +16 -16
  86. package/dist/plugins/stitch/stitch.d.ts +24 -8
  87. package/dist/plugins/stitch-with/index.d.ts +2 -0
  88. package/dist/plugins/stitch-with/index.js +5 -0
  89. package/dist/plugins/stitch-with/index.mjs +1 -0
  90. package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
  91. package/dist/plugins/stitch-with/stitch-with.js +92 -0
  92. package/dist/plugins/stitch-with/stitch-with.mjs +89 -0
  93. package/dist/plugins/stitchWith.d.ts +1 -0
  94. package/dist/plugins/stitchWith.js +2 -0
  95. package/dist/plugins/stitchWith.mjs +1 -0
  96. package/dist/plugins/string-min/string-min.js +19 -3
  97. package/dist/plugins/string-min/string-min.mjs +20 -4
  98. package/dist/presets/index.d.ts +1 -0
  99. package/dist/presets/index.js +9 -0
  100. package/dist/presets/index.mjs +1 -0
  101. package/dist/presets/presets.d.ts +164 -0
  102. package/dist/presets/presets.js +79 -0
  103. package/dist/presets/presets.mjs +76 -0
  104. package/dist/runtime/create-field-validator.js +22 -8
  105. package/dist/runtime/create-field-validator.mjs +22 -8
  106. package/dist/runtime/create-validator.js +38 -8
  107. package/dist/runtime/create-validator.mjs +38 -8
  108. package/dist/runtime/field-rule-context.d.ts +32 -0
  109. package/dist/runtime/field-rule-context.js +48 -0
  110. package/dist/runtime/field-rule-context.mjs +44 -0
  111. package/dist/runtime/index-stack.d.ts +29 -4
  112. package/dist/runtime/index-stack.js +76 -15
  113. package/dist/runtime/index-stack.mjs +76 -15
  114. package/dist/runtime/output-writer.js +5 -1
  115. package/dist/runtime/output-writer.mjs +5 -1
  116. package/dist/runtime/run-array-node.js +22 -10
  117. package/dist/runtime/run-array-node.mjs +22 -10
  118. package/dist/runtime/run-field.js +47 -18
  119. package/dist/runtime/run-field.mjs +47 -18
  120. package/dist/runtime/run-plan.js +5 -1
  121. package/dist/runtime/run-plan.mjs +5 -1
  122. package/dist/standard-schema/assemble-json-schema.d.ts +4 -0
  123. package/dist/standard-schema/assemble-json-schema.js +95 -0
  124. package/dist/standard-schema/assemble-json-schema.mjs +92 -0
  125. package/dist/standard-schema/declaration-recorder.d.ts +6 -0
  126. package/dist/standard-schema/declaration-recorder.js +30 -0
  127. package/dist/standard-schema/declaration-recorder.mjs +27 -0
  128. package/dist/standard-schema/declarations-unavailable-error.d.ts +4 -0
  129. package/dist/standard-schema/declarations-unavailable-error.js +32 -0
  130. package/dist/standard-schema/declarations-unavailable-error.mjs +28 -0
  131. package/dist/standard-schema/emit-field-schema.d.ts +9 -0
  132. package/dist/standard-schema/emit-field-schema.js +68 -0
  133. package/dist/standard-schema/emit-field-schema.mjs +65 -0
  134. package/dist/standard-schema/index.d.ts +5 -0
  135. package/dist/standard-schema/index.js +9 -1
  136. package/dist/standard-schema/index.mjs +4 -0
  137. package/dist/standard-schema/json-schema-target.d.ts +6 -0
  138. package/dist/standard-schema/json-schema-target.js +44 -0
  139. package/dist/standard-schema/json-schema-target.mjs +39 -0
  140. package/dist/standard-schema/plugin-keyword-map.d.ts +3 -0
  141. package/dist/standard-schema/plugin-keyword-map.js +93 -0
  142. package/dist/standard-schema/plugin-keyword-map.mjs +90 -0
  143. package/dist/standard-schema/split-issue-path.d.ts +6 -4
  144. package/dist/standard-schema/split-issue-path.js +15 -13
  145. package/dist/standard-schema/split-issue-path.mjs +15 -13
  146. package/dist/standard-schema/standard-schema.types.d.ts +8 -7
  147. package/dist/standard-schema/standard-schema.types.js +6 -6
  148. package/dist/standard-schema/standard-schema.types.mjs +6 -6
  149. package/dist/standard-schema/to-standard-json-schema.d.ts +19 -0
  150. package/dist/standard-schema/to-standard-json-schema.js +36 -0
  151. package/dist/standard-schema/to-standard-json-schema.mjs +33 -0
  152. package/dist/standard-schema/to-standard-schema.d.ts +16 -15
  153. package/dist/standard-schema/to-standard-schema.js +15 -22
  154. package/dist/standard-schema/to-standard-schema.mjs +15 -22
  155. package/dist/standard-schema/unrepresentable-rule-error.d.ts +15 -0
  156. package/dist/standard-schema/unrepresentable-rule-error.js +43 -0
  157. package/dist/standard-schema/unrepresentable-rule-error.mjs +38 -0
  158. package/dist/types/index.d.ts +19 -7
  159. package/dist/types/index.js +7 -7
  160. package/dist/types/index.mjs +7 -7
  161. package/package.json +35 -19
@@ -50,6 +50,14 @@ function createBuilderSurface() {
50
50
  registerPlugin(registration.plugins, plugin);
51
51
  return surface;
52
52
  },
53
+ useAll(plugins) {
54
+ // In enumeration order, first-wins: a name arriving twice keeps the
55
+ // first one, so a preset never silently replaces what is registered.
56
+ for (const plugin of Object.values(plugins)) {
57
+ registerPlugin(registration.plugins, plugin);
58
+ }
59
+ return surface;
60
+ },
53
61
  withConfig(config) {
54
62
  registration.config = Object.assign({}, registration.config, config);
55
63
  return surface;
@@ -44,6 +44,14 @@ export function createBuilderSurface() {
44
44
  registerPlugin(registration.plugins, plugin);
45
45
  return surface;
46
46
  },
47
+ useAll(plugins) {
48
+ // In enumeration order, first-wins: a name arriving twice keeps the
49
+ // first one, so a preset never silently replaces what is registered.
50
+ for (const plugin of Object.values(plugins)) {
51
+ registerPlugin(registration.plugins, plugin);
52
+ }
53
+ return surface;
54
+ },
47
55
  withConfig(config) {
48
56
  registration.config = Object.assign({}, registration.config, config);
49
57
  return surface;
@@ -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
+ }
@@ -26,6 +26,19 @@ export interface FieldBuilder<T extends object, B extends PluginBag, TDeclared e
26
26
  }
27
27
  export interface Builder<B extends PluginBag = Record<never, never>> {
28
28
  use<P extends AnyPlugin>(plugin: P): Builder<B & BagEntry<P>>;
29
+ /**
30
+ * A whole SET of plugins at once — a preset, or any object of them.
31
+ *
32
+ * A bag is a name -> plugin map, so a preset is that value and nothing more;
33
+ * there is no registry and no preset type to learn. `use()` one at a time
34
+ * still works and still costs only what it names, which is the point of the
35
+ * subpaths — this is for the case where writing fifteen `use()` lines is the
36
+ * thing standing between you and the validator.
37
+ *
38
+ * Duplicates follow the same rule as `use()`: FIRST WINS, so a preset cannot
39
+ * quietly replace a plugin you already registered.
40
+ */
41
+ useAll<Bag extends PluginBag>(plugins: Bag): Builder<B & Bag>;
29
42
  /**
30
43
  * The per-builder override of the process-wide GlobalConfig. Merged over
31
44
  * getGlobalConfig() once, at build(), and handed to every plugin as
@@ -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
  }
@@ -0,0 +1,23 @@
1
+ import type { FieldPath } from "../path/field-path.types";
2
+ import type { ValueAtPath } from "../path/value-at-path.types";
3
+ import type { AnyChain } from "./field-chain.types";
4
+ import type { FieldSlots } from "./field-slots.types";
5
+ import type { PluginBag } from "./plugin-bag.types";
6
+ /** Alias to a path from the root; only paths that exist there are writable. */
7
+ export type BundlePaths<TRoot> = Readonly<Record<string, FieldPath<TRoot> & string>>;
8
+ /** The bundle built from the table: aliases as keys, the path's value as value. */
9
+ export type BundleOf<TRoot, M extends BundlePaths<TRoot>> = {
10
+ readonly [A in keyof M & string]: ValueAtPath<TRoot, M[A]>;
11
+ };
12
+ /**
13
+ * The sub-chain for one bundle.
14
+ *
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`.
18
+ *
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.
22
+ */
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,13 +1,18 @@
1
- import type { GuardOut, TransformOut } from "../plugin-kit/marker.types";
1
+ import type { BundleOut, GuardOut, StitchOut, TransformOut } from "../plugin-kit/marker.types";
2
+ import type { BundleChain, BundlePaths } from "./bundle-paths.types";
2
3
  import type { PluginDefinition, PluginSignature } from "../plugin-kit/plugin-definition";
3
- import type { Present, RuleOptions, TypeName } from "../types";
4
+ import type { CrossFieldOutcome, Present, RuleOptions, TypeName } from "../types";
5
+ import type { FieldPath } from "../path/field-path.types";
6
+ import type { PickPaths } from "../path/value-at-path.types";
4
7
  import type { PluginBag } from "./plugin-bag.types";
5
8
  import type { ChainState, CoverWith } from "./chain-state.types";
6
9
  import type { ResolveArgs, ResolveOut } from "./resolve-args.types";
7
10
  import type { AnyChain, FieldChain } from "./field-chain.types";
8
11
  import type { FieldSlots } from "./field-slots.types";
9
12
  /** One plugin definition -> one call signature. */
10
- export type ChainMethod<P, B extends PluginBag, S extends TypeName, TRoot, TValue, TState extends ChainState> = P extends PluginDefinition<string, string, readonly TypeName[], infer Sig extends PluginSignature> ? [Sig["out"]] extends [TransformOut] ? <R>(map: (value: Present<TValue, TState>) => R, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, R, TState> : [Sig["out"]] extends [GuardOut] ? <X extends Present<TValue, TState>>(condition: (value: Present<TValue, TState>) => value is X, define: (b: FieldSlots<TRoot, B, X>) => AnyChain, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, TValue, CoverWith<TState, X>> : (...args: [
13
+ export type ChainMethod<P, B extends PluginBag, S extends TypeName, TRoot, TValue, TState extends ChainState> = P extends PluginDefinition<string, string, readonly TypeName[], infer Sig extends PluginSignature> ? [Sig["out"]] extends [TransformOut] ? <R>(map: (value: Present<TValue, TState>) => R, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, R, TState> : [Sig["out"]] extends [GuardOut] ? <X extends Present<TValue, TState>>(condition: (value: Present<TValue, TState>) => value is X, define: (b: FieldSlots<TRoot, B, X>) => AnyChain, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, TValue, CoverWith<TState, X>> : [
14
+ Sig["out"]
15
+ ] extends [StitchOut] ? <const F extends readonly (FieldPath<TRoot> & string)[]>(fields: F, check: (fieldValues: PickPaths<TRoot, F>, value: Present<TValue, TState>, root: TRoot) => CrossFieldOutcome, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, TValue, TState> : [Sig["out"]] extends [BundleOut] ? <const M extends BundlePaths<TRoot>>(fields: M, define: BundleChain<TRoot, M, B>, options?: RuleOptions<Sig["context"]>) => FieldChain<B, S, TRoot, TValue, TState> : (...args: [
11
16
  ...ResolveArgs<Sig["args"], B, TRoot, TValue, TState>,
12
17
  options?: RuleOptions<Sig["context"]>
13
18
  ]) => ResolveOut<Sig["out"], TValue, TState> extends [
@@ -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
  }
@@ -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
  }
@@ -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 });