@maroonedog/luq 2.4.4 → 2.6.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 (64) hide show
  1. package/README.md +64 -10
  2. package/dist/chain/slot-type-guard.js +20 -6
  3. package/dist/chain/slot-type-guard.mjs +20 -6
  4. package/dist/compile/resolve-presence.js +36 -5
  5. package/dist/compile/resolve-presence.mjs +36 -5
  6. package/dist/json-schema/array-keyword-guards.d.ts +31 -0
  7. package/dist/json-schema/array-keyword-guards.js +120 -0
  8. package/dist/json-schema/array-keyword-guards.mjs +114 -0
  9. package/dist/json-schema/assert-object-keyword-values.d.ts +59 -0
  10. package/dist/json-schema/assert-object-keyword-values.js +149 -0
  11. package/dist/json-schema/assert-object-keyword-values.mjs +141 -0
  12. package/dist/json-schema/assert-supported-dialect.d.ts +37 -0
  13. package/dist/json-schema/assert-supported-dialect.js +113 -0
  14. package/dist/json-schema/assert-supported-dialect.mjs +109 -0
  15. package/dist/json-schema/build-from-schema.d.ts +22 -3
  16. package/dist/json-schema/build-from-schema.js +27 -5
  17. package/dist/json-schema/build-from-schema.mjs +27 -5
  18. package/dist/json-schema/collect-definitions.d.ts +7 -0
  19. package/dist/json-schema/collect-definitions.js +9 -0
  20. package/dist/json-schema/collect-definitions.mjs +10 -1
  21. package/dist/json-schema/declare-additional-properties.d.ts +11 -1
  22. package/dist/json-schema/declare-additional-properties.js +13 -1
  23. package/dist/json-schema/declare-additional-properties.mjs +13 -1
  24. package/dist/json-schema/declare-object-keywords.d.ts +4 -0
  25. package/dist/json-schema/declare-object-keywords.js +11 -4
  26. package/dist/json-schema/declare-object-keywords.mjs +11 -4
  27. package/dist/json-schema/declare-required-properties.js +6 -0
  28. package/dist/json-schema/declare-required-properties.mjs +6 -0
  29. package/dist/json-schema/declare-value-keywords.d.ts +10 -1
  30. package/dist/json-schema/declare-value-keywords.js +43 -4
  31. package/dist/json-schema/declare-value-keywords.mjs +43 -4
  32. package/dist/json-schema/extensions/json-schema/index.d.ts +2 -2
  33. package/dist/json-schema/extensions/json-schema/index.js +4 -1
  34. package/dist/json-schema/extensions/json-schema/index.mjs +1 -1
  35. package/dist/json-schema/extensions/json-schema/json-schema.d.ts +2 -2
  36. package/dist/json-schema/extensions/json-schema/json-schema.js +3 -0
  37. package/dist/json-schema/extensions/json-schema/json-schema.mjs +4 -1
  38. package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +3 -1
  39. package/dist/json-schema/extensions/json-schema-full-feature/index.js +18 -1
  40. package/dist/json-schema/extensions/json-schema-full-feature/index.mjs +11 -0
  41. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +9 -1
  42. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +9 -2
  43. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +9 -2
  44. package/dist/json-schema/flatten-array-schema.d.ts +11 -1
  45. package/dist/json-schema/flatten-array-schema.js +20 -7
  46. package/dist/json-schema/flatten-array-schema.mjs +20 -7
  47. package/dist/json-schema/index.d.ts +4 -0
  48. package/dist/json-schema/index.js +9 -1
  49. package/dist/json-schema/index.mjs +3 -0
  50. package/dist/json-schema/keyword-map-core.js +3 -1
  51. package/dist/json-schema/keyword-map-core.mjs +3 -1
  52. package/dist/json-schema/keyword-map-string.js +30 -1
  53. package/dist/json-schema/keyword-map-string.mjs +30 -1
  54. package/dist/json-schema/keyword-map.js +5 -0
  55. package/dist/json-schema/keyword-map.mjs +5 -0
  56. package/dist/json-schema/malformed-schema-error.d.ts +38 -0
  57. package/dist/json-schema/malformed-schema-error.js +103 -0
  58. package/dist/json-schema/malformed-schema-error.mjs +98 -0
  59. package/dist/json-schema/unsupported-dialect-error.d.ts +21 -0
  60. package/dist/json-schema/unsupported-dialect-error.js +58 -0
  61. package/dist/json-schema/unsupported-dialect-error.mjs +54 -0
  62. package/dist/plugins/one-of/one-of.js +18 -4
  63. package/dist/plugins/one-of/one-of.mjs +18 -4
  64. package/package.json +8 -5
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ // ===========================================================================
3
+ // L8 src/json-schema/malformed-schema-error.ts
4
+ //
5
+ // UnsupportedKeywordError refuses a keyword NAME Luq cannot honour. This
6
+ // refuses a keyword VALUE the Draft-07 meta-schema does not allow, which until
7
+ // it is refused does not fail — it quietly enforces less than the document
8
+ // says. `{"type":"strig"}` drops the unknown name, so the type check vanishes
9
+ // and the field starts accepting null; `{"pattern":{"source":"^SKU-"}}` reaches
10
+ // the regex constructor as a stringified object and compiles to one matching
11
+ // almost anything; `{"additionalProperties":"false"}` loses the closed-object
12
+ // guarantee; `{"required":"name"}` builds and then throws a raw TypeError
13
+ // inside validate() on the first request. A validator that silently drops a
14
+ // constraint is worse than one that refuses the schema, so the refusal is a
15
+ // typed error carrying the keyword, what the meta-schema requires, and a
16
+ // rendering of what was found instead.
17
+ //
18
+ // The two refusals stay separate classes, neither extending the other, because
19
+ // they answer different questions: "Luq cannot honour this keyword" versus
20
+ // "this document is not valid Draft-07". The first is a limit of the library
21
+ // and the second is a bug in the document; a caller catching one must not
22
+ // silently catch the other.
23
+ // ===========================================================================
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.MalformedSchemaError = exports.SCHEMA_FORMS = void 0;
26
+ exports.renderReceivedValue = renderReceivedValue;
27
+ /**
28
+ * The two shapes §4.4 lets a schema take, worded once. Several keywords take a
29
+ * schema as their value — `properties` members, `additionalProperties`, every
30
+ * position of the tuple form of `items` — and each is read by a different
31
+ * module; sharing the phrase is what stops the same refusal being explained
32
+ * three different ways to the same caller.
33
+ *
34
+ * It is a fragment, so it composes: "the value ", `the schema under "a" `.
35
+ */
36
+ exports.SCHEMA_FORMS = "must be an object or a boolean";
37
+ /**
38
+ * How much of the offending value a message may carry. A schema node is
39
+ * arbitrarily large, and an error whose message is a megabyte long is unusable
40
+ * in a log line.
41
+ */
42
+ const RECEIVED_MAX_LENGTH = 80;
43
+ const TRUNCATION_MARKER = "...";
44
+ /**
45
+ * Renders an untrusted schema value for a message, in at most
46
+ * RECEIVED_MAX_LENGTH characters. The value comes from the same document that
47
+ * is already known to be malformed, so nothing about it can be assumed: it may
48
+ * be circular, hold a bigint, or be large enough to drown the message.
49
+ * Rendering must never be the thing that throws.
50
+ */
51
+ function renderReceivedValue(value) {
52
+ if (value === undefined)
53
+ return "undefined";
54
+ if (typeof value === "function")
55
+ return "[function]";
56
+ if (typeof value === "symbol")
57
+ return "[symbol]";
58
+ return truncate(renderJson(value));
59
+ }
60
+ /**
61
+ * JSON.stringify throws on a circular reference and on a bigint anywhere in
62
+ * the value, and returns undefined for a value it has no encoding for at all.
63
+ * Both outcomes become a description of the shape rather than a failure.
64
+ */
65
+ function renderJson(value) {
66
+ try {
67
+ const rendered = JSON.stringify(value);
68
+ return rendered === undefined ? describeUnrenderable(value) : rendered;
69
+ }
70
+ catch {
71
+ return describeUnrenderable(value);
72
+ }
73
+ }
74
+ function describeUnrenderable(value) {
75
+ return Array.isArray(value)
76
+ ? "[unrenderable array]"
77
+ : `[unrenderable ${typeof value}]`;
78
+ }
79
+ function truncate(rendered) {
80
+ if (rendered.length <= RECEIVED_MAX_LENGTH)
81
+ return rendered;
82
+ const kept = RECEIVED_MAX_LENGTH - TRUNCATION_MARKER.length;
83
+ return `${rendered.slice(0, kept)}${TRUNCATION_MARKER}`;
84
+ }
85
+ /**
86
+ * Raised when a keyword's value violates the Draft-07 meta-schema. Thrown at
87
+ * build time, before a validator that would enforce less than the document
88
+ * states can exist.
89
+ */
90
+ class MalformedSchemaError extends Error {
91
+ constructor(keyword, reason, value) {
92
+ const received = renderReceivedValue(value);
93
+ super(`JSON Schema keyword "${keyword}" has a value the Draft-07 meta-schema ` +
94
+ `does not allow: ${reason}. Received: ${received}.`);
95
+ this.name = "MalformedSchemaError";
96
+ this.keyword = keyword;
97
+ this.reason = reason;
98
+ this.received = received;
99
+ // Without this, `instanceof` fails when the package is compiled to ES5.
100
+ Object.setPrototypeOf(this, MalformedSchemaError.prototype);
101
+ }
102
+ }
103
+ exports.MalformedSchemaError = MalformedSchemaError;
@@ -0,0 +1,98 @@
1
+ // ===========================================================================
2
+ // L8 src/json-schema/malformed-schema-error.ts
3
+ //
4
+ // UnsupportedKeywordError refuses a keyword NAME Luq cannot honour. This
5
+ // refuses a keyword VALUE the Draft-07 meta-schema does not allow, which until
6
+ // it is refused does not fail — it quietly enforces less than the document
7
+ // says. `{"type":"strig"}` drops the unknown name, so the type check vanishes
8
+ // and the field starts accepting null; `{"pattern":{"source":"^SKU-"}}` reaches
9
+ // the regex constructor as a stringified object and compiles to one matching
10
+ // almost anything; `{"additionalProperties":"false"}` loses the closed-object
11
+ // guarantee; `{"required":"name"}` builds and then throws a raw TypeError
12
+ // inside validate() on the first request. A validator that silently drops a
13
+ // constraint is worse than one that refuses the schema, so the refusal is a
14
+ // typed error carrying the keyword, what the meta-schema requires, and a
15
+ // rendering of what was found instead.
16
+ //
17
+ // The two refusals stay separate classes, neither extending the other, because
18
+ // they answer different questions: "Luq cannot honour this keyword" versus
19
+ // "this document is not valid Draft-07". The first is a limit of the library
20
+ // and the second is a bug in the document; a caller catching one must not
21
+ // silently catch the other.
22
+ // ===========================================================================
23
+ /**
24
+ * The two shapes §4.4 lets a schema take, worded once. Several keywords take a
25
+ * schema as their value — `properties` members, `additionalProperties`, every
26
+ * position of the tuple form of `items` — and each is read by a different
27
+ * module; sharing the phrase is what stops the same refusal being explained
28
+ * three different ways to the same caller.
29
+ *
30
+ * It is a fragment, so it composes: "the value ", `the schema under "a" `.
31
+ */
32
+ export const SCHEMA_FORMS = "must be an object or a boolean";
33
+ /**
34
+ * How much of the offending value a message may carry. A schema node is
35
+ * arbitrarily large, and an error whose message is a megabyte long is unusable
36
+ * in a log line.
37
+ */
38
+ const RECEIVED_MAX_LENGTH = 80;
39
+ const TRUNCATION_MARKER = "...";
40
+ /**
41
+ * Renders an untrusted schema value for a message, in at most
42
+ * RECEIVED_MAX_LENGTH characters. The value comes from the same document that
43
+ * is already known to be malformed, so nothing about it can be assumed: it may
44
+ * be circular, hold a bigint, or be large enough to drown the message.
45
+ * Rendering must never be the thing that throws.
46
+ */
47
+ export function renderReceivedValue(value) {
48
+ if (value === undefined)
49
+ return "undefined";
50
+ if (typeof value === "function")
51
+ return "[function]";
52
+ if (typeof value === "symbol")
53
+ return "[symbol]";
54
+ return truncate(renderJson(value));
55
+ }
56
+ /**
57
+ * JSON.stringify throws on a circular reference and on a bigint anywhere in
58
+ * the value, and returns undefined for a value it has no encoding for at all.
59
+ * Both outcomes become a description of the shape rather than a failure.
60
+ */
61
+ function renderJson(value) {
62
+ try {
63
+ const rendered = JSON.stringify(value);
64
+ return rendered === undefined ? describeUnrenderable(value) : rendered;
65
+ }
66
+ catch {
67
+ return describeUnrenderable(value);
68
+ }
69
+ }
70
+ function describeUnrenderable(value) {
71
+ return Array.isArray(value)
72
+ ? "[unrenderable array]"
73
+ : `[unrenderable ${typeof value}]`;
74
+ }
75
+ function truncate(rendered) {
76
+ if (rendered.length <= RECEIVED_MAX_LENGTH)
77
+ return rendered;
78
+ const kept = RECEIVED_MAX_LENGTH - TRUNCATION_MARKER.length;
79
+ return `${rendered.slice(0, kept)}${TRUNCATION_MARKER}`;
80
+ }
81
+ /**
82
+ * Raised when a keyword's value violates the Draft-07 meta-schema. Thrown at
83
+ * build time, before a validator that would enforce less than the document
84
+ * states can exist.
85
+ */
86
+ export class MalformedSchemaError extends Error {
87
+ constructor(keyword, reason, value) {
88
+ const received = renderReceivedValue(value);
89
+ super(`JSON Schema keyword "${keyword}" has a value the Draft-07 meta-schema ` +
90
+ `does not allow: ${reason}. Received: ${received}.`);
91
+ this.name = "MalformedSchemaError";
92
+ this.keyword = keyword;
93
+ this.reason = reason;
94
+ this.received = received;
95
+ // Without this, `instanceof` fails when the package is compiled to ES5.
96
+ Object.setPrototypeOf(this, MalformedSchemaError.prototype);
97
+ }
98
+ }
@@ -0,0 +1,21 @@
1
+ /** The dialect this library implements, in the spelling the draft publishes. */
2
+ export declare const DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
3
+ /**
4
+ * Raised when a document's root `$schema` names a dialect Luq does not
5
+ * implement. Thrown at build time, before a validator that would read the
6
+ * document under the wrong dialect's rules can exist.
7
+ *
8
+ * `reason` follows the convention the other two refusals use: a sentence
9
+ * fragment with no terminal punctuation, saying what the dialect does that
10
+ * Draft-07 does not, so that two refusals from two different dialects read as
11
+ * one library rather than as two unrelated messages.
12
+ */
13
+ export declare class UnsupportedDialectError extends Error {
14
+ /** The `$schema` value the document wrote, verbatim. */
15
+ readonly declared: string;
16
+ /** The dialect Luq reads every document under. */
17
+ readonly implemented: string;
18
+ /** What that dialect does differently, or that it is unrecognised. */
19
+ readonly reason: string;
20
+ constructor(declared: string, reason: string);
21
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // ===========================================================================
3
+ // L8 src/json-schema/unsupported-dialect-error.ts
4
+ //
5
+ // The third refusal, and the only one with no keyword to hang on.
6
+ // UnsupportedKeywordError refuses a keyword NAME; MalformedSchemaError refuses
7
+ // a keyword VALUE. Both need the document to write something recognisable, and
8
+ // there is one divergence that writes nothing at all:
9
+ //
10
+ // {"$schema": "https://json-schema.org/draft/2020-12/schema",
11
+ // "$defs": {"name": {"type": "string"}},
12
+ // "properties": {"nick": {"$ref": "#/$defs/name", "minLength": 5}}}
13
+ //
14
+ // Every keyword there is a Draft-07 keyword spelled the Draft-07 way, so the
15
+ // keyword table sees nothing to refuse. The meaning is not Draft-07: §8.3 has
16
+ // `$ref` REPLACE the object it appears in, and from 2019-09 on `$ref` is an
17
+ // ordinary applicator whose siblings are applied. Read as Draft-07 the document
18
+ // above builds a validator that accepts `{"nick":"ab"}` — the `minLength` is
19
+ // gone, silently, and nothing in the result says a constraint went missing.
20
+ // `$schema` is the only signal the document offers, so it is the one read.
21
+ //
22
+ // It stays a separate class from the other two, neither extending the other,
23
+ // for the reason written in malformed-schema-error.ts: each answers a different
24
+ // question. "Luq cannot honour this keyword", "this document is not valid
25
+ // Draft-07", and now "this document is not Draft-07 AT ALL" are three distinct
26
+ // facts, and a caller catching one must not silently catch another.
27
+ // ===========================================================================
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.UnsupportedDialectError = exports.DRAFT07_DIALECT_URI = void 0;
30
+ /** The dialect this library implements, in the spelling the draft publishes. */
31
+ exports.DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
32
+ /**
33
+ * Raised when a document's root `$schema` names a dialect Luq does not
34
+ * implement. Thrown at build time, before a validator that would read the
35
+ * document under the wrong dialect's rules can exist.
36
+ *
37
+ * `reason` follows the convention the other two refusals use: a sentence
38
+ * fragment with no terminal punctuation, saying what the dialect does that
39
+ * Draft-07 does not, so that two refusals from two different dialects read as
40
+ * one library rather than as two unrelated messages.
41
+ */
42
+ class UnsupportedDialectError extends Error {
43
+ constructor(declared, reason) {
44
+ super(`JSON Schema dialect "${declared}" is not supported: ${reason}. Luq ` +
45
+ `implements Draft-07 (${exports.DRAFT07_DIALECT_URI}) and would read this ` +
46
+ "document under Draft-07 rules, which can enforce less than it " +
47
+ "states. Convert the document to Draft-07, or pass " +
48
+ "`{ assumeDraft07: true }` to take the Draft-07 reading deliberately.");
49
+ /** The dialect Luq reads every document under. */
50
+ this.implemented = exports.DRAFT07_DIALECT_URI;
51
+ this.name = "UnsupportedDialectError";
52
+ this.declared = declared;
53
+ this.reason = reason;
54
+ // Without this, `instanceof` fails when the package is compiled to ES5.
55
+ Object.setPrototypeOf(this, UnsupportedDialectError.prototype);
56
+ }
57
+ }
58
+ exports.UnsupportedDialectError = UnsupportedDialectError;
@@ -0,0 +1,54 @@
1
+ // ===========================================================================
2
+ // L8 src/json-schema/unsupported-dialect-error.ts
3
+ //
4
+ // The third refusal, and the only one with no keyword to hang on.
5
+ // UnsupportedKeywordError refuses a keyword NAME; MalformedSchemaError refuses
6
+ // a keyword VALUE. Both need the document to write something recognisable, and
7
+ // there is one divergence that writes nothing at all:
8
+ //
9
+ // {"$schema": "https://json-schema.org/draft/2020-12/schema",
10
+ // "$defs": {"name": {"type": "string"}},
11
+ // "properties": {"nick": {"$ref": "#/$defs/name", "minLength": 5}}}
12
+ //
13
+ // Every keyword there is a Draft-07 keyword spelled the Draft-07 way, so the
14
+ // keyword table sees nothing to refuse. The meaning is not Draft-07: §8.3 has
15
+ // `$ref` REPLACE the object it appears in, and from 2019-09 on `$ref` is an
16
+ // ordinary applicator whose siblings are applied. Read as Draft-07 the document
17
+ // above builds a validator that accepts `{"nick":"ab"}` — the `minLength` is
18
+ // gone, silently, and nothing in the result says a constraint went missing.
19
+ // `$schema` is the only signal the document offers, so it is the one read.
20
+ //
21
+ // It stays a separate class from the other two, neither extending the other,
22
+ // for the reason written in malformed-schema-error.ts: each answers a different
23
+ // question. "Luq cannot honour this keyword", "this document is not valid
24
+ // Draft-07", and now "this document is not Draft-07 AT ALL" are three distinct
25
+ // facts, and a caller catching one must not silently catch another.
26
+ // ===========================================================================
27
+ /** The dialect this library implements, in the spelling the draft publishes. */
28
+ export const DRAFT07_DIALECT_URI = "http://json-schema.org/draft-07/schema#";
29
+ /**
30
+ * Raised when a document's root `$schema` names a dialect Luq does not
31
+ * implement. Thrown at build time, before a validator that would read the
32
+ * document under the wrong dialect's rules can exist.
33
+ *
34
+ * `reason` follows the convention the other two refusals use: a sentence
35
+ * fragment with no terminal punctuation, saying what the dialect does that
36
+ * Draft-07 does not, so that two refusals from two different dialects read as
37
+ * one library rather than as two unrelated messages.
38
+ */
39
+ export class UnsupportedDialectError extends Error {
40
+ constructor(declared, reason) {
41
+ super(`JSON Schema dialect "${declared}" is not supported: ${reason}. Luq ` +
42
+ `implements Draft-07 (${DRAFT07_DIALECT_URI}) and would read this ` +
43
+ "document under Draft-07 rules, which can enforce less than it " +
44
+ "states. Convert the document to Draft-07, or pass " +
45
+ "`{ assumeDraft07: true }` to take the Draft-07 reading deliberately.");
46
+ /** The dialect Luq reads every document under. */
47
+ this.implemented = DRAFT07_DIALECT_URI;
48
+ this.name = "UnsupportedDialectError";
49
+ this.declared = declared;
50
+ this.reason = reason;
51
+ // Without this, `instanceof` fails when the package is compiled to ES5.
52
+ Object.setPrototypeOf(this, UnsupportedDialectError.prototype);
53
+ }
54
+ }
@@ -36,7 +36,10 @@ function describeAllowed(allowed) {
36
36
  function areAllMembersPrimitive(allowed) {
37
37
  return allowed.every((member) => member === null || typeof member !== "object");
38
38
  }
39
- /** One membership test, decided ONCE at build time and never re-decided. */
39
+ /** One membership test, decided ONCE at build time and never re-decided. The
40
+ * list must already be a snapshot: the linear path CLOSES OVER it rather than
41
+ * copying it into a Set, so a caller-owned array reaching here would stay
42
+ * readable — and mutable — through the returned validator. */
40
43
  function createMembershipTest(allowed) {
41
44
  if (allowed.length > SET_MEMBERSHIP_THRESHOLD &&
42
45
  areAllMembersPrimitive(allowed)) {
@@ -53,13 +56,24 @@ exports.oneOfPlugin = (0, plugin_definition_1.definePlugin)()({
53
56
  if (!(0, types_1.isArray)(allowed) || allowed.length === 0) {
54
57
  throw new plugin_definition_1.PluginArgumentError(ctx.pluginName, "allowed", allowed);
55
58
  }
56
- const isMember = createMembershipTest(allowed);
57
- const rendered = describeAllowed(allowed);
59
+ // A built validator is a SNAPSHOT of what it was built from, and this list
60
+ // arrives BY REFERENCE: reached through the `enum` keyword it is the very
61
+ // array inside the caller's schema document. Without this copy, appending
62
+ // to that array in place after build() — a config reload that mutates the
63
+ // document rather than replacing it — silently widens a validator that is
64
+ // already serving traffic. It would also do so only SOMETIMES: the Set
65
+ // path in createMembershipTest copies and the linear path does not, so the
66
+ // behaviour would flip at a member count the caller has no reason to know
67
+ // about. Copying here, at the one point where the caller's array enters
68
+ // the plugin, is what makes both paths and the reported `expected` agree.
69
+ const members = Object.freeze(allowed.slice());
70
+ const isMember = createMembershipTest(members);
71
+ const rendered = describeAllowed(members);
58
72
  return (0, create_rule_1.check)({
59
73
  code: ctx.code,
60
74
  messageFactory: ctx.messageFactory,
61
75
  severity: ctx.severity,
62
- run: (value) => isMember(value) ? types_1.PASS : (0, types_1.fail)({ expected: allowed, actual: value }),
76
+ run: (value) => isMember(value) ? types_1.PASS : (0, types_1.fail)({ expected: members, actual: value }),
63
77
  describe: () => `Value must be one of: ${rendered}`,
64
78
  buildMessageContext: () => ({}),
65
79
  });
@@ -33,7 +33,10 @@ function describeAllowed(allowed) {
33
33
  function areAllMembersPrimitive(allowed) {
34
34
  return allowed.every((member) => member === null || typeof member !== "object");
35
35
  }
36
- /** One membership test, decided ONCE at build time and never re-decided. */
36
+ /** One membership test, decided ONCE at build time and never re-decided. The
37
+ * list must already be a snapshot: the linear path CLOSES OVER it rather than
38
+ * copying it into a Set, so a caller-owned array reaching here would stay
39
+ * readable — and mutable — through the returned validator. */
37
40
  function createMembershipTest(allowed) {
38
41
  if (allowed.length > SET_MEMBERSHIP_THRESHOLD &&
39
42
  areAllMembersPrimitive(allowed)) {
@@ -50,13 +53,24 @@ export const oneOfPlugin = /*#__PURE__*/ definePlugin()({
50
53
  if (!isArray(allowed) || allowed.length === 0) {
51
54
  throw new PluginArgumentError(ctx.pluginName, "allowed", allowed);
52
55
  }
53
- const isMember = createMembershipTest(allowed);
54
- const rendered = describeAllowed(allowed);
56
+ // A built validator is a SNAPSHOT of what it was built from, and this list
57
+ // arrives BY REFERENCE: reached through the `enum` keyword it is the very
58
+ // array inside the caller's schema document. Without this copy, appending
59
+ // to that array in place after build() — a config reload that mutates the
60
+ // document rather than replacing it — silently widens a validator that is
61
+ // already serving traffic. It would also do so only SOMETIMES: the Set
62
+ // path in createMembershipTest copies and the linear path does not, so the
63
+ // behaviour would flip at a member count the caller has no reason to know
64
+ // about. Copying here, at the one point where the caller's array enters
65
+ // the plugin, is what makes both paths and the reported `expected` agree.
66
+ const members = Object.freeze(allowed.slice());
67
+ const isMember = createMembershipTest(members);
68
+ const rendered = describeAllowed(members);
55
69
  return check({
56
70
  code: ctx.code,
57
71
  messageFactory: ctx.messageFactory,
58
72
  severity: ctx.severity,
59
- run: (value) => isMember(value) ? PASS : fail({ expected: allowed, actual: value }),
73
+ run: (value) => isMember(value) ? PASS : fail({ expected: members, actual: value }),
60
74
  describe: () => `Value must be one of: ${rendered}`,
61
75
  buildMessageContext: () => ({}),
62
76
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedog/luq",
3
- "version": "2.4.4",
3
+ "version": "2.6.0",
4
4
  "description": "Universal Model & API Definition Platform - TypeScript validation library evolving into cross-language code generation",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -459,18 +459,20 @@
459
459
  "typecheck:src": "tsc --noEmit -p tsconfig.json",
460
460
  "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
461
461
  "typecheck:bench": "tsc --noEmit -p bench/tsconfig.json",
462
- "generate": "npm run generate:sources && npm run generate:exports && npm run generate:lock",
462
+ "generate": "npm run generate:sources && npm run generate:exports && npm run generate:lock && npm run generate:issue-codes",
463
463
  "generate:sources": "npm run generate:manifest && npm run generate:barrel && npm run generate:slot-catalog",
464
464
  "generate:slot-catalog": "npx ts-node --project scripts/tsconfig.json scripts/generate-slot-catalog.ts",
465
465
  "generate:manifest": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-manifest.ts",
466
466
  "generate:barrel": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-barrel.ts",
467
467
  "generate:exports": "npx ts-node --project scripts/tsconfig.json scripts/generate-package-exports.ts",
468
468
  "generate:lock": "npx ts-node --project scripts/tsconfig.json scripts/generate-plugin-catalog.ts",
469
+ "generate:issue-codes": "npx ts-node --project scripts/tsconfig.json scripts/generate-issue-code-catalog.ts",
469
470
  "check:exports": "npx ts-node --project scripts/tsconfig.json scripts/check-exports.ts --mode=exact",
470
471
  "check:catalog-lock": "npx ts-node --project scripts/tsconfig.json scripts/check-catalog-lock.ts",
472
+ "check:issue-code-lock": "npx ts-node --project scripts/tsconfig.json scripts/check-issue-code-lock.ts",
471
473
  "check:plugin-isolation": "npx ts-node --project scripts/tsconfig.json scripts/check-plugin-isolation.ts",
472
474
  "check:plugin-uniqueness": "npx ts-node --project scripts/tsconfig.json scripts/check-plugin-uniqueness.ts",
473
- "check:catalog": "npm run check:plugin-uniqueness && npm run check:plugin-isolation && npm run check:exports && npm run check:catalog-lock",
475
+ "check:catalog": "npm run check:plugin-uniqueness && npm run check:plugin-isolation && npm run check:exports && npm run check:catalog-lock && npm run check:issue-code-lock",
474
476
  "check:size": "npx ts-node --project scripts/tsconfig.json scripts/measure-bundle-size.ts",
475
477
  "check:barrel-equivalence": "npx ts-node --project scripts/tsconfig.json scripts/check-barrel-equivalence.ts",
476
478
  "check:no-dynamic-code": "npx ts-node --project scripts/tsconfig.json scripts/check-no-dynamic-code.ts",
@@ -493,6 +495,7 @@
493
495
  "check:conformance-figures": "npx ts-node --project scripts/tsconfig.json scripts/check-conformance-figures.ts",
494
496
  "bench:competitors": "npx ts-node --project bench/tsconfig.json bench/competitors/report-competitors.ts",
495
497
  "bench:competitors:check": "npx ts-node --project bench/tsconfig.json bench/competitors/check-competitors.ts",
498
+ "bench:megamorphism": "npx ts-node --project bench/tsconfig.json bench/megamorphism/report-megamorphism.ts",
496
499
  "lint:ox": "oxlint -c .oxlintrc.json src bench",
497
500
  "lint:naming": "eslint \"src/**/*.ts\" \"bench/**/*.ts\"",
498
501
  "generate:perf-figures": "npx ts-node --project scripts/tsconfig.json scripts/check-perf-figures.ts --write",
@@ -538,9 +541,9 @@
538
541
  "ts-node": "^10.9.2",
539
542
  "tsx": "^4.20.3",
540
543
  "typescript": "^5.8.3",
541
- "valibot": "^1.4.2",
544
+ "valibot": "^1.5.0",
542
545
  "yup": "^1.7.1",
543
- "zod": "^4.0.14"
546
+ "zod": "^4.6.2"
544
547
  },
545
548
  "sideEffects": false
546
549
  }