@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
@@ -53,15 +53,29 @@ function createSubjectComposer(template) {
53
53
  const write = createValueWriter(template);
54
54
  return (siblings, value) => write(siblings ?? {}, value);
55
55
  }
56
- return (siblings, value) => mergeOntoSiblings(siblings, nestValue(template, 0, value));
56
+ const nest = compileNester(template);
57
+ return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
57
58
  }
58
- function nestValue(template, from, value) {
59
- const segment = template[from];
60
- if (segment === undefined)
61
- return value;
62
- if (segment.kind === "each")
63
- return [nestValue(template, from + 1, value)];
64
- return { [segment.key]: nestValue(template, from + 1, value) };
59
+ /**
60
+ * Walks the template **once** and folds it into a wrapping function.
61
+ *
62
+ * Walking it per validation re-read each segment's kind every time, which is
63
+ * exactly the thing validation time must not do: what something IS was settled
64
+ * at compile time. The non-wildcard route was always shaped this way.
65
+ */
66
+ function compileNester(template) {
67
+ let nest = (value) => value;
68
+ for (let index = template.length - 1; index >= 0; index -= 1) {
69
+ const segment = template[index];
70
+ if (segment === undefined)
71
+ continue;
72
+ const inner = nest;
73
+ nest =
74
+ segment.kind === "each"
75
+ ? (value) => [inner(value)]
76
+ : ((key) => (value) => ({ [key]: inner(value) }))(segment.key);
77
+ }
78
+ return nest;
65
79
  }
66
80
  /** The wrapper wins on the key it names; every other sibling survives. */
67
81
  function mergeOntoSiblings(siblings, fragment) {
@@ -39,7 +39,34 @@ function rejectMissingRoot() {
39
39
  function hasRejectingIssue(issues) {
40
40
  return issues.some((issue) => issue.severity === "error");
41
41
  }
42
- function runRoot(plan, value, options, targets, shouldApplyTransforms) {
42
+ /**
43
+ * The shared do-nothing runner for a plan that cannot recurse.
44
+ *
45
+ * Building a real runner allocates a WeakSet and several closures. Doing that
46
+ * on every validate() of a plan with no recursive rule in it was most of the
47
+ * fixed per-call cost. Whether a plan can recurse is known at build time and
48
+ * is not a question to re-ask at validation time.
49
+ *
50
+ * Never actually called: a field only reaches its runner when it declares
51
+ * recursion, and a plan containing one gets the real runner instead.
52
+ */
53
+ const NO_RECURSION = () => { };
54
+ /**
55
+ * Whether the root needs a real runner.
56
+ *
57
+ * Only the fields directly under the root and the elements of array nodes
58
+ * matter. A composite branch makes its own runner for the nested plan, so it
59
+ * never uses the one passed from here.
60
+ */
61
+ function planCanRecurse(plan) {
62
+ return (plan.fields.some((field) => field.recursion !== null) ||
63
+ plan.arrays.some(nodeCanRecurse));
64
+ }
65
+ function nodeCanRecurse(node) {
66
+ return (node.elementFields.some((field) => field.recursion !== null) ||
67
+ node.nested.some(nodeCanRecurse));
68
+ }
69
+ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
43
70
  if (value === null || value === undefined)
44
71
  return rejectMissingRoot();
45
72
  const sink = new issue_sink_1.IssueSink((0, issue_sink_1.resolveAbortPolicy)(options));
@@ -48,11 +75,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
48
75
  sink,
49
76
  indices: new index_stack_1.IndexStack(),
50
77
  shouldApplyTransforms,
51
- runRecursion: (0, run_recursion_1.createRecursionRunner)({
52
- root: value,
53
- sink,
54
- external: options?.external,
55
- }),
78
+ runRecursion: canRecurse
79
+ ? (0, run_recursion_1.createRecursionRunner)({
80
+ root: value,
81
+ sink,
82
+ external: options?.external,
83
+ })
84
+ : NO_RECURSION,
56
85
  external: options?.external,
57
86
  }, targets);
58
87
  const issues = Object.freeze(sink.issues);
@@ -63,8 +92,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
63
92
  function createValidator(plan) {
64
93
  const parseTargets = (0, output_writer_1.createPlanWriteTargets)(plan);
65
94
  const shouldWriteOutput = parseTargets !== null;
95
+ const canRecurse = planCanRecurse(plan);
66
96
  return {
67
- validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false),
68
- parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput),
97
+ validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false, canRecurse),
98
+ parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
69
99
  };
