@jarenjs/core 0.72.0 → 0.72.3

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.
package/ARCHITECTURE.md CHANGED
@@ -83,7 +83,7 @@ flowchart TB
83
83
  Integer["integer.js<br/>Int8/16/32/64 validation"]
84
84
  Float["float.js<br/>Float16/32/64 validation"]
85
85
  BigIntModule["bigint.js<br/>BigInt utilities"]
86
- NumberModule["number.js<br/>Number coercion"]
86
+ NumberModule["number.js<br/>Coercion and JSON number grammar"]
87
87
  end
88
88
 
89
89
  subgraph CollectionModules["Collection Modules"]
@@ -445,6 +445,7 @@ flowchart TB
445
445
  RegExpUtils --> IsRegExp["isRegExpType()"]
446
446
  RegExpUtils --> IsStringRegExp["isStringRegExp()<br/>(tests if valid pattern)"]
447
447
  RegExpUtils --> CreateRegExp["createRegExp()<br/>(handles /pattern/flags syntax)"]
448
+ RegExpUtils --> TestRegExp["createRegExpTester()<br/>(repeatable, isolated global/sticky state)"]
448
449
 
449
450
  Unicode["Unicode Support"]
450
451
  Unicode --> Ascii["isAsciiString()"]
package/README.md CHANGED
@@ -11,8 +11,8 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
11
11
  | `@jarenjs/core` | type guards and getters (`isStringType`, `isObjectClass`, `getIntegerType`, ...) |
12
12
  | `@jarenjs/core/array` | array helpers (`isUniqueArray`, `getUniqueArray`, `includesAll`, ...) |
13
13
  | `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` and deep `isJsonValue` predicates, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
14
- | `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), cached regex compilation, the suite's one content hash (`fnv1a` and the `hashContent` fingerprint over it) and `kebabCase` |
15
- | `@jarenjs/core/cache` | the bounded LRU (`createBoundedCache`), the reference-keyed `createWeakCache`, and `createSemanticCache` — keyed by what a value IS, for caches whose entries decide a result |
14
+ | `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), regex compilation and repeatable `createRegExpTester` predicates, the suite's one content hash (`fnv1a` and the `hashContent` fingerprint over it) and `kebabCase` |
15
+ | `@jarenjs/core/cache` | the bounded LRU with key deletion (`createBoundedCache`), the reference-keyed `createWeakCache`, and `createSemanticCache` — keyed by what a value IS, for caches whose entries decide a result |
16
16
  | `@jarenjs/core/random` | the suite's one seeded generator (`mulberry32`, pinned sequence, ToUint32 seed) and the draws built on it: `randomInt` over a half-open range, in-place Fisher–Yates `shuffle`, and `drawDistinct` — `k` distinct indices from one stream |
17
17
  | `@jarenjs/core/runtime` | the runtime record — `createRuntime({ now, uuid, random, zoneProvider })`, frozen, defaulting member for member to the platform's own (`Date.now`, `crypto.randomUUID`, `Math.random`, no zone provider) — that the store (its query deadlines included), the jobs engine, the migration runner, every contract binding and the contract memory ledger take as `runtime`, so a deterministic run is configured once; a subsystem's own explicit option wins over the record, and the record reaches hosts, never query compilation |
18
18
  | `@jarenjs/core/stats` | descriptive statistics over a sample: `mean`, sample `variance`/`stddev`, the midpoint `median`, and `quantile(values, p, { method })` — `p` on 0..1 under a NAMED rule, `'nearest-rank'` or `'linear'`, because a default would decide silently; an empty sample answers `undefined`, never `0` |
@@ -21,7 +21,7 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
21
21
  | `@jarenjs/core/scan` | char-code constants and predicates for recursive-descent parsers |
22
22
  | `@jarenjs/core/message` | the message template/catalog compiler shared by the validator and the form layer |
23
23
  | `@jarenjs/core/color` | pure color math (`lerpColor` — hex `#rrggbb` interpolation) |
