@jarenjs/validate 0.8.4 → 0.34.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 (53) hide show
  1. package/ARCHITECTURE.md +1131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +796 -2
  4. package/dist/types/array.d.ts +2 -0
  5. package/dist/types/bigint.d.ts +1 -0
  6. package/dist/types/combine.d.ts +1 -0
  7. package/dist/types/condition.d.ts +1 -0
  8. package/dist/types/content.d.ts +3 -0
  9. package/dist/types/data.d.ts +7 -0
  10. package/dist/types/dollar-data.d.ts +11 -0
  11. package/dist/types/dynamic-ref.d.ts +44 -0
  12. package/dist/types/enum.d.ts +1 -0
  13. package/dist/types/format.d.ts +21 -0
  14. package/dist/types/index.d.ts +972 -0
  15. package/dist/types/messages.d.ts +142 -0
  16. package/dist/types/normalize.d.ts +107 -0
  17. package/dist/types/number.d.ts +1 -0
  18. package/dist/types/object.d.ts +3 -0
  19. package/dist/types/query-keyword.d.ts +19 -0
  20. package/dist/types/query.d.ts +29 -0
  21. package/dist/types/schema.d.ts +1 -0
  22. package/dist/types/string.d.ts +1 -0
  23. package/dist/types/tools.d.ts +109 -0
  24. package/dist/types/traverse.d.ts +32 -0
  25. package/dist/types/unevaluated.d.ts +12 -0
  26. package/docs/ERROR-MESSAGES.md +251 -0
  27. package/package.json +37 -7
  28. package/src/array.js +610 -0
  29. package/src/bigint.js +108 -0
  30. package/src/combine.js +276 -0
  31. package/src/condition.js +129 -0
  32. package/src/content.js +83 -0
  33. package/src/data.js +101 -0
  34. package/src/dollar-data.js +212 -0
  35. package/src/dynamic-ref.js +121 -0
  36. package/src/enum.js +147 -0
  37. package/src/format.js +108 -0
  38. package/src/index.js +1896 -0
  39. package/src/messages.js +497 -0
  40. package/src/normalize.js +585 -0
  41. package/src/number.js +169 -0
  42. package/src/object.js +848 -0
  43. package/src/query-keyword.js +99 -0
  44. package/src/query.js +85 -0
  45. package/src/schema.js +690 -0
  46. package/src/string.js +164 -0
  47. package/src/tools.js +397 -0
  48. package/src/traverse.js +442 -0
  49. package/src/unevaluated.js +173 -0
  50. package/dist/index.js +0 -1998
  51. package/dist/index.js.map +0 -7
  52. package/dist/index.min.js +0 -2
  53. package/dist/index.min.js.map +0 -7