70
100
  }
@@ -34,7 +34,34 @@ function rejectMissingRoot() {
34
34
  export function hasRejectingIssue(issues) {
35
35
  return issues.some((issue) => issue.severity === "error");
36
36
  }
37
- function runRoot(plan, value, options, targets, shouldApplyTransforms) {
37
+ /**
38
+ * The shared do-nothing runner for a plan that cannot recurse.
39
+ *
40
+ * Building a real runner allocates a WeakSet and several closures. Doing that
41
+ * on every validate() of a plan with no recursive rule in it was most of the
42
+ * fixed per-call cost. Whether a plan can recurse is known at build time and
43
+ * is not a question to re-ask at validation time.
44
+ *
45
+ * Never actually called: a field only reaches its runner when it declares
46
+ * recursion, and a plan containing one gets the real runner instead.
47
+ */
48
+ const NO_RECURSION = () => { };
49
+ /**
50
+ * Whether the root needs a real runner.
51
+ *
52
+ * Only the fields directly under the root and the elements of array nodes
53
+ * matter. A composite branch makes its own runner for the nested plan, so it
54
+ * never uses the one passed from here.
55
+ */
56
+ function planCanRecurse(plan) {
57
+ return (plan.fields.some((field) => field.recursion !== null) ||
58
+ plan.arrays.some(nodeCanRecurse));
59
+ }
60
+ function nodeCanRecurse(node) {
61
+ return (node.elementFields.some((field) => field.recursion !== null) ||
62
+ node.nested.some(nodeCanRecurse));
63
+ }
64
+ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
38
65
  if (value === null || value === undefined)
39
66
  return rejectMissingRoot();
40
67
  const sink = new IssueSink(resolveAbortPolicy(options));
@@ -43,11 +70,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
43
70
  sink,
44
71
  indices: new IndexStack(),
45
72
  shouldApplyTransforms,
46
- runRecursion: createRecursionRunner({
47
- root: value,
48
- sink,
49
- external: options?.external,
50
- }),
73
+ runRecursion: canRecurse
74
+ ? createRecursionRunner({
75
+ root: value,
76
+ sink,
77
+ external: options?.external,
78
+ })
79
+ : NO_RECURSION,
51
80
  external: options?.external,
52
81
  }, targets);
53
82
  const issues = Object.freeze(sink.issues);
@@ -58,8 +87,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
58
87
  export function createValidator(plan) {
59
88
  const parseTargets = createPlanWriteTargets(plan);
60
89
  const shouldWriteOutput = parseTargets !== null;
90
+ const canRecurse = planCanRecurse(plan);
61
91
  return {
62
- validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false),
63
- parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput),
92
+ validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false, canRecurse),
93
+ parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
64
94
  };
65
95
  }