24
- | `@jarenjs/core/number` | boolean/number/integer coercion helpers (`isIntishType`, ...) |
24
+ | `@jarenjs/core/number` | boolean/number/integer coercion helpers (`isIntishType`, ...) and `isJsonNumberString`, the strict lexical JSON-number predicate shared by query casts and schema normalization; conversion and overflow policy stay with each caller |
25
25
  | `@jarenjs/core/integer` | `int8` ... `uint64` ranges and validators |
26
26
  | `@jarenjs/core/float` | `float16` ... `float64` constants, validators, increment/decrement |
27
27
  | `@jarenjs/core/bigint` | bigint helpers (`BigInt_min`, `BigInt_MinMax`, ...) |
@@ -41,14 +41,16 @@ Deep imports work too (`@jarenjs/core/text/email`, `@jarenjs/core/math/vec2f64`,
41
41
  `@jarenjs/core/string` counts string length the way JSON Schema expects — by grapheme cluster, not UTF-16 code units — without paying for `Intl.Segmenter` unless the string actually needs it:
42
42
 
43
43
  ```javascript
44
- import { getStringLength, isAsciiString, createRegExp } from '@jarenjs/core/string';
44
+ import { getStringLength, isAsciiString, createRegExp, createRegExpTester } from '@jarenjs/core/string';
45
45
 
46
46
  getStringLength('hello', true); // 5 (ASCII fast path: str.length)
47
47
  getStringLength('héllo', true); // 5 (surrogate-aware code point count)
48
48
  getStringLength('👨‍👩‍👧‍👦', true); // 1 (grapheme segmentation, only when clusters can form)
49
49
  getStringLength('👨‍👩‍👧‍👦'); // 11 (default: plain UTF-16 length)
50
50
 
51
- createRegExp('^\\p{L}+$'); // cached, unicode-flagged RegExp
51
+ createRegExp('^\\p{L}+$'); // unicode-flagged RegExp
52
+ const matches = createRegExpTester('/^x/gi');
53
+ matches('X'); matches('X'); // true both times; global/sticky state is isolated
52
54
  ```
53
55
 
54
56
  ## Text validation
@@ -88,7 +90,7 @@ orient2d(0, 0, 1, 0, 0, 1); // > 0 — counter-clockwise, exa
88
90
  haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 m (Amsterdam–Paris)
89
91
  ringWinding([[0,0],[1,0],[1,1],[0,1],[0,0]]); // 1 — an RFC 7946 exterior ring
90
92
  bboxOf({ type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,52]]] }); // [4, 52, 5, 53]
91
- geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string, so a prefix test is proximity
93
+ geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string identifying a cell; use distances for proximity
92
94
 
93
95
  wktToGeoJson('POINT (4.9041 52.3676)'); // { type: 'Point', coordinates: [4.9041, 52.3676] }
94
96
  geoJsonToWkt({ type: 'Point', coordinates: [4.9041, 52.3676] }); // 'POINT (4.9041 52.3676)'
@@ -1,18 +1,13 @@
1
1
  /**
2
- * @file The suite's one bounded-cache primitive. Before this file the
3
- * FIFO-512 delete-oldest map was written five times (the query engine's
4
- * string cache, the JSONPath query cache, forms' three pointer caches)
5
- * with a sixth divergent flush-all variant in forms' regex cache and a
6
- * true LRU in the view projection memo. One implementation, one policy:
2
+ * @file The suite's bounded-cache primitive for compiled queries,
3
+ * pointers, regular expressions and view projections. One eviction policy
4
+ * keeps each consumer's retained working set bounded:
7
5
  *
8
6
  * - **LRU with recency refresh**: a `get` hit re-inserts the entry, so
9
7
  * the evicted entry is the least recently USED, not the oldest
10
- * inserted. (FIFO versus LRU was never result-observable at any call
11
- * site — both bound memory; LRU keeps hot entries hotter.)
8
+ * inserted.
12
9
  * - **Evict at `size >= limit` before inserting a new key**, so the
13
- * cache never holds more than `limit` entries. (The old sites
14
- * disagreed between `>=`-before and `>`-after; the capacity is the
15
- * same, the invariant here is simply "never above `limit`".)
10
+ * cache never holds more than `limit` entries for a positive integer limit.
16
11
  * - `undefined` is the miss sentinel: a cache MUST NOT store
17
12
  * `undefined` as a value (store `null` for "computed, negative" —
18
13
  * the regex cache does exactly that).
@@ -44,6 +39,11 @@ export type BoundedCache<K, V> = {
44
39
  * or compute-and-insert in one step.
45
40
  */