@@ -0,0 +1,1131 @@
1
+ # @jarenjs/validate Architecture
2
+
3
+ **IMPORTANT**: Update this document only when introducing architectural changes. If multiple architects are involved, ensure consensus on significant changes.
4
+
5
+ ## Overview
6
+
7
+ **@jarenjs/validate** is a high-performance JSON Schema validator for JavaScript/TypeScript. It implements a **compilation-based architecture** that transforms JSON Schemas into optimized validation functions at build time, rather than interpreting schemas during validation. This design choice delivers significant performance advantages over traditional interpretation-based validators.
8
+
9
+ ### Core Design Philosophy
10
+
11
+ ```
12
+ Schema → Compile → Optimized Validator Function → Execute against Data
13
+ ```
14
+
15
+ The validator embraces these architectural principles:
16
+
17
+ 1. **Compile-Time Optimization**: Schemas are parsed and compiled once, generating specialized validation functions
18
+ 2. **Fast Path Specialization**: Common schema patterns receive dedicated optimized code paths
19
+ 3. **Zero-Copy Validation**: Minimal object allocations during validation through careful memory management
20
+ 4. **Reference Pre-Resolution**: All `$ref` references are resolved and flattened at compile time
21
+ 5. **Lazy Error Generation**: Error objects are only created when explicitly requested
22
+
23
+ ### Key Files
24
+
25
+ | File | Purpose |
26
+ |------|---------|
27
+ | `packages/validate/src/index.js` | Main validator classes (`JarenValidator`, `ValidationRoot`, `ValidationObject`) |
28
+ | `packages/validate/src/traverse.js` | Schema traversal and ref resolution (`storeSchemaIdsInMap`, `restoreSchemaRefsInMap`) |
29
+ | `packages/validate/src/schema.js` | Schema compilation dispatcher (`compileSchemaObject`) |
30
+ | `packages/validate/src/messages.js` | Error conversion, message catalogs, the `errorMessage` keyword specs (`ValidationError`, `messagesEn`, `localizeErrors`) |
31
+ | `packages/validate/src/array.js` | Array validation logic |
32
+ | `packages/validate/src/object.js` | Object validation logic |
33
+ | `packages/validate/src/string.js` | String validation logic |
34
+ | `packages/validate/src/number.js` | Number validation logic |
35
+
36
+ ---
37
+
38
+ ## Core Design Principles
39
+
40
+ ### 1. Compile-Time Optimization
41
+
42
+ The most important architectural insight: **all ref resolution must happen at compile time**. This means:
43
+
44
+ - `$ref` chains are flattened during schema loading
45
+ - Validation functions are pre-compiled before any data is validated
46
+ - No URI resolution happens during validation
47
+
48
+ ### 2. Lazy vs Eager Evaluation Trade-offs
49
+
50
+ Refs are resolved eagerly at compile time, never lazily on first validation. This keeps the first `validate(data)` call as fast as every subsequent one:
51
+
52
+ ```javascript
53
+ const validate = compile(schema); // All refs resolved here
54
+ validate(data); // Direct function call, no resolution
55
+ ```
56
+
57
+ ### 3. Function Inlining
58
+
59
+ To reduce call stack depth, simple schemas compile to inline validation functions rather than delegating to helper functions:
60
+
61
+ ```javascript
62
+ // Before: Multiple function calls
63
+ function validate(data) {
64
+ return validateType(data) && validateString(data);
65
+ }
66
+
67
+ // After: example, but use @jarenjs/core
68
+ function validate(data) {
69
+ if (typeof data !== 'string') return false;
70
+ if (data.length > maxLength) return false;
71
+ return true;
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Four-Phase Architecture
78
+
79
+ The validator operates in four distinct phases that cleanly separate concerns:
80
+
81
+ ### Phase 1: Schema Registration
82
+
83
+ **Purpose**: Build a map of all reachable schemas by URI.
84
+
85
+ **Entry Point**: `JarenValidator.addSchema(schema, key)`
86
+
87
+ **Flow**:
88
+ ```mermaid
89
+ flowchart TD
90
+ A["addSchema(schema, key)"]
91
+ B["store schema under key<br/>(and the alt key with/without #)"]
92
+ C["#traverseAndStoreIds(baseUri, schema)"]
93
+ D["storeSchemaIdsInMap(schemasMap, baseUri, schema)"]
94
+ E["store the root schema under baseUri"]
95
+ F{"BFS over the schema structure"}
96
+ G["$id — store subschema, update baseUri"]
97
+ H["$anchor — store anchor"]
98
+ I["$ref — store a null placeholder, marked as a ref"]
99
+ J["objects / arrays — keep traversing"]
100
+ A --> B
101
+ A --> C
102
+ C --> D
103
+ D --> E
104
+ D --> F
105
+ F --> G
106
+ F --> H
107
+ F --> I
108
+ F --> J
109
+ N["the placeholder is what lets refs<br/>be added in any order"]
110
+ I -.- N
111
+ class N note
112
+ ```
113
+
114
+ **Key Data Structure**: `#schemas Map<string, schema|null>`
115
+
116
+ - Keys are normalized URIs (e.g., `http://example.com/schema#`, `#/$defs/foo`)
117
+ - Values are either the schema object or `null` (for refs that point elsewhere)
118
+ - Stored at `JarenValidator` instance level (survives multiple `compile()` calls)
119
+
120
+ **Important**: At this phase, refs are stored as `null` placeholders. The actual resolution happens later.
121
+
122
+ ### Phase 2: Schema Traversal
123
+
124
+ **Purpose**: Create a schemas map for a specific compilation, merging instance schemas with passed schemas.
125
+
126
+ **Entry Point**: `JarenValidator.compile(schema, schemas)`
127
+
128
+ **Flow**:
129
+ ```
130
+ compile(schema, schemas)
131
+ └── #traverseSchema(schema, schemas, instanceSchemas)
132
+ ├── Create new schemaMap from instanceSchemas
133
+ ├── storeSchemaIdsInMap(schemaMap, origin, schema)
134
+ ├── (Optional) storeSchemaIdsInMap for additional schemas
135
+ └── restoreSchemaRefsInMap(schemaMap)
136
+ └── For each null entry:
137
+ ├── Try resolveRefSchemaDeep (flatten chains)
138
+ └── On failure: resolveRefSchemaShallow (single hop)
139
+ ```
140
+
141
+ **Key Data Structure**: `schemaMap Map<string, schema>` (local to this compilation)
142
+
143
+ - Merges instance-level schemas (`this.#schemas`) with compile-time schemas
144
+ - After `restoreSchemaRefsInMap`, all ref values are resolved schemas (not null)
145
+ - **Ref chains are flattened**: `A → B → C` becomes `A → C`
146
+
147
+ **The Critical Optimization**: `restoreSchemaRefsInMap` now uses `resolveRefSchemaDeep` instead of `resolveRefSchemaShallow`. This follows the entire ref chain at load time and stores the final schema directly.
148
+
149
+ ### Phase 3: Compilation
150
+
151
+ **Purpose**: Create compiled validator functions from schemas.
152
+
153
+ **Entry Point**: `JarenValidator.#compileSchemaWithRoot(...)`
154
+
155
+ **Flow**:
156
+ ```
157
+ compile()
158
+ ├── Create ValidationRoot(origin, schemaMap, formats, options)
159
+ ├── #precompileRefs(root, schemaMap, origin)
160
+ │ └── For each ref in schemaMap:
161
+ │ ├── Skip if already compiled
162
+ │ └── root.createObject(id, schema, origin)
163
+ │ └── ValidationObject(root, id, schema, baseUri)
164
+ │ └── compileValidator() → returns validator function
165
+ └── Return jarenValidateSchema(data) function
166
+ ```
167
+
168
+ **Key Classes**:
169
+
170
+ #### ValidationRoot
171
+ - Manages the `Map<string, ValidationObject>` called `#objects`
172
+ - Entry point: `validate(data)` clears errors and calls `#firstSchema.validate(data, data)`
173
+ - Creates objects via `createObject()` and caches them in `#objects`
174
+ - **Pre-compilation**: Now creates ValidationObjects for ALL refs before returning
175
+
176
+ #### ValidationObject
177
+ - Represents a single schema location (identified by URI)
178
+ - **Constructor**: Immediately compiles validator via `compileValidator()`
179
+ - **Key property**: `#validator` - the compiled function that validates data
180
+ - For refs: `#validator` is the target validator function (direct reference)
181
+
182
+ **The Critical Optimization**: `#precompileRefs()` iterates through `schemaMap` and creates `ValidationObject` instances for every ref. This moves object creation from validation-time to compile-time.
183
+
184
+ ### Phase 4: Validation
185
+
186
+ **Purpose**: Validate data against compiled schema.
187
+
188
+ **Entry Point**: `jarenValidateSchema(data)`
189
+
190
+ **Flow**:
191
+ ```
192
+ jarenValidateSchema(data)
193
+ └── root.validate(data)
194
+ └── #firstSchema.validate(data, dataRoot)
195
+ └── this.#validator(data, dataRoot)
196
+ └── Either:
197
+ ├── compileSchemaObject() result (for non-ref schemas)
198
+ └── Target validator function (for refs, pre-compiled)
199
+ ```
200
+
201
+ **Key Insight**: For refs, `this.#validator` is **already** the target validator function (set during Phase 3 pre-compilation). No resolution happens at validation time.
202
+
203
+ ---
204
+
205
+ ## High-Level Architecture
206
+
207
+ ```mermaid
208
+ flowchart TB
209
+ subgraph "Public API Layer"
210
+ A[JarenValidator]
211
+ B[ValidationOptions]
212
+ C[ValidatorOptions]
213
+ end
214
+
215
+ subgraph "Compilation Layer"
216
+ D[ValidationRoot]
217
+ E[ValidationObject]
218
+ F[Schema Compilation]
219
+ end
220
+
221
+ subgraph "Schema Processing"
222
+ G[Schema Traversal]
223
+ H[Reference Resolution]
224
+ I[JSON Pointer Handling]
225
+ end
226
+
227
+ subgraph "Keyword Compilers"
228
+ J[Type Validators]
229
+ K[String Keywords]
230
+ L[Number Keywords]
231
+ M[Array Keywords]
232
+ N[Object Keywords]
233
+ O[Combine Keywords]
234
+ P[Conditional Keywords]
235
+ end
236
+
237
+ subgraph "Runtime Dependencies"
238
+ Q[@jarenjs/core]
239
+ end
240
+
241
+ A --> D
242
+ D --> E
243
+ E --> F
244
+ F --> J & K & L & M & N & O & P
245
+ G --> H
246
+ H --> I
247
+ D --> G
248
+ J & K & L & M & N & O & P --> Q
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Module Organization
254
+
255
+ ```mermaid
256
+ flowchart LR
257
+ subgraph "Entry Point"
258
+ INDEX[index.js]
259
+ end
260
+
261
+ subgraph "Core Classes"
262
+ INDEX --> VR[ValidationRoot]
263
+ INDEX --> VO[ValidationObject]
264
+ INDEX --> JV[JarenValidator]
265
+ end
266
+
267
+ subgraph "Schema Compilation"
268
+ VO --> SCHEMA[schema.js]
269
+ SCHEMA --> TYPE[type validation]
270
+ SCHEMA --> COMPILE[compileSchemaObject]
271
+ end
272
+
273
+ subgraph "Keyword Modules"
274
+ SCHEMA --> STRING[string.js]
275
+ SCHEMA --> NUMBER[number.js]
276
+ SCHEMA --> BIGINT[bigint.js]
277
+ SCHEMA --> ARRAY[array.js]
278
+ SCHEMA --> OBJECT[object.js]
279
+ SCHEMA --> COMBINE[combine.js]
280
+ SCHEMA --> CONDITION[condition.js]
281
+ SCHEMA --> ENUM[enum.js]
282
+ SCHEMA --> FORMAT[format.js]
283
+ SCHEMA --> CONTENT[content.js]
284
+ SCHEMA --> DATA[data.js]
285
+ SCHEMA --> DDLR[dollar-data.js]
286
+ SCHEMA --> UNEVAL[unevaluated.js]
287
+ end
288
+
289
+ subgraph "Infrastructure"
290
+ INDEX --> TRAVERSE[traverse.js]
291
+ INDEX --> TOOLS[tools.js]
292
+ INDEX --> DYNREF[dynamic-ref.js]
293
+ end
294
+
295
+ subgraph "External Dependencies"
296
+ TOOLS --> CORE[@jarenjs/core]
297
+ end
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Core Classes
303
+
304
+ ### JarenValidator
305
+
306
+ The primary entry point for consumers. Manages schema registration, format registration, and orchestrates the compilation process.
307
+
308
+ ```mermaid
309
+ classDiagram
310
+ class JarenValidator {
311
+ -Map #schemas
312
+ -Map #metaSchemas
313
+ -object #formats
314
+ -ValidatorOptions #options
315
+ +addSchema(schema, key)
316
+ +addFormat(name, compiler)
317
+ +addFormats(compilers)
318
+ +compile(schema)
319
+ +validateSchema(schema)
320
+ +addMetaSchema(schema, key)
321
+ +getSchema(key)
322
+ }
323
+
324
+ class ValidationRoot {
325
+ -string #rootOrigin
326
+ -Map #schemas
327
+ -object #formats
328
+ -ValidationOptions #options
329
+ -TraverseOptions #traverse
330
+ -Map #objects
331
+ -Array #errors
332
+ -ValidationObject #firstSchema
333
+ +validate(data)
334
+ +createObject(path, schema, baseUri)
335
+ +resolveObject(ref, path, schema)
336
+ +addError(error)
337
+ }
338
+
339
+ class ValidationObject {
340
+ -ValidationRoot #root
341
+ -string #path
342
+ -Array #members
343
+ -object #schema
344
+ -function #validator
345
+ -string #baseUri
346
+ -string #effectiveBaseUri
347
+ +createValidator(schema, key, index)
348
+ +createErrorHandler(expected, key)
349
+ +validate(data, dataPath, dataRoot)
350
+ }
351
+
352
+ JarenValidator --> ValidationRoot : creates
353
+ ValidationRoot --> ValidationObject : manages
354
+ ValidationObject --> ValidationObject : creates children
355
+ ```
356
+
357
+ ### ValidationRoot
358
+
359
+ The compilation context that:
360
+ - Maintains the schema registry (URI → Schema mapping)
361
+ - Manages format validators
362
+ - Houses all compiled ValidationObjects
363
+ - Collects validation errors
364
+ - Provides `$ref` resolution services
365
+
366
+ **Owner threading**: the constructor takes a sixth argument `owner`
367
+ (default `null`), exposed through an `owner` getter, and both construction
368
+ sites in `index.js` pass the owning `JarenValidator` for it. This lets
369
+ `query-keyword.js` hand that instance to `createTypeTestCompiler`, so the
370
+ schema literals inside a `$query` document (`$valid`/`$assert`/`$as`)
371
+ resolve their `$ref`s against the owner's `addSchema` registrations. The
372
+ resulting ESM cycle — `index.js` → `schema.js` → `query-keyword.js` →
373
+ `query.js` → `index.js` — is benign: `query.js` only *reads* the hoisted
374
+ `JarenValidator` declaration at call time, never at module-evaluation time.
375
+
376
+ ### ValidationObject
377
+
378
+ Represents a single schema location with its compiled validator:
379
+ - **Identity**: URI path identifying this schema object
380
+ - **State**: The schema definition and compiled validator function
381
+ - **Relationships**: Parent root reference, child members
382
+ - **Behavior**: Creates child validators, handles error generation
383
+
384
+ ---
385
+
386
+ ## Compilation Pipeline
387
+
388
+ ```mermaid
389
+ sequenceDiagram
390
+ participant User
391
+ participant JV as JarenValidator
392
+ participant VR as ValidationRoot
393
+ participant VO as ValidationObject
394
+ participant SC as Schema Compilers
395
+ participant TD as Type-specific Compilers
396
+
397
+ User->>JV: compile(schema)
398
+ JV->>JV: #traverseSchema()
399
+ Note over JV: Build schema map with<br/>all $id and $ref locations
400
+
401
+ JV->>VR: new ValidationRoot(origin, schemas, formats, opts)
402
+ VR->>VR: Pre-compile all refs
403
+
404
+ VR->>VO: create root ValidationObject
405
+ VO->>VO: compileValidator()
406
+
407
+ alt Has $ref
408
+ VO->>VR: resolveObject(ref)
409
+ VR-->>VO: cached ValidationObject
410
+ else Complex Schema
411
+ VO->>SC: compileSchemaObject()
412
+ SC->>TD: compile keywords
413
+ TD-->>SC: validators[]
414
+ SC->>SC: compose validators
415
+ SC-->>VO: compiled function
416
+ end
417
+
418
+ VO-->>VR: ready
419
+ VR-->>JV: ValidationRoot ready
420
+ JV-->>User: validation function
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Schema Compilation Flow
426
+
427
+ ```mermaid
428
+ flowchart TD
429
+ A[JSON Schema Input] --> B{Schema Type}
430
+
431
+ B -->|true| C[Return trueThat]
432
+ B -->|false| D[Return false validator]
433
+ B -->|object| E{Has $ref?}
434
+
435
+ E -->|Yes| F[Delegate to ref resolver]
436
+ E -->|No| G{Fast Path?}
437
+
438
+ G -->|Type only| H[compileTypeSimple]
439
+ G -->|Required only| I[compileRequired]
440
+ G -->|MinLength only| J[compileMinLengthFast]
441
+ G -->|Complex| K[Full Compilation]
442
+
443
+ K --> L[compileTypeBasic]
444
+ K --> M[compileEnumBasic]
445
+ K --> N[compileDollarDataSchema]
446
+ K --> O[compileNumberBasic]
447
+ K --> P[compileBigIntBasic]
448
+ K --> Q[compileStringBasic]
449
+ K --> R[compileFormatBasic]
450
+ K --> S[compileContentSchema]
451
+ K --> T[compileArraySchema]
452
+ K --> U[compileObjectSchema]
453
+ K --> V[compileCombineSchema]
454
+ K --> W[compileConditionSchema]
455
+
456
+ L & M & N & O & P & Q & R & S & T & U & V & W --> X[Compose validators]
457
+ X --> Y{Count}
458
+
459
+ Y -->|0| C
460
+ Y -->|1| Z[Return single]
461
+ Y -->|2| AA[Dual validator]
462
+ Y -->|3| AB[Triple validator]
463
+ Y -->|4+| AC[Loop validator]
464
+ ```
465
+
466
+ ---
467
+
468
+ ## Keyword Compiler Architecture
469
+
470
+ Each keyword module follows a consistent pattern:
471
+
472
+ ```mermaid
473
+ flowchart LR
474
+ A[Schema Object] --> B[Extract Keyword Value]
475
+ B --> C{Value Valid?}
476
+ C -->|No| D[Return undefined]
477
+ C -->|Yes| E[Create Error Handler]
478
+ E --> F[Compile Validator Function]
479
+ F --> G[Return Closure]
480
+
481
+ style D fill:#f99
482
+ style G fill:#9f9
483
+ ```
484
+
485
+ ### Example: String Keyword Compilation
486
+
487
+ ```mermaid
488
+ flowchart TD
489
+ A[compileStringBasic] --> B[compileStringIntern]
490
+
491
+ B --> C[compileMinLength]
492
+ B --> D[compileMaxLength]
493
+ B --> E[compilePattern]
494
+
495
+ C --> F{min > 0?}
496
+ D --> G{max >= 0?}
497
+ E --> H{pattern valid?}
498
+
499
+ F -->|No| I[undefined]
500
+ F -->|Yes| J[minLength validator]
501
+ G -->|No| K[undefined]
502
+ G -->|Yes| L[maxLength validator]
503
+ H -->|No| M[undefined]
504
+ H -->|Yes| N[pattern validator]
505
+
506
+ I & J & K & L & M & N --> O{Any validators?}
507
+
508
+ O -->|No| P[Return undefined]
509
+ O -->|Yes| Q{Fast path?}
510
+
511
+ Q -->|Max only| R[maxOnly validator]
512
+ Q -->|Min only| S[minOnly validator]
513
+ Q -->|Pattern only| T[patternOnly validator]
514
+ Q -->|Complex| U[full validator]
515
+
516
+ style P fill:#f99
517
+ style R fill:#9f9
518
+ style S fill:#9f9
519
+ style T fill:#9f9
520
+ style U fill:#9f9
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Reference Resolution System
526
+
527
+ The `$ref` resolution is one of the most critical performance optimizations. Rather than resolving references at validation time, all references are pre-compiled:
528
+
529
+ ```mermaid
530
+ flowchart TD
531
+ A[Schema with $ref] --> B[Traverse Schema]
532
+ B --> C[Build Schema Map]
533
+ C --> D[Store all $id locations]
534
+ C --> E[Store null placeholders for $refs]
535
+
536
+ E --> F[restoreSchemaRefsInMap]
537
+ F --> G[resolveRefSchemaDeep]
538
+
539
+ G --> H{Chain resolution}
540
+ H --> I[Flatten a→b→c to a→c]
541
+
542
+ I --> J[Pre-compile ValidationObjects]
543
+ J --> K[Cache by URI]
544
+
545
+ K --> L[Validation time]
546
+ L --> M{Ref in cache?}
547
+ M -->|Yes| N[Direct lookup O1]
548
+ M -->|No| O[Lazy resolve fallback]
549
+
550
+ style N fill:#9f9
551
+ style O fill:#ff9
552
+ ```
553
+
554
+ ### JSON Pointer Handling
555
+
556
+ ```mermaid
557
+ classDiagram
558
+ class TraverseOptions {
559
+ +string origin
560
+ +boolean mergeSchemas
561
+ +boolean anchorsGlobal
562
+ +boolean anchorsAllowed
563
+ +boolean skipErrors
564
+ }
565
+
566
+ class JsonPointer {
567
+ +string id
568
+ +string search
569
+ +string leftUri
570
+ +string fragment
571
+ }
572
+
573
+ TraverseOptions --|> JsonPointerOptions
574
+ JsonPointerOptions --> JsonPointer : creates via createJsonPointer()
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Data Reference Systems
580
+
581
+ The validator implements two complementary data reference mechanisms:
582
+
583
+ ```mermaid
584
+ flowchart TB
585
+ subgraph "$data Keyword Ajv-style"
586
+ A1[Relative JSON Pointer] --> A2[resolveRelativePointer]
587
+ A2 --> A3[Access parent/ancestor data]
588
+ A3 --> A4[Dynamic constraint values]
589
+ A4 --> A5[Example: maximum: {$data: '1/larger'}]
590
+ end
591
+
592
+ subgraph "data Keyword json-everything-style"
593
+ B1[JSON Pointer Path] --> B2[resolveDataRef]
594
+ B2 --> B3[Access from data root]
595
+ B3 --> B4[Cross-property validation]
596
+ B4 --> B5[Example: data: {minimum: '/A'}]
597
+ end
598
+
599
+ A5 --> C[Runtime value resolution]
600
+ B5 --> C
601
+ ```
602
+
603
+ **Compile-failure fallback**: the ref compilers of `@jarenjs/json`
604
+ (`compileDataRef` for the `data` keyword, `compileRelativeJSONPointer` for
605
+ `$data`) *throw* on a malformed reference. `data.js` and `dollar-data.js`
606
+ each wrap the compile in a `compileRefResolver` try/catch that falls back to
607
+ an always-`JSONPOINTER_NOTHING` resolver. A malformed `$data`/`data`
608
+ reference therefore validates as "not found" — the keyword asserts nothing —
609
+ instead of throwing at compile time, keeping the lax keyword semantics.
610
+
611
+ ---
612
+
613
+ ## Error Handling Architecture
614
+
615
+ ```mermaid
616
+ flowchart LR
617
+ A[Validation Failure] --> B{skipErrors?}
618
+
619
+ B -->|true| C[Return false only]
620
+ B -->|false| D[Create InternalValidationError]
621
+
622
+ D --> E[Store in ValidationRoot.errors]
623
+ E --> F[Continue validation]
624
+
625
+ F --> G[All validators complete]
626
+ G --> H{collectErrors?}
627
+
628
+ H -->|true| I[convertInternalErrors - messages.js]
629
+ H -->|false| J[Return boolean only]
630
+
631
+ I --> K[Extract params, resolve msgid,<br/>match errorMessage registry,<br/>render through catalog]
632
+ K --> L[Return {valid, errors}]
633
+
634
+ style C fill:#9f9
635
+ style J fill:#9f9
636
+ style L fill:#9f9
637
+ ```
638
+
639
+ ### Report-Time Messages (messages.js)
640
+
641
+ Conversion is structured-first, render-late (the normative spec is
642
+ [docs/ERROR-MESSAGES.md](./docs/ERROR-MESSAGES.md)):
643
+
644
+ - Every public `ValidationError` carries a stable `msgid` (the matched
645
+ `errorMessage` spec's `$msgid`, else the `$query` runtime code, else the
646
+ keyword) plus raw `params`; `instancePath` is a straight read of the
647
+ first meta argument every handler call site passes (the
648
+ handler-contract invariant), behind a charCode guard that yields `''`
649
+ rather than ever a wrong path.
650
+ - The `errorMessage` keyword compiles at schema compile time into a
651
+ registry on `ValidationRoot` (`registerErrorMessage`) — no validator
652
+ closure is emitted and the single-keyword fast paths stay eligible (the
653
+ key count excludes it). Matching happens only over the failed set:
654
+ nearest registered ancestor by segment-aware prefix; map-form entries
655
+ and `_` apply at the node itself, the string form covers the subtree.
656
+ - Human text renders through catalogs — plain objects of closures /
657
+ template strings (`messagesEn` built in; packs in `@jarenjs/locales`).
658
+ `localizeErrors(errors, catalog)` re-renders post hoc from
659
+ `msgid` + `params`; the `messages: false` option skips rendering
660
+ entirely (`message: ''`).
661
+
662
+ ### Error Handler Creation
663
+
664
+ ```mermaid
665
+ sequenceDiagram
666
+ participant Compiler
667
+ participant VO as ValidationObject
668
+ participant VR as ValidationRoot
669
+ participant User
670
+
671
+ Compiler->>VO: createErrorHandler(expected, keyword)
672
+ VO->>VR: check options.skipErrors
673
+
674
+ alt skipErrors = true
675
+ VR-->>VO: fast no-op handler
676
+ VO-->>Compiler: () => false
677
+ else
678
+ VR-->>VO: full error handler
679
+ VO-->>Compiler: (data, ...meta) => {...}
680
+ end
681
+
682
+ Compiler->>User: validation function with handlers
683
+ ```
684
+
685
+ ---
686
+
687
+ ## Performance Optimizations
688
+
689
+ ### 1. Fast Path Specialization
690
+
691
+ ```mermaid
692
+ flowchart TD
693
+ A[Schema Analysis] --> B{Common Pattern?}
694
+
695
+ B -->|type only| C[Inline type check]
696
+ B -->|required only| D[Inline required check]
697
+ B -->|minLength only| E[Direct length compare]
698
+ B -->|Complex| F[Full composition]
699
+
700
+ C --> G[No function call overhead]
701
+ D --> G
702
+ E --> G
703
+ F --> H[Minimal overhead]
704
+
705
+ G --> I[Maximum Performance]
706
+ H --> I
707
+ ```
708
+
709
+ ### 2. Validator Composition Strategies
710
+
711
+ Based on the number of validators, different composition strategies are used:
712
+
713
+ | Validator Count | Strategy | Implementation |
714
+ |-----------------|----------|----------------|
715
+ | 0 | No-op | `trueThat` |
716
+ | 1 | Direct | Return validator directly |
717
+ | 2 | Unrolled | `return v1(data) && v2(data)` |
718
+ | 3 | Unrolled | `return v1(data) && v2(data) && v3(data)` |
719
+ | 4+ | Loop | `for (let i = 0; i < n; i++)` |
720
+
721
+ ### 3. String Length Optimization
722
+
723
+ ```mermaid
724
+ flowchart TD
725
+ A[String validation] --> B{useGrapheme?}
726
+
727
+ B -->|false| C[data.length]
728
+ B -->|true| D{ASCII check}
729
+
730
+ D -->|All ASCII| E[data.length]
731
+ D -->|Non-ASCII| F{Cluster-forming chars?}
732
+
733
+ F -->|No| J[Code point count]
734
+ F -->|Yes| K[Segmenter iteration]
735
+
736
+ C --> G[Fast path]
737
+ E --> G
738
+ J --> G
739
+ K --> H[Slow path]
740
+
741
+ G --> I[Inline in hot loop]
742
+ H --> I
743
+ ```
744
+
745
+ ---
746
+
747
+ ## Module Dependencies
748
+
749
+ ```mermaid
750
+ flowchart TB
751
+ subgraph "@jarenjs/validate"
752
+ direction TB
753
+ INDEX[index.js]
754
+ SCHEMA[schema.js]
755
+
756
+ subgraph "Type Compilers"
757
+ STR[string.js]
758
+ NUM[number.js]
759
+ BINT[bigint.js]
760
+ ARR[array.js]
761
+ OBJ[object.js]
762
+ end
763
+
764
+ subgraph "Logic Compilers"
765
+ COMB[combine.js]
766
+ COND[condition.js]
767
+ ENUM[enum.js]
768
+ end
769
+
770
+ subgraph "Content Compilers"
771
+ FMT[format.js]
772
+ CONT[content.js]
773
+ end
774
+
775
+ subgraph "Data Reference"
776
+ DATA[data.js]
777
+ DDLR[dollar-data.js]
778
+ end
779
+
780
+ subgraph "Infrastructure"
781
+ TRAV[traverse.js]
782
+ TOOLS[tools.js]
783
+ end
784
+ end
785
+
786
+ subgraph "@jarenjs/core"
787
+ CORE[index.js]
788
+ CORE_NUM[number.js]
789
+ CORE_STR[string.js]
790
+ CORE_ARR[array.js]
791
+ CORE_OBJ[object.js]
792
+ CORE_FN[function.js]
793
+ CORE_TXT[text/index.js]
794
+ end
795
+
796
+ subgraph "@jarenjs/json"
797
+ CORE_JSON[index.js]
798
+ end
799
+
800
+ INDEX --> SCHEMA & TRAV & TOOLS & FMT
801
+ SCHEMA --> STR & NUM & BINT & ARR & OBJ & COMB & COND & ENUM & CONT & DATA & DDLR
802
+
803
+ INDEX --> CORE
804
+ SCHEMA --> CORE
805
+ STR --> CORE_STR & CORE_FN
806
+ NUM --> CORE_NUM & CORE_FN
807
+ BINT --> CORE_NUM & CORE_FN
808
+ ARR --> CORE_ARR & CORE_OBJ & CORE_FN
809
+ OBJ --> CORE_OBJ & CORE_ARR & CORE_FN
810
+ ENUM --> CORE_OBJ
811
+ CONT --> CORE_TXT & CORE_JSON
812
+ DATA --> CORE_JSON
813
+ DDLR --> CORE_JSON
814
+ TOOLS --> CORE & CORE_STR & CORE_ARR
815
+ TRAV --> CORE & CORE_NUM & CORE_STR & CORE_TXT
816
+ ```
817
+
818
+ ---
819
+
820
+ ## Extension Points
821
+
822
+ ### Format Registration
823
+
824
+ ```mermaid
825
+ flowchart LR
826
+ A[Custom Format] --> B[Format Compiler]
827
+ B --> C[Returns Validator Function]
828
+ C --> D[Register via addFormat]
829
+ D --> E[Available in schemas]
830
+
831
+ style A fill:#9cf
832
+ style E fill:#9f9
833
+ ```
834
+
835
+ ### Custom Keywords
836
+
837
+ To add custom keywords, you would extend the compilation pipeline in `schema.js`:
838
+
839
+ ```javascript
840
+ // In compileSchemaObject or a new module
841
+ function compileCustomKeyword(schemaObj, jsonSchema) {
842
+ if (jsonSchema.myKeyword === undefined) return undefined;
843
+
844
+ const addError = schemaObj.createErrorHandler(value, 'myKeyword');
845
+
846
+ return function validateMyKeyword(data, dataPath) {
847
+ // Custom validation logic
848
+ return isValid(data) || addError(data, dataPath);
849
+ };
850
+ }
851
+ ```
852
+
853
+ ---
854
+
855
+ ## Testing Strategy
856
+
857
+ The architecture supports comprehensive testing at multiple levels:
858
+
859
+ ```mermaid
860
+ flowchart TB
861
+ subgraph "Unit Tests"
862
+ A[Individual Compilers]
863
+ B[Utility Functions]
864
+ C[Type Checkers]
865
+ end
866
+
867
+ subgraph "Integration Tests"
868
+ D[Full Schema Compilation]
869
+ E[Reference Resolution]
870
+ F[Error Collection]
871
+ end
872
+
873
+ subgraph "Compliance Tests"
874
+ G[JSON Schema Test Suite]
875
+ H[Draft 6/7/2019-09/2020-12]
876
+ end
877
+
878
+ A & B & C --> I[Test Coverage]
879
+ D & E & F --> I
880
+ G & H --> I
881
+ ```
882
+
883
+ ---
884
+
885
+ ## Data Structures
886
+
887
+ ### schemasMap Structure
888
+
889
+ ```typescript
890
+ Map<string, schema | null> {
891
+ // Root schemas
892
+ "http://example.com/schema#" → { type: "object", properties: {...} }
893
+
894
+ // Internal refs (after Phase 2, these point to final schemas)
895
+ "#/$defs/foo" → { type: "string" } // Flattened: was A→B→C, now A→C
896
+ "#/$defs/bar" → { $ref: "#/$defs/foo" } // If not yet resolved
897
+
898
+ // Anchors
899
+ "http://example.com/schema#myAnchor" → { type: "number" }
900
+ "#myAnchor" → { type: "number" } // Global anchor
901
+ }
902
+ ```
903
+
904
+ ### ValidationRoot.#objects Structure
905
+
906
+ ```typescript
907
+ Map<string, ValidationObject> {
908
+ "http://example.com/schema#" → ValidationObject {
909
+ #path: "http://example.com/schema#",
910
+ #validator: function validateObject(data, root) {...},
911
+ #schema: { type: "object", ... },
912
+ #root: ValidationRoot
913
+ },
914
+
915
+ "#/$defs/foo" → ValidationObject {
916
+ #path: "#/$defs/foo",
917
+ #validator: function validateString(data, root) {...}, // Direct reference
918
+ #schema: { type: "string" },
919
+ #root: ValidationRoot
920
+ }
921
+ }
922
+ ```
923
+
924
+ ---
925
+
926
+ ## Key Functions Reference
927
+
928
+ | Function | File | Purpose |
929
+ |----------|------|---------|
930
+ | `storeSchemaIdsInMap` | traverse.js | BFS traversal storing all schema IDs and refs |
931
+ | `resolveRefSchemaShallow` | traverse.js | Resolve single ref hop (baseUri + fragment) |
932
+ | `resolveRefSchemaDeep` | traverse.js | Follow ref chain to final schema |
933
+ | `restoreSchemaRefsInMap` | traverse.js | Flatten all ref chains at load time |
934
+ | `createJsonPointer` | traverse.js | Parse URI into components (id, leftUri, fragment) |
935
+ | `JarenValidator.compile` | index.js | Main entry point for schema compilation, draft/vocabulary selection |
936
+ | `JarenValidator.#precompileRefs` | index.js | Pre-create ValidationObjects for all refs |
937
+ | `ValidationObject.compileValidator` | index.js | Compile validator, ref combination, dynamic-anchor registration |
938
+ | `ValidationRoot.resolveObject` | index.js | Resolve ref to ValidationObject (fallback only) |
939
+ | `wrapUnevaluated` | unevaluated.js | Final-stage unevaluatedProperties/unevaluatedItems check |
940
+ | `EvalLog` | tools.js | Annotation log with mark/rollback for unevaluated* |
941
+ | `collectDynamicAnchorsDeep` | dynamic-ref.js | Gather a resource's $dynamicAnchors for scope entry |
942
+
943
+ ---
944
+
945
+ ## Implementation Notes
946
+
947
+ ### How $ref Works
948
+
949
+ 1. **Draft 7 and earlier - $ref ignores siblings**: when a schema has `$ref`, all other keywords are ignored, the referenced schema completely replaces the current schema, and a sibling `$id` does not affect `$ref` resolution
950
+ 2. **Draft 2019-09 and later - $ref has siblings**: `$ref` is just another keyword; sibling keywords apply together with the referenced schema, and a sibling `$id` DOES establish the base URI the `$ref` resolves against
951
+ 3. **Location-independent identifiers**: Anchors like `#foo` should be resolvable within their document
952
+ 4. **Ref chain resolution happens at compile time**: pre-resolved via `restoreSchemaRefsInMap` and `#precompileRefs`
953
+
954
+ ### Base URI Resolution
955
+
956
+ When schemas have `$id` that changes the base URI:
957
+
958
+ ```json
959
+ {
960
+ "$id": "http://example.com/schema",
961
+ "$defs": {
962
+ "nested": {
963
+ "$id": "nested/",
964
+ "$defs": {
965
+ "deep": {
966
+ "$ref": "folder/file.json" // Resolves against http://example.com/nested/
967
+ }
968
+ }
969
+ }
970
+ }
971
+ }
972
+ ```
973
+
974
+ **Resolution Rules**:
975
+ 1. `$id` changes the base URI for itself and all children
976
+ 2. `$ref` is resolved against the current base URI
977
+ 3. Relative `$id` values resolve against the parent's base URI
978
+ 4. Trailing `#` is stripped for URL resolution
979
+
980
+ ### ECMAScript Regex in JSON Schema
981
+
982
+ 1. **Unicode flag required**: All regex patterns use the `u` flag for proper Unicode support
983
+ 2. **Unicode property escapes**: Patterns using `\p{...}` require the `u` flag
984
+ 3. **Surrogate pairs**: Non-BMP characters (like emojis) are represented as surrogate pairs in JavaScript
985
+
986
+ ---
987
+
988
+ ## Debugging Guide
989
+
990
+ ### "Can not resolve schema for 'X'"
991
+
992
+ **Cause**: The ref `X` is not in schemasMap
993
+
994
+ **Check**:
995
+ - Was the schema containing `X` added via `addSchema()`?
996
+ - Is the ref path correct in the schema?
997
+ - After `restoreSchemaRefsInMap`, all refs should have non-null values
998
+
999
+ ### Performance Degradation on First Validation
1000
+
1001
+ **Cause**: Refs being resolved at validation time instead of compile time
1002
+
1003
+ **Check**:
1004
+ - Is `#precompileRefs` being called in `compile()`?
1005
+ - Are ValidationObjects being created for all refs?
1006
+ - Verify: `compileValidator` should find targets immediately without fallback
1007
+
1008
+ ### Test Interference (tests pass individually but fail together)
1009
+
1010
+ **Cause**: Global shared state between validators
1011
+
1012
+ **Check**:
1013
+ - Are you using a global cache? (Don't)
1014
+ - Solution: All state should be per-ValidationRoot or per-JarenValidator
1015
+
1016
+ ### Debugging Ref Resolution
1017
+
1018
+ Enable debug logging in traverse.js:
1019
+
1020
+ ```javascript
1021
+ // In packages/validate/src/traverse.js
1022
+ console.log('Resolving ref:', ref, 'against baseUri:', baseUri);
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ## Related Documentation
1028
+
1029
+ For broader context on how this package fits into the JarenJS ecosystem:
1030
+
1031
+ - **Project-wide Architecture**: See `docs/ARCHITECTURE.md`
1032
+ - **Developer Guide**: See `docs/HOWTO.md`
1033
+ - **Core Package**: Depends on `@jarenjs/core` for fundamental utilities and `@jarenjs/json` for the JSON addressing standards
1034
+
1035
+ ---
1036
+
1037
+ ## Key Files Reference
1038
+
1039
+ | File | Purpose | Key Exports |
1040
+ |------|---------|-------------|
1041
+ | `index.js` | Public API | `JarenValidator`, `ValidationOptions`, `ValidatorOptions` |
1042
+ | `messages.js` | Error conversion & i18n | `ValidationError`, `convertInternalErrors`, `messagesEn`, `compileMessageTemplate`, `compileMessageCatalog`, `renderErrorMessage`, `localizeErrors`, `compileErrorMessageSpec` |
1043
+ | `schema.js` | Schema compilation | `compileSchemaObject` |
1044
+ | `traverse.js` | Schema traversal | `TraverseOptions`, `storeSchemaIdsInMap`, `resolveRefSchemaDeep` |
1045
+ | `tools.js` | Shared utilities | `isBoolOrObjectClass`, `hasSchemaRef`, `createIsSchemaTypeHandler` |
1046
+ | `string.js` | String keywords | `compileStringBasic` |
1047
+ | `number.js` | Number keywords | `compileNumberBasic` |
1048
+ | `bigint.js` | BigInt keywords | `compileBigIntBasic` |
1049
+ | `array.js` | Array keywords | `compileArraySchema` |
1050
+ | `object.js` | Object keywords | `compileObjectSchema` |
1051
+ | `combine.js` | Logic keywords | `compileCombineSchema` |
1052
+ | `condition.js` | If/then/else | `compileConditionSchema` |
1053
+ | `enum.js` | Enum/const | `compileEnumBasic` |
1054
+ | `format.js` | Format registry | `registerFormatCompiler`, `compileFormatBasic` |
1055
+ | `content.js` | Content encoding | `compileContentSchema` |
1056
+ | `data.js` | Data keyword | `compileDataSchema` |
1057
+ | `dollar-data.js` | $data keyword | `compileDollarDataSchema` |
1058
+ | `unevaluated.js` | unevaluated* keywords | `wrapUnevaluated` |
1059
+ | `dynamic-ref.js` | Dynamic scope helpers | `collectDynamicAnchorsDeep`, `hasRecursiveAnchor`, `getDynamicAnchorName` |
1060
+ | `query-keyword.js` | `$query` extension keyword | `compileQuerySchema` |
1061
+
1062
+ ---
1063
+
1064
+ ## Architectural Decisions
1065
+
1066
+ ### 1. Compilation over Interpretation
1067
+
1068
+ **Decision**: Compile schemas to functions rather than interpret them at runtime.
1069
+
1070
+ **Rationale**:
1071
+ - Eliminates schema traversal during validation
1072
+ - Enables V8 optimization of hot paths
1073
+ - Allows fast-path specialization
1074
+
1075
+ **Trade-off**: Higher memory usage for storing compiled functions.
1076
+
1077
+ ### 2. Pre-compiled References
1078
+
1079
+ **Decision**: Resolve all `$ref` at compile time, not validation time.
1080
+
1081
+ **Rationale**:
1082
+ - Eliminates recursive lookup overhead during validation
1083
+ - Flattens reference chains (a→b→c becomes a→c)
1084
+ - Enables circular reference detection at compile time
1085
+
1086
+ ### 3. Lazy Error Generation
1087
+
1088
+ **Decision**: Only create error objects when `skipErrors` is false, and
1089
+ only produce human-readable text at report time (`convertInternalErrors`
1090
+ in messages.js), over the already-failed set, from a `msgid` + `params`
1091
+ pair through a message catalog.
1092
+
1093
+ **Rationale**:
1094
+ - Most production use cases only need boolean results
1095
+ - Error object creation is expensive
1096
+ - Reduces GC pressure during high-throughput validation
1097
+ - Structured-first errors make locale a report-time choice: switching
1098
+ language (`localizeErrors`, `@jarenjs/locales`) never recompiles a
1099
+ validator, and the `errorMessage` keyword resolves against a registry
1100
+ with zero validation-time cost (see docs/ERROR-MESSAGES.md)
1101
+
1102
+ ### 4. Dual Data Reference Systems
1103
+
1104
+ **Decision**: Support both `$data` (Ajv-style) and `data` (json-everything-style) keywords.
1105
+
1106
+ **Rationale**:
1107
+ - Maximizes compatibility with existing schema ecosystems
1108
+ - Different use cases favor different reference styles
1109
+ - Minimal overhead when not used
1110
+
1111
+ ### 5. Annotation Tracking via a Shared Log
1112
+
1113
+ **Decision**: Implement `unevaluatedProperties`/`unevaluatedItems` with a per-root `EvalLog` of `(instance, key)` pairs and mark/rollback semantics, compiled in only when the schema set uses these keywords.
1114
+
1115
+ **Rationale**:
1116
+ - Instance-identity keying makes annotations flow correctly through `$ref` chains and recursion without threading context through every validator signature
1117
+ - mark/rollback gives failed applicator branches (anyOf/oneOf/not/if) exact annotation-discarding semantics
1118
+ - The compile-time feature scan keeps schemas without unevaluated* keywords completely free of tracking overhead
1119
+
1120
+ ### 6. Dynamic Scope as Per-Anchor Stacks
1121
+
1122
+ **Decision**: Track the dynamic scope for `$recursiveRef`/`$dynamicRef` as per-anchor-name validator stacks, pushed on resource entry (root validation and `$ref` crossings) and popped on exit.
1123
+
1124
+ **Rationale**:
1125
+ - Entering a resource registers ALL of its `$dynamicAnchor`s (collected once at compile time), matching the specification's resource-based dynamic scope
1126
+ - Outermost-first resolution is a bottom-of-stack read
1127
+ - Balanced push/pop via try/finally keeps the scope correct across validation failures
1128
+
1129
+ ---
1130
+
1131
+ *This document was generated for contributors to understand the architecture of @jarenjs/validate. For implementation details, refer to the source code and inline JSDoc comments.*