@@ -0,0 +1,32 @@
1
+ import type { ArrayItemContext, RuleContext } from "../types";
2
+ import type { IndexStack } from "./index-stack";
3
+ export declare class FieldRuleContext implements RuleContext {
4
+ readonly root: unknown;
5
+ private readonly indices;
6
+ /** Built at compile time, relative to the subject this field reads from. */
7
+ private readonly ownPath;
8
+ readonly item: ArrayItemContext | undefined;
9
+ readonly external: Readonly<Record<string, unknown>> | undefined;
10
+ /**
11
+ * The path once something has asked for it. `null` rather than `""` because
12
+ * the empty string is a real path — the root.
13
+ *
14
+ * It is remembered because the index stack underneath is MUTABLE and is
15
+ * popped when the element ends. A rule that kept its context and read `path`
16
+ * afterwards would otherwise see wherever the runner had moved on to.
17
+ * Reading it during the rule's own call — which is what every rule does —
18
+ * pins it.
19
+ */
20
+ private rendered;
21
+ constructor(root: unknown, indices: IndexStack,
22
+ /** Built at compile time, relative to the subject this field reads from. */
23
+ ownPath: string, item: ArrayItemContext | undefined, external: Readonly<Record<string, unknown>> | undefined);
24
+ get path(): string;
25
+ /**
26
+ * Serialising a context is a thing people do when they log a failure, and a
27
+ * prototype getter would silently drop `path` from the output. This pins the
28
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
29
+ * internals above from reaching a log line.
30
+ */
31
+ toJSON(): Record<string, unknown>;
32
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FieldRuleContext = void 0;
4
+ class FieldRuleContext {
5
+ constructor(root, indices,
6
+ /** Built at compile time, relative to the subject this field reads from. */
7
+ ownPath, item, external) {
8
+ this.root = root;
9
+ this.indices = indices;
10
+ this.ownPath = ownPath;
11
+ this.item = item;
12
+ this.external = external;
13
+ /**
14
+ * The path once something has asked for it. `null` rather than `""` because
15
+ * the empty string is a real path — the root.
16
+ *
17
+ * It is remembered because the index stack underneath is MUTABLE and is
18
+ * popped when the element ends. A rule that kept its context and read `path`
19
+ * afterwards would otherwise see wherever the runner had moved on to.
20
+ * Reading it during the rule's own call — which is what every rule does —
21
+ * pins it.
22
+ */
23
+ this.rendered = null;
24
+ }
25
+ get path() {
26
+ const already = this.rendered;
27
+ if (already !== null)
28
+ return already;
29
+ const built = this.indices.renderFieldPath(this.ownPath);
30
+ this.rendered = built;
31
+ return built;
32
+ }
33
+ /**
34
+ * Serialising a context is a thing people do when they log a failure, and a
35
+ * prototype getter would silently drop `path` from the output. This pins the
36
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
37
+ * internals above from reaching a log line.
38
+ */
39
+ toJSON() {
40
+ return {
41
+ root: this.root,
42
+ path: this.path,
43
+ item: this.item,
44
+ external: this.external,
45
+ };
46
+ }
47
+ }
48
+ exports.FieldRuleContext = FieldRuleContext;
@@ -0,0 +1,44 @@
1
+ export class FieldRuleContext {
2
+ constructor(root, indices,
3
+ /** Built at compile time, relative to the subject this field reads from. */
4
+ ownPath, item, external) {
5
+ this.root = root;
6
+ this.indices = indices;
7
+ this.ownPath = ownPath;
8
+ this.item = item;
9
+ this.external = external;
10
+ /**
11
+ * The path once something has asked for it. `null` rather than `""` because
12
+ * the empty string is a real path — the root.
13
+ *
14
+ * It is remembered because the index stack underneath is MUTABLE and is
15
+ * popped when the element ends. A rule that kept its context and read `path`
16
+ * afterwards would otherwise see wherever the runner had moved on to.
17
+ * Reading it during the rule's own call — which is what every rule does —
18
+ * pins it.
19
+ */
20
+ this.rendered = null;
21
+ }
22
+ get path() {
23
+ const already = this.rendered;
24
+ if (already !== null)
25
+ return already;
26
+ const built = this.indices.renderFieldPath(this.ownPath);
27
+ this.rendered = built;
28
+ return built;
29
+ }
30
+ /**
31
+ * Serialising a context is a thing people do when they log a failure, and a
32
+ * prototype getter would silently drop `path` from the output. This pins the
33
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
34
+ * internals above from reaching a log line.
35
+ */
36
+ toJSON() {
37
+ return {
38
+ root: this.root,
39
+ path: this.path,
40
+ item: this.item,
41
+ external: this.external,
42
+ };
43
+ }
44
+ }
@@ -1,11 +1,30 @@
1
- import type { PathSegment } from "../path/path-segment.types";
2
1
  export declare class IndexStack {
3
- private readonly prefixes;
2
+ private readonly nodePaths;
4
3
  private readonly openIndices;
4
+ /**
5
+ * The rendered prefix, or `null` when it has not been asked for since the
6
+ * last push or pop. `null` and not `""` because the empty string is a real
7
+ * answer — the root.
8
+ *
9
+ * Invalidating rather than recomputing on push is the point: an element that
10
+ * raises no issue never renders its own prefix at all.
11
+ */
12
+ private currentPrefix;
5
13
  /** How many array levels are currently open. */
6
14
  get depth(): number;
7
15
  /** The open indices, outermost first: `[0, 2]` inside `grid[0][2]`. */
8
16
  get indices(): readonly number[];
17
+ /**
18
+ * The prefix for the currently open frames — `""` when none are open,
19
+ * `items[0]` inside one, `grid[0][2]` inside two.
20
+ *
21
+ * Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
22
+ * depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
23
+ * outside the array, so V8 falls back to a named-property lookup and walks
24
+ * the prototype chain. The `prefix` getter used to be written that way and
25
+ * it took 12% of the array shape's self time.
26
+ */
27
+ private readTop;
9
28
  /** `""` at the root, `items[0]` inside the first element of `items`. */
10
29
  get prefix(): string;
11
30
  /**
@@ -21,8 +40,14 @@ export declare class IndexStack {
21
40
  push(nodePath: string, index: number): void;
22
41
  /** An unbalanced pop means a runner lost track of its own nesting. */
23
42
  pop(): void;
24
- /** The issue path of a field read from the subject at the current level. */
25
- renderFieldPath(template: readonly PathSegment[]): string;
43
+ /**
44
+ * The issue path of a field read from the subject at the current level.
45
+ *
46
+ * `renderedPath` was built once, at compile time. This used to walk the
47
+ * template on every call, which for an array meant rebuilding the same
48
+ * string once per element.
49
+ */
50
+ renderFieldPath(renderedPath: string): string;
26
51
  }
27
52
  /** `""` is the root path, so it never contributes a separator dot. */
28
53
  export declare function joinIssuePath(prefix: string, own: string): string;
@@ -1,19 +1,47 @@
1
1
  "use strict";
2
+ // ===========================================================================
3
+ // L5 src/runtime/index-stack.ts
4
+ // Where the runtime currently is, expressed as a concrete issue path.
5
+ //
6
+ // An issue path must read `items[0].name`, never the declaration pattern
7
+ // `items[*].name`. The legacy tree carried indices two different ways — the
8
+ // nested-array processor threaded them through its own recursion while
9
+ // validator-factory fell back to `error.path || elementPath` — which is how
10
+ // the pattern string leaked into user-visible errors. There is one stack here
11
+ // and every path a runner reports comes out of it.
12
+ //
13
+ // A CompiledField's template is RELATIVE to the subject it reads from, so an
14
+ // element field of `items[*]` has the template `name` and could never render
15
+ // `items[0].name` on its own. The array runner pushes the enclosing node and
16
+ // the index; the stack renders the prefix.
17
+ //
18
+ // The prefix is rendered ON DEMAND and remembered until the next push or pop.
19
+ // Entering used to build `items[0]` eagerly, which is one string per element
20
+ // that nothing reads unless a rule fails — and now nothing reads it even then
21
+ // until FieldRuleContext is asked for its path. Entering therefore costs two
22
+ // pushes and one assignment, and the first field that asks pays the render.
23
+ //
24
+ // It is deliberately MUTABLE and pushed/popped around an element loop: one
25
+ // stack lives for one validate() call, and 10k elements must not allocate 10k
26
+ // index arrays. It is also why FieldRuleContext remembers the path it built —
27
+ // this stack has moved on by the time a retained context could read it.
28
+ // ===========================================================================
2
29
  Object.defineProperty(exports, "__esModule", { value: true });
3
30
  exports.IndexStack = void 0;
4
31
  exports.joinIssuePath = joinIssuePath;
5
- const format_issue_path_1 = require("../path/format-issue-path");
6
- /**
7
- * A compiled template never contains a wildcard (createValueReader rejects
8
- * one), so rendering it needs no index. Passing none is what makes a stray
9
- * wildcard template throw here instead of silently rendering a second copy of
10
- * an index this stack has already written into the prefix.
11
- */
12
- const NO_INDICES = Object.freeze([]);
13
32
  class IndexStack {
14
33
  constructor() {
15
- this.prefixes = [];
34
+ this.nodePaths = [];
16
35
  this.openIndices = [];
36
+ /**
37
+ * The rendered prefix, or `null` when it has not been asked for since the
38
+ * last push or pop. `null` and not `""` because the empty string is a real
39
+ * answer — the root.
40
+ *
41
+ * Invalidating rather than recomputing on push is the point: an element that
42
+ * raises no issue never renders its own prefix at all.
43
+ */
44
+ this.currentPrefix = null;
17
45
  }
18
46
  /** How many array levels are currently open. */
19
47
  get depth() {
@@ -23,9 +51,34 @@ class IndexStack {
23
51
  get indices() {
24
52
  return this.openIndices;
25
53
  }
54
+ /**
55
+ * The prefix for the currently open frames — `""` when none are open,
56
+ * `items[0]` inside one, `grid[0][2]` inside two.
57
+ *
58
+ * Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
59
+ * depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
60
+ * outside the array, so V8 falls back to a named-property lookup and walks
61
+ * the prototype chain. The `prefix` getter used to be written that way and
62
+ * it took 12% of the array shape's self time.
63
+ */
64
+ readTop() {
65
+ const already = this.currentPrefix;
66
+ if (already !== null)
67
+ return already;
68
+ let built = "";
69
+ for (let i = 0; i < this.nodePaths.length; i += 1) {
70
+ const nodePath = this.nodePaths[i];
71
+ const index = this.openIndices[i];
72
+ if (nodePath === undefined || index === undefined)
73
+ continue;
74
+ built = `${joinIssuePath(built, nodePath)}[${index}]`;
75
+ }
76
+ this.currentPrefix = built;
77
+ return built;
78
+ }
26
79
  /** `""` at the root, `items[0]` inside the first element of `items`. */
27
80
  get prefix() {
28
- return this.prefixes[this.prefixes.length - 1] ?? "";
81
+ return this.readTop();
29
82
  }
30
83
  /**
31
84
  * Enters element `index` of an array node. `nodePath` is the node's own path
@@ -41,20 +94,28 @@ class IndexStack {
41
94
  if (!Number.isInteger(index) || index < 0) {
42
95
  throw new RangeError(`an array index must be a non-negative integer, received ${String(index)}`);
43
96
  }
44
- this.prefixes.push(`${joinIssuePath(this.prefix, nodePath)}[${index}]`);
97
+ this.nodePaths.push(nodePath);
45
98
  this.openIndices.push(index);
99
+ this.currentPrefix = null;
46
100
  }
47
101
  /** An unbalanced pop means a runner lost track of its own nesting. */
48
102
  pop() {
49
103
  if (this.openIndices.length === 0) {
50
104
  throw new RangeError("popped an array index that was never pushed");
51
105
  }
52
- this.prefixes.pop();
106
+ this.nodePaths.pop();
53
107
  this.openIndices.pop();
108
+ this.currentPrefix = null;
54
109
  }
55
- /** The issue path of a field read from the subject at the current level. */
56
- renderFieldPath(template) {
57
- return joinIssuePath(this.prefix, (0, format_issue_path_1.formatIssuePath)(template, NO_INDICES));
110
+ /**
111
+ * The issue path of a field read from the subject at the current level.
112
+ *
113
+ * `renderedPath` was built once, at compile time. This used to walk the
114
+ * template on every call, which for an array meant rebuilding the same
115
+ * string once per element.
116
+ */
117
+ renderFieldPath(renderedPath) {
118
+ return joinIssuePath(this.readTop(), renderedPath);
58
119
  }
59
120
  }
60
121
  exports.IndexStack = IndexStack;
@@ -1,15 +1,43 @@
1
- import { formatIssuePath } from "../path/format-issue-path.mjs";
2
- /**
3
- * A compiled template never contains a wildcard (createValueReader rejects
4
- * one), so rendering it needs no index. Passing none is what makes a stray
5
- * wildcard template throw here instead of silently rendering a second copy of
6
- * an index this stack has already written into the prefix.
7
- */
8
- const NO_INDICES = Object.freeze([]);
1
+ // ===========================================================================
2
+ // L5 src/runtime/index-stack.ts
3
+ // Where the runtime currently is, expressed as a concrete issue path.
4
+ //
5
+ // An issue path must read `items[0].name`, never the declaration pattern
6
+ // `items[*].name`. The legacy tree carried indices two different ways the
7
+ // nested-array processor threaded them through its own recursion while
8
+ // validator-factory fell back to `error.path || elementPath` — which is how
9
+ // the pattern string leaked into user-visible errors. There is one stack here
10
+ // and every path a runner reports comes out of it.
11
+ //
12
+ // A CompiledField's template is RELATIVE to the subject it reads from, so an
13
+ // element field of `items[*]` has the template `name` and could never render
14
+ // `items[0].name` on its own. The array runner pushes the enclosing node and
15
+ // the index; the stack renders the prefix.
16
+ //
17
+ // The prefix is rendered ON DEMAND and remembered until the next push or pop.
18
+ // Entering used to build `items[0]` eagerly, which is one string per element
19
+ // that nothing reads unless a rule fails — and now nothing reads it even then
20
+ // until FieldRuleContext is asked for its path. Entering therefore costs two
21
+ // pushes and one assignment, and the first field that asks pays the render.
22
+ //
23
+ // It is deliberately MUTABLE and pushed/popped around an element loop: one
24
+ // stack lives for one validate() call, and 10k elements must not allocate 10k
25
+ // index arrays. It is also why FieldRuleContext remembers the path it built —
26
+ // this stack has moved on by the time a retained context could read it.
27
+ // ===========================================================================
9
28
  export class IndexStack {
10
29
  constructor() {
11
- this.prefixes = [];
30
+ this.nodePaths = [];
12
31
  this.openIndices = [];
32
+ /**
33
+ * The rendered prefix, or `null` when it has not been asked for since the
34
+ * last push or pop. `null` and not `""` because the empty string is a real
35
+ * answer — the root.
36
+ *
37
+ * Invalidating rather than recomputing on push is the point: an element that
38
+ * raises no issue never renders its own prefix at all.
39
+ */
40
+ this.currentPrefix = null;
13
41
  }
14
42
  /** How many array levels are currently open. */
15
43
  get depth() {
@@ -19,9 +47,34 @@ export class IndexStack {
19
47
  get indices() {
20
48
  return this.openIndices;
21
49
  }
50
+ /**
51
+ * The prefix for the currently open frames — `""` when none are open,
52
+ * `items[0]` inside one, `grid[0][2]` inside two.
53
+ *
54
+ * Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
55
+ * depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
56
+ * outside the array, so V8 falls back to a named-property lookup and walks
57
+ * the prototype chain. The `prefix` getter used to be written that way and
58
+ * it took 12% of the array shape's self time.
59
+ */
60
+ readTop() {
61
+ const already = this.currentPrefix;
62
+ if (already !== null)
63
+ return already;
64
+ let built = "";
65
+ for (let i = 0; i < this.nodePaths.length; i += 1) {
66
+ const nodePath = this.nodePaths[i];
67
+ const index = this.openIndices[i];
68
+ if (nodePath === undefined || index === undefined)
69
+ continue;
70
+ built = `${joinIssuePath(built, nodePath)}[${index}]`;
71
+ }
72
+ this.currentPrefix = built;
73
+ return built;
74
+ }
22
75
  /** `""` at the root, `items[0]` inside the first element of `items`. */
23
76
  get prefix() {
24
- return this.prefixes[this.prefixes.length - 1] ?? "";
77
+ return this.readTop();
25
78
  }
26
79
  /**
27
80
  * Enters element `index` of an array node. `nodePath` is the node's own path
@@ -37,20 +90,28 @@ export class IndexStack {
37
90
  if (!Number.isInteger(index) || index < 0) {
38
91
  throw new RangeError(`an array index must be a non-negative integer, received ${String(index)}`);
39
92
  }
40
- this.prefixes.push(`${joinIssuePath(this.prefix, nodePath)}[${index}]`);
93
+ this.nodePaths.push(nodePath);
41
94
  this.openIndices.push(index);
95
+ this.currentPrefix = null;
42
96
  }
43
97
  /** An unbalanced pop means a runner lost track of its own nesting. */
44
98
  pop() {
45
99
  if (this.openIndices.length === 0) {
46
100
  throw new RangeError("popped an array index that was never pushed");
47
101
  }
48
- this.prefixes.pop();
102
+ this.nodePaths.pop();
49
103
  this.openIndices.pop();
104
+ this.currentPrefix = null;
50
105
  }
51
- /** The issue path of a field read from the subject at the current level. */
52
- renderFieldPath(template) {
53
- return joinIssuePath(this.prefix, formatIssuePath(template, NO_INDICES));
106
+ /**
107
+ * The issue path of a field read from the subject at the current level.
108
+ *
109
+ * `renderedPath` was built once, at compile time. This used to walk the
110
+ * template on every call, which for an array meant rebuilding the same
111
+ * string once per element.
112
+ */
113
+ renderFieldPath(renderedPath) {
114
+ return joinIssuePath(this.readTop(), renderedPath);
54
115
  }
55
116
  }
56
117
  /** `""` is the root path, so it never contributes a separator dot. */
@@ -24,8 +24,12 @@ function createArrayWriteTargets(nodes) {
24
24
  * hands back the very object it was given.
25
25
  */
26
26
  function createPlanWriteTargets(plan) {
27
- if (!plan.hasTransforms && !plan.hasDefaults)
27
+ // A field declaring only a normalizer is still written back by parse().
28
+ // Leave it out of this condition and no writer is made, so the value read
29
+ // comes back unchanged and in silence.
30
+ if (!plan.hasTransforms && !plan.hasDefaults && !plan.hasNormalizers) {
28
31
  return null;
32
+ }
29
33
  return createArrayWriteTargets(plan.arrays);
30
34
  }
31
35
  /**
@@ -17,8 +17,12 @@ export function createArrayWriteTargets(nodes) {
17
17
  * hands back the very object it was given.
18
18
  */
19
19
  export function createPlanWriteTargets(plan) {
20
- if (!plan.hasTransforms && !plan.hasDefaults)
20
+ // A field declaring only a normalizer is still written back by parse().
21
+ // Leave it out of this condition and no writer is made, so the value read
22
+ // comes back unchanged and in silence.
23
+ if (!plan.hasTransforms && !plan.hasDefaults && !plan.hasNormalizers) {
21
24
  return null;
25
+ }
22
26
  return createArrayWriteTargets(plan.arrays);
23
27
  }
24
28
  /**