46
41
  getOrCreate: (key: K, create: (key: K) => V) => V;
42
+ /**
43
+ * - Drop one entry; true when the
44
+ * key existed, including an entry whose value was undefined.
45
+ */
46
+ delete: (key: K) => boolean;
47
47
  /**
48
48
  * - Drop every entry.
49
49
  */
@@ -62,6 +62,8 @@ export type BoundedCache<K, V> = {
62
62
  * the least recently used entry when the cache is full.
63
63
  * @property {(key: K, create: (key: K) => V) => V} getOrCreate - Lookup
64
64
  * or compute-and-insert in one step.
65
+ * @property {(key: K) => boolean} delete - Drop one entry; true when the
66
+ * key existed, including an entry whose value was undefined.
65
67
  * @property {() => void} clear - Drop every entry.
66
68
  * @property {() => number} size - Current entry count.
67
69
  */
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Whether a string is exactly one JSON number (RFC 8259 section 6), with
3
+ * no surrounding whitespace. This checks spelling only: `1e999` is valid
4
+ * even though converting it to a JavaScript number overflows. Callers
5
+ * choose their own conversion, finiteness and safe-integer policies.
6
+ * @param {unknown} value - The value to check, without coercion
7
+ * @returns {boolean} True for a string matching the complete number grammar
8
+ */
9
+ export declare function isJsonNumberString(value: unknown): boolean;
1
10
  /**
2
11
  * Checks if the given data is of boolean type or boolean-like (the strings 'true' or 'false').
3
12
  * @param {any} data - The data to check.
@@ -1,16 +1,13 @@
1
1
  /**
2
- * @file The runtime record: the host facts every subsystem that needs
3
- * one used to take separately the clock, secure identifiers,
4
- * randomness and the zone provider — as one frozen record a host builds
2
+ * @file The runtime record: the clock, secure identifiers, randomness
3
+ * and the zone provider as one frozen record a host builds
5
4
  * once and hands to the store, the jobs engine, the migration runner and
6
5
  * the http binding. Nothing here IS a clock, a random source or a zone
7
6
  * database: the defaults are the platform's own (`Date.now`,
8
- * `crypto.randomUUID`, `Math.random`, and no zone provider, which keeps
9
- * a named zone the refusal it always was), so adopting the record changes
10
- * nothing a consumer can observe. What it buys is that a deterministic
11
- * run a fixed clock, a seeded generator, a counting identifier — is
12
- * configured in one place, and that four subsystems can no longer
13
- * disagree about what time it is.
7
+ * `crypto.randomUUID`, `Math.random`, and no zone provider). Named zones
8
+ * require an injected provider. A deterministic run a fixed clock,
9
+ * a seeded generator, a counting identifier is configured in one place
10
+ * so the subsystems agree about time and identity.
14
11
  *
15
12
  * Precedence is fixed: a subsystem's own explicit option wins over the
16
13
  * record's member, which wins over the built-in default. The options are
@@ -1,13 +1,10 @@
1
1
  /**
2
2
  * @file The JSON Schema constraint-keyword vocabulary, grouped by the
3
- * value family each keyword constrains. Before this file the same
4
- * keyword lists were spelled four times (forms' constraint extraction,
5
- * emit's dropped-constraint table, the validator's `$data` dispatch
6
- * order and its `$ref`-sibling detection) and a keyword added to one
7
- * list silently missed the others. Each site composes the list it wants
8
- * from these groups and appends its own extras; the loops stay where
9
- * they are, because the drift risk was always in the data, not the
10
- * code.
3
+ * value family each keyword constrains. Forms' constraint extraction,
4
+ * emit's dropped-constraint table and the validator's dispatch and
5
+ * `$ref`-sibling detection compose their lists from these shared groups
6
+ * and append their own extras. Consumers own their processing loops;
7
+ * the shared vocabulary keeps keyword membership consistent.
11
8
  *
12
9
  * ORDER IS PART OF THE CONTRACT: the validator's `$data` dispatch
13
10
  * applies keywords in list order and its error order is observable
@@ -33,6 +33,13 @@ export declare function isStringRegExp(data: string | RegExp | null | undefined)
33
33
  * @returns {RegExp | undefined}
34
34
  */
35
35
  export declare function createRegExp(pattern: string | RegExp | null | undefined): RegExp | undefined;
36
+ /**
37
+ * Compile a pattern into a repeatable predicate. Global and sticky patterns
38
+ * start at index zero on every call and never change a caller's RegExp state.
39
+ * @param {string | RegExp | null | undefined} pattern - The regular expression
40
+ * @returns {((value: string) => boolean) | undefined} The tester, or undefined when no pattern was supplied
41
+ */
42
+ export declare function createRegExpTester(pattern: string | RegExp | null | undefined): ((value: string) => boolean) | undefined;
36
43
  export declare function getSegmenter(): Intl.Segmenter;
37
44
  /**
38
45
  * @param {string} str
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/core",
3
3
  "private": false,
4
- "version": "0.72.0",
4
+ "version": "0.72.3",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
package/src/cache.js CHANGED
@@ -1,19 +1,14 @@
1
1
  //@ts-check
2
2
  /**
3
- * @file The suite's one bounded-cache primitive. Before this file the
4
- * FIFO-512 delete-oldest map was written five times (the query engine's
5
- * string cache, the JSONPath query cache, forms' three pointer caches)
6
- * with a sixth divergent flush-all variant in forms' regex cache and a
7
- * true LRU in the view projection memo. One implementation, one policy:
3
+ * @file The suite's bounded-cache primitive for compiled queries,
4
+ * pointers, regular expressions and view projections. One eviction policy
5
+ * keeps each consumer's retained working set bounded:
8
6
  *
9
7
  * - **LRU with recency refresh**: a `get` hit re-inserts the entry, so
10
8
  * the evicted entry is the least recently USED, not the oldest
11
- * inserted. (FIFO versus LRU was never result-observable at any call
12
- * site — both bound memory; LRU keeps hot entries hotter.)
9
+ * inserted.
13
10
  * - **Evict at `size >= limit` before inserting a new key**, so the
14
- * cache never holds more than `limit` entries. (The old sites
15
- * disagreed between `>=`-before and `>`-after; the capacity is the
16
- * same, the invariant here is simply "never above `limit`".)
11
+ * cache never holds more than `limit` entries for a positive integer limit.
17
12
  * - `undefined` is the miss sentinel: a cache MUST NOT store
18
13
  * `undefined` as a value (store `null` for "computed, negative" —
19
14
  * the regex cache does exactly that).
@@ -41,6 +36,8 @@ import { semanticKey } from './object.js';
41
36
  * the least recently used entry when the cache is full.
42
37
  * @property {(key: K, create: (key: K) => V) => V} getOrCreate - Lookup
43
38
  * or compute-and-insert in one step.
39
+ * @property {(key: K) => boolean} delete - Drop one entry; true when the
40
+ * key existed, including an entry whose value was undefined.
44
41
  * @property {() => void} clear - Drop every entry.
45
42
  * @property {() => number} size - Current entry count.
46
43
  */
@@ -84,7 +81,8 @@ export function createBoundedCache(limit) {
84
81
  return value;
85
82
  }
86
83
 
87
- return { get, set, getOrCreate, clear: () => map.clear(), size: () => map.size };
84
+ return { get, set, getOrCreate, delete: (key) => map.delete(key),
85
+ clear: () => map.clear(), size: () => map.size };
88
86
  }
89
87
 
90
88
  /**
package/src/number.js CHANGED
@@ -1,5 +1,19 @@
1
1
  //@ts-check
2
2
 
3
+ const JSON_NUMBER_RE = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
4
+
5
+ /**
6
+ * Whether a string is exactly one JSON number (RFC 8259 section 6), with
7
+ * no surrounding whitespace. This checks spelling only: `1e999` is valid
8
+ * even though converting it to a JavaScript number overflows. Callers
9
+ * choose their own conversion, finiteness and safe-integer policies.
10
+ * @param {unknown} value - The value to check, without coercion
11
+ * @returns {boolean} True for a string matching the complete number grammar
12
+ */
13
+ export function isJsonNumberString(value) {
14
+ return typeof value === 'string' && JSON_NUMBER_RE.test(value);
15
+ }
16
+
3
17
  /**
4
18
  * Checks if the given data is of boolean type or boolean-like (the strings 'true' or 'false').
5
19
  * @param {any} data - The data to check.
package/src/runtime.js CHANGED
@@ -1,17 +1,14 @@
1
1
  //@ts-check
2
2
  /**
3
- * @file The runtime record: the host facts every subsystem that needs
4
- * one used to take separately the clock, secure identifiers,
5
- * randomness and the zone provider — as one frozen record a host builds
3
+ * @file The runtime record: the clock, secure identifiers, randomness
4
+ * and the zone provider as one frozen record a host builds
6
5
  * once and hands to the store, the jobs engine, the migration runner and
7
6
  * the http binding. Nothing here IS a clock, a random source or a zone
8
7
  * database: the defaults are the platform's own (`Date.now`,
9
- * `crypto.randomUUID`, `Math.random`, and no zone provider, which keeps
10
- * a named zone the refusal it always was), so adopting the record changes
11
- * nothing a consumer can observe. What it buys is that a deterministic
12
- * run a fixed clock, a seeded generator, a counting identifier — is
13
- * configured in one place, and that four subsystems can no longer
14
- * disagree about what time it is.
8
+ * `crypto.randomUUID`, `Math.random`, and no zone provider). Named zones
9
+ * require an injected provider. A deterministic run a fixed clock,
10
+ * a seeded generator, a counting identifier is configured in one place
11
+ * so the subsystems agree about time and identity.
15
12
  *
16
13
  * Precedence is fixed: a subsystem's own explicit option wins over the
17
14
  * record's member, which wins over the built-in default. The options are
package/src/schema.js CHANGED
@@ -1,14 +1,11 @@
1
1
  //@ts-check
2
2
  /**
3
3
  * @file The JSON Schema constraint-keyword vocabulary, grouped by the
4
- * value family each keyword constrains. Before this file the same
5
- * keyword lists were spelled four times (forms' constraint extraction,
6
- * emit's dropped-constraint table, the validator's `$data` dispatch
7
- * order and its `$ref`-sibling detection) and a keyword added to one
8
- * list silently missed the others. Each site composes the list it wants
9
- * from these groups and appends its own extras; the loops stay where
10
- * they are, because the drift risk was always in the data, not the
11
- * code.
4
+ * value family each keyword constrains. Forms' constraint extraction,
5
+ * emit's dropped-constraint table and the validator's dispatch and
6
+ * `$ref`-sibling detection compose their lists from these shared groups
7
+ * and append their own extras. Consumers own their processing loops;
8
+ * the shared vocabulary keeps keyword membership consistent.
12
9
  *
13
10
  * ORDER IS PART OF THE CONTRACT: the validator's `$data` dispatch
14
11
  * applies keywords in list order and its error order is observable
package/src/string.js CHANGED
@@ -88,6 +88,24 @@ export function createRegExp(pattern) {
88
88
  throw new Error(`Unknown Regular Expression Pattern Type: ${pattern}`);
89
89
  }
90
90
 
91
+ /**
92
+ * Compile a pattern into a repeatable predicate. Global and sticky patterns
93
+ * start at index zero on every call and never change a caller's RegExp state.
94
+ * @param {string | RegExp | null | undefined} pattern - The regular expression
95
+ * @returns {((value: string) => boolean) | undefined} The tester, or undefined when no pattern was supplied
96
+ */
97
+ export function createRegExpTester(pattern) {
98
+ const parsed = createRegExp(pattern);
99
+ if (parsed === undefined) return undefined;
100
+ if (!parsed.global && !parsed.sticky)
101
+ return (value) => parsed.test(value);
102
+ const owned = new RegExp(parsed.source, parsed.flags);
103
+ return (value) => {
104
+ owned.lastIndex = 0;
105
+ return owned.test(value);
106
+ };
107
+ }
108
+
91
109
  /**
92
110
  * @type {Intl.Segmenter | null}
93
111
  */