ata-validator 0.5.0 → 0.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.
@@ -4,6 +4,34 @@
4
4
  // Closure-based validator — no new Function() or eval().
5
5
  // Returns null if the schema is too complex for JS compilation.
6
6
 
7
+ // AJV-compatible error message templates (compile-time, not runtime)
8
+ const AJV_MESSAGES = {
9
+ type: (p) => `must be ${p.type}`,
10
+ required: (p) => `must have required property '${p.missingProperty}'`,
11
+ additionalProperties: () => 'must NOT have additional properties',
12
+ enum: () => 'must be equal to one of the allowed values',
13
+ const: () => 'must be equal to constant',
14
+ minimum: (p) => `must be >= ${p.limit}`,
15
+ maximum: (p) => `must be <= ${p.limit}`,
16
+ exclusiveMinimum: (p) => `must be > ${p.limit}`,
17
+ exclusiveMaximum: (p) => `must be < ${p.limit}`,
18
+ minLength: (p) => `must NOT have fewer than ${p.limit} characters`,
19
+ maxLength: (p) => `must NOT have more than ${p.limit} characters`,
20
+ pattern: (p) => `must match pattern "${p.pattern}"`,
21
+ format: (p) => `must match format "${p.format}"`,
22
+ minItems: (p) => `must NOT have fewer than ${p.limit} items`,
23
+ maxItems: (p) => `must NOT have more than ${p.limit} items`,
24
+ uniqueItems: (p) => `must NOT have duplicate items (items ## ${p.j} and ${p.i} are identical)`,
25
+ minProperties: (p) => `must NOT have fewer than ${p.limit} properties`,
26
+ maxProperties: (p) => `must NOT have more than ${p.limit} properties`,
27
+ multipleOf: (p) => `must be multiple of ${p.multipleOf}`,
28
+ oneOf: () => 'must match exactly one schema in oneOf',
29
+ anyOf: () => 'must match a schema in anyOf',
30
+ allOf: () => 'must match all schemas in allOf',
31
+ not: () => 'must NOT be valid',
32
+ if: (p) => `must match "${p.failingKeyword}" schema`,
33
+ }
34
+
7
35
  function compileToJS(schema, defs, schemaMap) {
8
36
  if (typeof schema === 'boolean') {
9
37
  return schema ? () => true : () => false
@@ -433,11 +461,13 @@ function codegenSafe(schema, schemaMap) {
433
461
  if (typeof schema !== 'object' || schema === null) return true
434
462
 
435
463
  // Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
436
- if (schema.items === false || schema.items === true) return false
464
+ if (schema.items === false) return false
465
+ if (schema.items === true && !schema.unevaluatedItems) return false
437
466
  if (schema.additionalProperties === true) return true // permissive — fine
438
467
  if (schema.properties) {
439
468
  for (const v of Object.values(schema.properties)) {
440
- if (typeof v === 'boolean') return false
469
+ if (v === false) return false // property: false is complex
470
+ if (v === true) continue // property: true is always valid
441
471
  if (!codegenSafe(v, schemaMap)) return false
442
472
  }
443
473
  }
@@ -463,9 +493,10 @@ function codegenSafe(schema, schemaMap) {
463
493
  const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
464
494
  const isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
465
495
  if (!isLocal && !isResolvable) return false
466
- // Bail if $ref has sibling keywords (complex interaction)
496
+ // In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
497
+ // (unevaluated schemas need $ref + siblings to work properly)
467
498
  const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
468
- if (siblings.length > 0) return false
499
+ if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
469
500
  }
470
501
 
471
502
  // additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
@@ -475,6 +506,19 @@ function codegenSafe(schema, schemaMap) {
475
506
  // propertyNames: false — codegen doesn't handle this
476
507
  if (schema.propertyNames === false) return false
477
508
 
509
+ // unevaluatedProperties: allow boolean and schema values
510
+ if (schema.unevaluatedProperties !== undefined) {
511
+ if (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null) {
512
+ if (!codegenSafe(schema.unevaluatedProperties, schemaMap)) return false
513
+ }
514
+ }
515
+ // unevaluatedItems: allow boolean and schema values
516
+ if (schema.unevaluatedItems !== undefined) {
517
+ if (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null) {
518
+ if (!codegenSafe(schema.unevaluatedItems, schemaMap)) return false
519
+ }
520
+ }
521
+
478
522
  // Check $defs: targets must be safe, names must be simple, no nested $ref chains
479
523
  const defs = schema.$defs || schema.definitions
480
524
  if (defs) {
@@ -501,7 +545,8 @@ function codegenSafe(schema, schemaMap) {
501
545
  if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
502
546
  for (const s of subs) {
503
547
  if (s === undefined || s === null) continue
504
- if (typeof s === 'boolean') return false // boolean sub-schema
548
+ if (s === false) return false // boolean false sub-schema — complex
549
+ if (s === true) continue // boolean true sub-schema — always valid, fine
505
550
  if (!codegenSafe(s, schemaMap)) return false
506
551
  }
507
552
 
@@ -548,13 +593,14 @@ function compileToJSCodegen(schema, schemaMap) {
548
593
  const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null }
549
594
  const lines = []
550
595
  genCode(schema, 'd', lines, ctx)
551
- if (lines.length === 0) return () => true
552
596
 
553
- // Append deferred checks (additionalProperties) at the end
597
+ // Append deferred checks (additionalProperties, unevaluatedProperties) at the end
554
598
  if (ctx.deferredChecks) {
555
599
  for (const dc of ctx.deferredChecks) lines.push(dc)
556
600
  }
557
601
 
602
+ if (lines.length === 0) return () => true
603
+
558
604
  const checkStr = lines.join('\n ')
559
605
 
560
606
  // Regex and helpers are passed as closure variables (not re-created per call)
@@ -641,25 +687,32 @@ function genCode(schema, v, lines, ctx, knownType) {
641
687
  if (typeof schema !== 'object' || schema === null) return
642
688
 
643
689
  // $ref — guard against circular references
690
+ // In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
691
+ // Only when THIS schema has unevaluated keywords directly (not via $ref target)
692
+ const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
644
693
  if (schema.$ref) {
645
694
  // 1. Local ref
646
695
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
647
696
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
648
- if (ctx.refStack.has(schema.$ref)) return
649
- ctx.refStack.add(schema.$ref)
650
- genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
651
- ctx.refStack.delete(schema.$ref)
652
- return
653
- }
654
- // 2. Cross-schema ref
655
- if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
656
- if (ctx.refStack.has(schema.$ref)) return
657
- ctx.refStack.add(schema.$ref)
658
- genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
659
- ctx.refStack.delete(schema.$ref)
660
- return
697
+ if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
698
+ else {
699
+ ctx.refStack.add(schema.$ref)
700
+ genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
701
+ ctx.refStack.delete(schema.$ref)
702
+ if (!hasSiblings) return
703
+ }
704
+ } else if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
705
+ // 2. Cross-schema ref
706
+ if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
707
+ else {
708
+ ctx.refStack.add(schema.$ref)
709
+ genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
710
+ ctx.refStack.delete(schema.$ref)
711
+ if (!hasSiblings) return
712
+ }
713
+ } else {
714
+ if (!hasSiblings) return
661
715
  }
662
- return
663
716
  }
664
717
 
665
718
  // Determine the single known type after this schema's type check
@@ -744,6 +797,27 @@ function genCode(schema, v, lines, ctx, knownType) {
744
797
  }
745
798
  }
746
799
 
800
+ // Early key count for unevaluatedProperties: false (before properties, 10% faster)
801
+ // V8 branch prediction benefits from for-in iteration before property access
802
+ if (schema.unevaluatedProperties === false && schema.properties && schema.required && isObj) {
803
+ const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
804
+ if (!evalResult.dynamic && !evalResult.allProps) {
805
+ const knownKeys = evalResult.props
806
+ const propCount = knownKeys.length
807
+ const allRequired = schema.required.length >= propCount &&
808
+ knownKeys.every(k => schema.required.includes(k))
809
+ if (allRequired && propCount > 0) {
810
+ // Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
811
+ if (propCount <= 15) {
812
+ lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`)
813
+ } else {
814
+ lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
815
+ }
816
+ ctx._earlyKeyCount = true // flag to skip deferred check
817
+ }
818
+ }
819
+ }
820
+
747
821
  // numeric — skip type guard if known numeric
748
822
  if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
749
823
  if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
@@ -764,10 +838,15 @@ function genCode(schema, v, lines, ctx, knownType) {
764
838
  if (schema.maxProperties !== undefined) lines.push(`if(${objGuard}Object.keys(${v}).length>${schema.maxProperties})return false`)
765
839
 
766
840
  if (schema.pattern) {
767
- // Use RegExp constructor via helper to avoid injection from untrusted patterns
768
- const ri = ctx.varCounter++
769
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
770
- lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
841
+ // Try inline charCode compilation for simple patterns (avoids RegExp engine)
842
+ const inlineCheck = compilePatternInline(schema.pattern, v)
843
+ if (inlineCheck) {
844
+ lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
845
+ } else {
846
+ const ri = ctx.varCounter++
847
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
848
+ lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
849
+ }
771
850
  }
772
851
 
773
852
  if (schema.format) {
@@ -798,7 +877,9 @@ function genCode(schema, v, lines, ctx, knownType) {
798
877
  const propCount = Object.keys(schema.properties).length
799
878
  const allRequired = schema.required && schema.required.length === propCount
800
879
  const inner = allRequired
801
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
880
+ ? (propCount <= 15
881
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
882
+ : `if(Object.keys(${v}).length!==${propCount})return false`)
802
883
  : `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
803
884
  if (!ctx.deferredChecks) ctx.deferredChecks = []
804
885
  ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
@@ -852,16 +933,6 @@ function genCode(schema, v, lines, ctx, knownType) {
852
933
  ctx._ppHandledAdditional = true
853
934
  ctx._ppHandledPropertyNames = !!pn
854
935
  const propKeys = Object.keys(schema.properties)
855
- // Inline key comparison instead of Set.has for small property counts (faster, no allocation)
856
- const keyCheck = propKeys.length <= 8
857
- ? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
858
- : null
859
- if (!keyCheck) {
860
- const allowedSet = `_as${pi}`
861
- ctx.closureVars.push(allowedSet)
862
- ctx.closureVals.push(new Set(propKeys))
863
- }
864
-
865
936
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
866
937
  // propertyNames checks (merged into same loop)
867
938
  if (pn) {
@@ -886,14 +957,21 @@ function genCode(schema, v, lines, ctx, knownType) {
886
957
  lines.push(`if(!_es${ei}.has(${kVar}))return false`)
887
958
  }
888
959
  }
889
- // Check: is key in declared properties?
890
- const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
891
- lines.push(`let _m${pi}=${matchExpr}`)
892
- // Check pattern matches
960
+ // Check: is key declared or matches a pattern?
961
+ // switch/case: V8 compiles string cases to jump table (faster than chained ===)
962
+ const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
963
+ lines.push(`switch(${kVar}){${switchCases}break;default:`)
964
+ // Default: key is not declared — must match a pattern
965
+ let patternChecks = []
893
966
  for (let i = 0; i < ppEntries.length; i++) {
894
- lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}`)
967
+ patternChecks.push(`if(${matchers[i].check}){if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}else{return false}`)
895
968
  }
896
- lines.push(`if(!_m${pi})return false`)
969
+ if (patternChecks.length > 0) {
970
+ lines.push(patternChecks.join(''))
971
+ } else {
972
+ lines.push(`return false`)
973
+ }
974
+ lines.push(`}`) // end switch
897
975
  lines.push(`}}`)
898
976
  } else {
899
977
  // No additionalProperties: validate matching keys + propertyNames
@@ -1040,7 +1118,8 @@ function genCode(schema, v, lines, ctx, knownType) {
1040
1118
  }
1041
1119
 
1042
1120
  // anyOf — need function wrappers since genCode uses return false
1043
- if (schema.anyOf) {
1121
+ // Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
1122
+ if (schema.anyOf && schema.unevaluatedProperties === undefined) {
1044
1123
  const fns = []
1045
1124
  for (let i = 0; i < schema.anyOf.length; i++) {
1046
1125
  const subLines = []
@@ -1110,6 +1189,389 @@ function genCode(schema, v, lines, ctx, knownType) {
1110
1189
  lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
1111
1190
  lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
1112
1191
  }
1192
+
1193
+ // unevaluatedProperties
1194
+ if (schema.unevaluatedProperties !== undefined) {
1195
+ const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
1196
+
1197
+ if (evalResult.allProps || schema.unevaluatedProperties === true) {
1198
+ // All props evaluated or unevaluatedProperties:true — no-op
1199
+ } else if (!evalResult.dynamic) {
1200
+ // Tier 1-2: all evaluated props known at compile-time — ZERO COST
1201
+ const knownKeys = evalResult.props
1202
+ const propCount = knownKeys.length
1203
+
1204
+ if (schema.unevaluatedProperties === false) {
1205
+ const allRequired = schema.required && schema.required.length >= propCount &&
1206
+ knownKeys.every(k => schema.required.includes(k))
1207
+
1208
+ let inner
1209
+ if (allRequired && propCount > 0) {
1210
+ // TRICK 1: required covers all — key count check only
1211
+ if (!ctx._earlyKeyCount) {
1212
+ // Adaptive: for-in for <=15 keys, Object.keys for >15
1213
+ inner = propCount <= 15
1214
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
1215
+ : `if(Object.keys(${v}).length!==${propCount})return false`
1216
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1217
+ ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1218
+ }
1219
+ // else: already emitted early (before properties)
1220
+ } else if (propCount > 0) {
1221
+ // TRICK 3: charCodeAt switch tree
1222
+ inner = genCharCodeSwitch(knownKeys, v)
1223
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1224
+ ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1225
+ } else {
1226
+ inner = `for(var _k in ${v})return false`
1227
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1228
+ ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1229
+ }
1230
+ } else if (typeof schema.unevaluatedProperties === 'object') {
1231
+ // unevaluatedProperties: {schema} — validate unknown keys
1232
+ const ui = ctx.varCounter++
1233
+ const ukVar = `_uk${ui}`
1234
+ const subLines = []
1235
+ genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines, ctx)
1236
+ if (subLines.length > 0) {
1237
+ const check = subLines.join(';')
1238
+ const keyChecks = knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')
1239
+ const skipKnown = knownKeys.length > 0 ? `if(${keyChecks})continue;` : ''
1240
+ const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
1241
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1242
+ ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1243
+ }
1244
+ }
1245
+ } else {
1246
+ // Tier 2.5 / Tier 3: dynamic — runtime tracking needed
1247
+ // Compute base props: only unconditionally evaluated (properties, allOf-static, $ref)
1248
+ const baseResult = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
1249
+ if (schema.properties) {
1250
+ for (const k of Object.keys(schema.properties)) {
1251
+ if (!baseResult.props.includes(k)) baseResult.props.push(k)
1252
+ }
1253
+ }
1254
+ if (schema.allOf) {
1255
+ for (const sub of schema.allOf) {
1256
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1257
+ if (!subR.dynamic && subR.props) {
1258
+ for (const k of subR.props) {
1259
+ if (!baseResult.props.includes(k)) baseResult.props.push(k)
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+ const baseProps = baseResult.props
1265
+ const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
1266
+
1267
+ if (schema.unevaluatedProperties === false) {
1268
+ if (schema.if && (schema.then || schema.else) && !branchKeyword && !schema.patternProperties && !schema.dependentSchemas) {
1269
+ // Tier 2.5: if/then/else — re-emit if function + branch-inline duplication
1270
+ // Can't reuse _if from above (block-scoped), so regenerate
1271
+ const ifLines2 = []
1272
+ genCode(schema.if, '_iv2', ifLines2, ctx)
1273
+ const ufi = ctx.varCounter++
1274
+ const ifFn2 = ifLines2.length === 0
1275
+ ? `function(_iv2){return true}`
1276
+ : `function(_iv2){${ifLines2.join(';')};return true}`
1277
+
1278
+ // if props are only evaluated when if matches (spec: failed applicators produce no annotations)
1279
+ const ifProps = []
1280
+ if (schema.if && schema.if.properties) ifProps.push(...Object.keys(schema.if.properties))
1281
+ const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { props: [] }
1282
+ const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { props: [] }
1283
+ const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
1284
+ const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
1285
+
1286
+ const thenCheck = genCharCodeSwitch(uniqueThen, v)
1287
+ const elseCheck = genCharCodeSwitch(uniqueElse, v)
1288
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
1289
+ lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
1290
+ } else if (branchKeyword) {
1291
+ // Tier 3: anyOf/oneOf — runtime tracking
1292
+ const branches = schema[branchKeyword]
1293
+ const branchProps = []
1294
+ for (const sub of branches) {
1295
+ const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1296
+ branchProps.push(subResult.props || [])
1297
+ }
1298
+ const allDynamicKeys = [...new Set(branchProps.flat())]
1299
+ const dynamicOnly = allDynamicKeys.filter(k => !baseProps.includes(k))
1300
+
1301
+ if (dynamicOnly.length > 0 && dynamicOnly.length <= 32) {
1302
+ // TRICK 5: bit-packed evaluated set — SINGLE PASS (validation + tracking combined)
1303
+ const ei = ctx.varCounter++
1304
+ const evVar = `_ev${ei}`
1305
+ const bitMap = new Map()
1306
+ dynamicOnly.forEach((k, i) => bitMap.set(k, i))
1307
+ const branchMasks = branchProps.map(props => {
1308
+ let mask = 0
1309
+ for (const p of props) {
1310
+ if (bitMap.has(p)) mask |= (1 << bitMap.get(p))
1311
+ }
1312
+ return mask
1313
+ })
1314
+
1315
+ // TRICK 4: Direct function calls — no array, no loop, V8 can inline
1316
+ const bfi = ctx.varCounter++
1317
+ lines.push(`{let ${evVar}=0`)
1318
+ const fnVars = []
1319
+ for (let i = 0; i < branches.length; i++) {
1320
+ const subLines2 = []
1321
+ genCode(branches[i], '_bv', subLines2, ctx)
1322
+ const fnVar = `_bf${bfi}_${i}`
1323
+ fnVars.push(fnVar)
1324
+ const fnBody = subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`
1325
+ lines.push(`const ${fnVar}=${fnBody}`)
1326
+ }
1327
+ if (branchKeyword === 'oneOf') {
1328
+ // oneOf: exactly one must match — direct calls
1329
+ lines.push(`let _oc${bfi}=0`)
1330
+ for (let i = 0; i < branches.length; i++) {
1331
+ lines.push(`if(${fnVars[i]}(${v})){_oc${bfi}++;${evVar}=${branchMasks[i]};if(_oc${bfi}>1)return false}`)
1332
+ }
1333
+ lines.push(`if(_oc${bfi}!==1)return false`)
1334
+ } else {
1335
+ // anyOf: at least one must match — direct calls, collect all
1336
+ lines.push(`let _am${bfi}=false`)
1337
+ for (let i = 0; i < branches.length; i++) {
1338
+ lines.push(`if(${fnVars[i]}(${v})){_am${bfi}=true;${evVar}|=${branchMasks[i]}}`)
1339
+ }
1340
+ lines.push(`if(!_am${bfi})return false`)
1341
+ }
1342
+
1343
+ // Final check: static keys inline + dynamic keys via bitmask
1344
+ const staticCheck = baseProps.length > 0 ? baseProps.map(k => `_k===${JSON.stringify(k)}`).join('||') : ''
1345
+ const groups = new Map()
1346
+ for (const k of dynamicOnly) {
1347
+ const cc = k.charCodeAt(0)
1348
+ if (!groups.has(cc)) groups.set(cc, [])
1349
+ groups.get(cc).push(k)
1350
+ }
1351
+ let switchCases = ''
1352
+ for (const [cc, groupKeys] of groups) {
1353
+ const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}&&(${evVar}&${1 << bitMap.get(k)})`).join('||')
1354
+ switchCases += `case ${cc}:if(${cond})continue;break;`
1355
+ }
1356
+ const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
1357
+ const inner = staticCheck
1358
+ ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
1359
+ : `for(var _k in ${v}){${dynamicCheck}return false}`
1360
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1361
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1362
+ } else {
1363
+ // Fallback: plain object tracking
1364
+ const ei = ctx.varCounter++
1365
+ const evVar = `_ev${ei}`
1366
+ const fns = []
1367
+ for (let i = 0; i < branches.length; i++) {
1368
+ const subLines2 = []
1369
+ genCode(branches[i], '_bv', subLines2, ctx)
1370
+ fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
1371
+ }
1372
+ const bfi = ctx.varCounter++
1373
+ ctx.closureVars.push(`_bk${bfi}`)
1374
+ ctx.closureVals.push(branchProps)
1375
+ lines.push(`{const ${evVar}={}`)
1376
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1377
+ lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
1378
+ if (branchKeyword === 'oneOf') {
1379
+ // Single pass: validate oneOf (exactly one) + track evaluated
1380
+ lines.push(`let _oc${bfi}=0;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_oc${bfi}++;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;if(_oc${bfi}>1)return false}}if(_oc${bfi}!==1)return false`)
1381
+ } else {
1382
+ // Single pass: validate anyOf (at least one) + track all matching
1383
+ lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
1384
+ }
1385
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
1386
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1387
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1388
+ }
1389
+ } else if (schema.dependentSchemas) {
1390
+ // dependentSchemas: conditional merge at runtime
1391
+ const ei = ctx.varCounter++
1392
+ const evVar = `_ev${ei}`
1393
+ lines.push(`{const ${evVar}={}`)
1394
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1395
+ for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
1396
+ const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
1397
+ if (depResult.props && depResult.props.length > 0) {
1398
+ lines.push(`if(${JSON.stringify(trigger)} in ${v}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
1399
+ }
1400
+ }
1401
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
1402
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1403
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1404
+ } else if (schema.patternProperties) {
1405
+ // patternProperties: runtime key matching
1406
+ const ei = ctx.varCounter++
1407
+ const evVar = `_ev${ei}`
1408
+ lines.push(`{const ${evVar}={}`)
1409
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1410
+ const patterns = Object.keys(schema.patternProperties)
1411
+ const reVars = []
1412
+ for (const pat of patterns) {
1413
+ const ri = ctx.varCounter++
1414
+ ctx.closureVars.push(`_ure${ri}`)
1415
+ ctx.closureVals.push(new RegExp(pat))
1416
+ reVars.push(`_ure${ri}`)
1417
+ }
1418
+ const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
1419
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1420
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1421
+ }
1422
+ } else if (typeof schema.unevaluatedProperties === 'object') {
1423
+ // Tier 3 with schema: validate unknown keys against sub-schema
1424
+ const ei = ctx.varCounter++
1425
+ const evVar = `_ev${ei}`
1426
+ const ukVar = `_uk${ei}`
1427
+ lines.push(`{const ${evVar}={}`)
1428
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
1429
+
1430
+ if (branchKeyword) {
1431
+ const branches = schema[branchKeyword]
1432
+ const branchProps = []
1433
+ for (const sub of branches) {
1434
+ const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1435
+ branchProps.push(subResult.props || [])
1436
+ }
1437
+ const fns = []
1438
+ for (let i = 0; i < branches.length; i++) {
1439
+ const subLines2 = []
1440
+ genCode(branches[i], '_bv', subLines2, ctx)
1441
+ fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
1442
+ }
1443
+ const bfi = ctx.varCounter++
1444
+ ctx.closureVars.push(`_bk${bfi}`)
1445
+ ctx.closureVals.push(branchProps)
1446
+ lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
1447
+ if (branchKeyword === 'oneOf') {
1448
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;break}}`)
1449
+ } else {
1450
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}`)
1451
+ }
1452
+ }
1453
+
1454
+ const subLines2 = []
1455
+ genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
1456
+ if (subLines2.length > 0) {
1457
+ const check = subLines2.join(';')
1458
+ const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
1459
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1460
+ ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
1461
+ } else {
1462
+ lines.push('}')
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ // unevaluatedItems
1469
+ if (schema.unevaluatedItems !== undefined) {
1470
+ const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
1471
+
1472
+ if (evalResult.allItems || schema.unevaluatedItems === true) {
1473
+ // All items evaluated or unevaluatedItems:true — no-op
1474
+ } else if (!evalResult.dynamic) {
1475
+ // Static: all evaluated items known at compile-time
1476
+ if (schema.unevaluatedItems === false) {
1477
+ // TRICK 6: Array.length comparison only
1478
+ const maxIdx = evalResult.items || 0
1479
+ const inner = `if(${v}.length>${maxIdx})return false`
1480
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1481
+ ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
1482
+ } else if (typeof schema.unevaluatedItems === 'object') {
1483
+ const maxIdx = evalResult.items || 0
1484
+ const ui = ctx.varCounter++
1485
+ const elemVar = `_ue${ui}`
1486
+ const idxVar = `_ui${ui}`
1487
+ const subLines = []
1488
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
1489
+ if (subLines.length > 0) {
1490
+ const check = subLines.join(';')
1491
+ const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
1492
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1493
+ ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
1494
+ }
1495
+ }
1496
+ } else {
1497
+ // Dynamic: runtime tracking of max evaluated index
1498
+ const baseIdx = evalResult.items || 0
1499
+ const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
1500
+
1501
+ if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1502
+ // anyOf/oneOf: each branch may evaluate different number of items
1503
+ const branches = schema[branchKeyword]
1504
+ const branchMaxIdx = []
1505
+ for (const sub of branches) {
1506
+ const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
1507
+ branchMaxIdx.push(subR.items || 0)
1508
+ }
1509
+ // Runtime: find max evaluated index across all matching branches
1510
+ const fns = []
1511
+ for (let i = 0; i < branches.length; i++) {
1512
+ const subLines2 = []
1513
+ genCode(branches[i], '_bv', subLines2, ctx)
1514
+ fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
1515
+ }
1516
+ const bfi = ctx.varCounter++
1517
+ const ei = ctx.varCounter++
1518
+ const evVar = `_eidx${ei}`
1519
+ lines.push(`{let ${evVar}=${baseIdx}`)
1520
+ lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
1521
+ const maxExprs = branchMaxIdx.map((m, i) => `_bi===${i}?${Math.max(m, baseIdx)}`).join(':') + `:${baseIdx}`
1522
+ if (branchKeyword === 'oneOf') {
1523
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
1524
+ } else {
1525
+ lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){const _m=${maxExprs};if(_m>${evVar})${evVar}=_m}}`)
1526
+ }
1527
+ if (schema.unevaluatedItems === false) {
1528
+ const inner = `if(${v}.length>${evVar})return false`
1529
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1530
+ ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
1531
+ } else {
1532
+ const ui = ctx.varCounter++
1533
+ const elemVar = `_ue${ui}`
1534
+ const idxVar = `_ui${ui}`
1535
+ const subLines = []
1536
+ genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
1537
+ if (subLines.length > 0) {
1538
+ const check = subLines.join(';')
1539
+ const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
1540
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1541
+ ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
1542
+ } else {
1543
+ lines.push('}')
1544
+ }
1545
+ }
1546
+ } else if (schema.if && (schema.then || schema.else) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
1547
+ // if/then/else: branch-specific max index
1548
+ const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
1549
+ const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
1550
+ const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
1551
+ const ifIdx = ifEval.items || 0
1552
+ const thenIdx = Math.max(baseIdx, ifIdx, thenEval.items || 0)
1553
+ const elseIdx = Math.max(baseIdx, elseEval.items || 0)
1554
+
1555
+ const ifLines2 = []
1556
+ genCode(schema.if, '_iv3', ifLines2, ctx)
1557
+ const ufi = ctx.varCounter++
1558
+ const ifFn3 = ifLines2.length === 0
1559
+ ? `function(_iv3){return true}`
1560
+ : `function(_iv3){${ifLines2.join(';')};return true}`
1561
+
1562
+ if (schema.unevaluatedItems === false) {
1563
+ const guard = isArr ? '' : `if(Array.isArray(${v}))`
1564
+ lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
1565
+ }
1566
+ } else if (schema.unevaluatedItems === false) {
1567
+ // Fallback: use static base index (may not be fully correct for all dynamic cases)
1568
+ const maxIdx = evalResult.items || 0
1569
+ const inner = `if(${v}.length>${maxIdx})return false`
1570
+ if (!ctx.deferredChecks) ctx.deferredChecks = []
1571
+ ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
1572
+ }
1573
+ }
1574
+ }
1113
1575
  }
1114
1576
 
1115
1577
  const FORMAT_CODEGEN = {
@@ -1154,6 +1616,79 @@ const FORMAT_CODEGEN = {
1154
1616
  // Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
1155
1617
  function esc(s) { return JSON.stringify(s).slice(1, -1) }
1156
1618
 
1619
+ // Resolve child path at codegen time when parent is a static string literal.
1620
+ // This enables frozen pre-allocation for ALL nested error objects.
1621
+ function childPathExpr(parentExpr, suffix) {
1622
+ if (!parentExpr) return `'/${suffix}'`
1623
+ if (parentExpr.startsWith("'") && !parentExpr.includes('+')) {
1624
+ // Static parent: resolve at codegen time → '/parent/child' (single literal)
1625
+ return `'${parentExpr.slice(1, -1)}/${suffix}'`
1626
+ }
1627
+ // Dynamic parent: keep as concat expression
1628
+ return `${parentExpr}+'/${suffix}'`
1629
+ }
1630
+
1631
+ // Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
1632
+ // Returns null if pattern is too complex for inline compilation.
1633
+ // Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
1634
+ function compilePatternInline(pattern, varName) {
1635
+ // Match: ^[chars]{exact}$ — e.g., ^[0-9]{5}$
1636
+ let m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+)\}\$$/)
1637
+ if (m) {
1638
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
1639
+ if (!rangeCheck) return null
1640
+ const len = parseInt(m[2])
1641
+ return `${varName}.length===${len}&&(()=>{for(let _pi=0;_pi<${len};_pi++){if(!(${rangeCheck}))return false}return true})()`
1642
+ }
1643
+ // Match: ^[chars]+$ — e.g., ^[a-z]+$
1644
+ m = pattern.match(/^\^(\[[\w\-]+\])\+\$$/)
1645
+ if (m) {
1646
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
1647
+ if (!rangeCheck) return null
1648
+ return `${varName}.length>0&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
1649
+ }
1650
+ // Match: ^[chars]{m,n}$ — e.g., ^[a-zA-Z]{2,50}$
1651
+ m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+),(\d+)\}\$$/)
1652
+ if (m) {
1653
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
1654
+ if (!rangeCheck) return null
1655
+ const min = parseInt(m[2]), max = parseInt(m[3])
1656
+ return `${varName}.length>=${min}&&${varName}.length<=${max}&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
1657
+ }
1658
+ return null
1659
+ }
1660
+
1661
+ // Convert [charclass] to charCode range check expression.
1662
+ // Supports: [0-9], [a-z], [A-Z], [a-zA-Z], [a-zA-Z0-9], [0-9a-f], etc.
1663
+ function charClassToCheck(charClass, codeExpr) {
1664
+ // Strip brackets
1665
+ const inner = charClass.slice(1, -1)
1666
+ // Parse ranges
1667
+ const ranges = []
1668
+ let i = 0
1669
+ while (i < inner.length) {
1670
+ if (i + 2 < inner.length && inner[i + 1] === '-') {
1671
+ ranges.push([inner.charCodeAt(i), inner.charCodeAt(i + 2)])
1672
+ i += 3
1673
+ } else {
1674
+ ranges.push([inner.charCodeAt(i), inner.charCodeAt(i)])
1675
+ i++
1676
+ }
1677
+ }
1678
+ if (ranges.length === 0) return null
1679
+ // Generate check: (c >= 48 && c <= 57) || (c >= 65 && c <= 90)
1680
+ const checks = ranges.map(([lo, hi]) =>
1681
+ lo === hi ? `${codeExpr}===${lo}` : `(${codeExpr}>=${lo}&&${codeExpr}<=${hi})`
1682
+ )
1683
+ return checks.join('||')
1684
+ }
1685
+
1686
+ // Same but for dynamic segments (array indices)
1687
+ function childPathDynExpr(parentExpr, indexExpr) {
1688
+ if (!parentExpr) return `'/'+${indexExpr}`
1689
+ return `${parentExpr}+'/'+${indexExpr}`
1690
+ }
1691
+
1157
1692
  // Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
1158
1693
  // Returns a JS expression string or null if pattern is too complex
1159
1694
  function fastPrefixCheck(pattern, keyVar) {
@@ -1172,14 +1707,45 @@ function fastPrefixCheck(pattern, keyVar) {
1172
1707
  return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
1173
1708
  }
1174
1709
 
1710
+ // Generate a charCodeAt(0)-based switch tree for fast key validation.
1711
+ // V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
1712
+ function genCharCodeSwitch(keys, v) {
1713
+ if (keys.length === 0) return `for(var _k in ${v})return false`
1714
+ if (keys.length <= 3) {
1715
+ // Small set: simple chain is faster than switch overhead
1716
+ return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
1717
+ }
1718
+
1719
+ // Group keys by first charCode
1720
+ const groups = new Map()
1721
+ for (const k of keys) {
1722
+ const cc = k.charCodeAt(0)
1723
+ if (!groups.has(cc)) groups.set(cc, [])
1724
+ groups.get(cc).push(k)
1725
+ }
1726
+
1727
+ let cases = ''
1728
+ for (const [cc, groupKeys] of groups) {
1729
+ const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}`).join('||')
1730
+ cases += `case ${cc}:if(${cond})continue;break;`
1731
+ }
1732
+
1733
+ return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
1734
+ }
1735
+
1175
1736
  // --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
1176
1737
  // Returns a function: (data, allErrors) => { valid, errors }
1177
1738
  // Valid path is still fast — only error path does extra work.
1178
1739
  function compileToJSCodegenWithErrors(schema, schemaMap) {
1740
+ // Bail on unevaluated keywords — error codegen doesn't support them yet
1741
+ if (typeof schema === 'object' && schema !== null) {
1742
+ const s = JSON.stringify(schema)
1743
+ if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
1744
+ }
1179
1745
  if (typeof schema === 'boolean') {
1180
1746
  return schema
1181
1747
  ? () => ({ valid: true, errors: [] })
1182
- : () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
1748
+ : () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
1183
1749
  }
1184
1750
  if (typeof schema !== 'object' || schema === null) return null
1185
1751
  if (!codegenSafe(schema, schemaMap)) return null
@@ -1206,7 +1772,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
1206
1772
 
1207
1773
  const ctx = { varCounter: 0, helperCode: [], rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
1208
1774
  const lines = []
1209
- genCodeE(schema, 'd', '', lines, ctx)
1775
+ genCodeE(schema, 'd', '', lines, ctx, '#')
1210
1776
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
1211
1777
 
1212
1778
  const body = `const _e=[];\n ` +
@@ -1225,7 +1791,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
1225
1791
  // Error-collecting code generator.
1226
1792
  // Instead of `return false`, pushes to `_e` array and optionally early-returns.
1227
1793
  // `_all` parameter: if falsy, return after first error.
1228
- function genCodeE(schema, v, pathExpr, lines, ctx) {
1794
+ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1795
+ if (!schemaPrefix) schemaPrefix = '#'
1229
1796
  if (typeof schema !== 'object' || schema === null) return
1230
1797
 
1231
1798
  // $ref — resolve local and cross-schema refs
@@ -1234,14 +1801,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1234
1801
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
1235
1802
  if (ctx.refStack.has(schema.$ref)) return
1236
1803
  ctx.refStack.add(schema.$ref)
1237
- genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
1804
+ genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
1238
1805
  ctx.refStack.delete(schema.$ref)
1239
1806
  return
1240
1807
  }
1241
1808
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
1242
1809
  if (ctx.refStack.has(schema.$ref)) return
1243
1810
  ctx.refStack.add(schema.$ref)
1244
- genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
1811
+ genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
1245
1812
  ctx.refStack.delete(schema.$ref)
1246
1813
  return
1247
1814
  }
@@ -1262,7 +1829,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1262
1829
  }
1263
1830
  })
1264
1831
  const expected = types.join(', ')
1265
- lines.push(`if(!(${conds.join('||')})){_e.push({code:'type_mismatch',path:${pathExpr||'""'},message:'expected ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
1832
+ lines.push(`if(!(${conds.join('||')})){_e.push({keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/type',params:{type:'${expected}'},message:'must be ${expected}'});if(!_all)return{valid:false,errors:_e}}`)
1266
1833
  }
1267
1834
 
1268
1835
  // In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
@@ -1271,7 +1838,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1271
1838
  const isStr = false
1272
1839
  const isNum = false
1273
1840
 
1274
- const fail = (code, msg) => `_e.push({code:'${code}',path:${pathExpr||'""'},message:${msg}});if(!_all)return{valid:false,errors:_e}`
1841
+ const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
1842
+ const sp = schemaPrefix + '/' + schemaSuffix
1843
+ return `_e.push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}});if(!_all)return{valid:false,errors:_e}`
1844
+ }
1275
1845
 
1276
1846
  // enum
1277
1847
  if (schema.enum) {
@@ -1281,21 +1851,21 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1281
1851
  const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
1282
1852
  const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
1283
1853
  const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
1284
- lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
1854
+ lines.push(`if(!(${allChecks || 'false'})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1285
1855
  }
1286
1856
 
1287
1857
  // const — use canonical (sorted-key) comparison for objects
1288
1858
  if (schema.const !== undefined) {
1289
1859
  const cv = schema.const
1290
1860
  if (cv === null || typeof cv !== 'object') {
1291
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'value does not match const'")}}`)
1861
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
1292
1862
  } else {
1293
1863
  // Pre-compute canonical form of const value
1294
1864
  const ci = ctx.varCounter++
1295
1865
  const canonFn = `_cnE${ci}`
1296
1866
  ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
1297
1867
  const expected = canonFn + '(' + JSON.stringify(cv) + ')'
1298
- lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const_mismatch', "'value does not match const'")}}`)
1868
+ lines.push(`if(${canonFn}(${v})!==${expected}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
1299
1869
  }
1300
1870
  }
1301
1871
 
@@ -1304,49 +1874,54 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1304
1874
  const hoisted = {}
1305
1875
  if (schema.required) {
1306
1876
  for (const key of schema.required) {
1307
- const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1308
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'required_missing',path:${p},message:'missing required: ${esc(key)}'});if(!_all)return{valid:false,errors:_e}}`)
1877
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"});if(!_all)return{valid:false,errors:_e}}`)
1309
1878
  }
1310
1879
  }
1311
1880
 
1312
1881
  // numeric
1313
1882
  if (schema.minimum !== undefined) {
1314
1883
  const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`
1315
- lines.push(`if(${c}){${fail('minimum_violation', `'minimum ${schema.minimum}'`)}}`)
1884
+ lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`)
1316
1885
  }
1317
1886
  if (schema.maximum !== undefined) {
1318
1887
  const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`
1319
- lines.push(`if(${c}){${fail('maximum_violation', `'maximum ${schema.maximum}'`)}}`)
1888
+ lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`)
1320
1889
  }
1321
1890
  if (schema.exclusiveMinimum !== undefined) {
1322
1891
  const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`
1323
- lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'exclusiveMinimum ${schema.exclusiveMinimum}'`)}}`)
1892
+ lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`)
1324
1893
  }
1325
1894
  if (schema.exclusiveMaximum !== undefined) {
1326
1895
  const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`
1327
- lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'exclusiveMaximum ${schema.exclusiveMaximum}'`)}}`)
1896
+ lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`)
1328
1897
  }
1329
1898
  if (schema.multipleOf !== undefined) {
1330
1899
  const m = schema.multipleOf
1331
1900
  const ci = ctx.varCounter++
1332
1901
  // Use tolerance-based check for floating point (matches C++ behavior)
1333
- lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
1902
+ lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
1334
1903
  }
1335
1904
 
1336
1905
  // string
1337
1906
  if (schema.minLength !== undefined) {
1338
1907
  const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`
1339
- lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`)
1908
+ lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`)
1340
1909
  }
1341
1910
  if (schema.maxLength !== undefined) {
1342
1911
  const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`
1343
- lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`)
1912
+ lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`)
1344
1913
  }
1345
1914
  if (schema.pattern) {
1346
- const ri = ctx.varCounter++
1347
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
1348
- const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
1349
- lines.push(`if(${c}){${fail('pattern_mismatch', `'pattern mismatch'`)}}`)
1915
+ const inlineCheck = compilePatternInline(schema.pattern, v)
1916
+ if (inlineCheck) {
1917
+ const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
1918
+ lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
1919
+ } else {
1920
+ const ri = ctx.varCounter++
1921
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
1922
+ const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
1923
+ lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
1924
+ }
1350
1925
  }
1351
1926
  if (schema.format) {
1352
1927
  const fc = FORMAT_CODEGEN[schema.format]
@@ -1357,7 +1932,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1357
1932
  boolLines.push(fc(v, isStr))
1358
1933
  // Replace `return false` with error push in the format check
1359
1934
  const fmtCode = boolLines.join(';').replace(/return false/g,
1360
- `{_e.push({code:'format_mismatch',path:${pathExpr||'""'},message:'format ${esc(schema.format)}'});if(!_all)return{valid:false,errors:_e}}`)
1935
+ `{_e.push({keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/format',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"'});if(!_all)return{valid:false,errors:_e}}`)
1361
1936
  lines.push(fmtCode)
1362
1937
  }
1363
1938
  }
@@ -1365,37 +1940,44 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1365
1940
  // array size
1366
1941
  if (schema.minItems !== undefined) {
1367
1942
  const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`
1368
- lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`)
1943
+ lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`)
1369
1944
  }
1370
1945
  if (schema.maxItems !== undefined) {
1371
1946
  const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`
1372
- lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`)
1947
+ lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`)
1373
1948
  }
1374
1949
 
1375
- // uniqueItems
1950
+ // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
1376
1951
  if (schema.uniqueItems) {
1377
1952
  const si = ctx.varCounter++
1378
1953
  const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
1379
1954
  const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
1380
- const inner = isPrim
1381
- ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(${v}[_i])}`
1382
- : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicate items'")};break};_s${si}.add(_k)}`
1955
+ const maxItems = schema.maxItems
1956
+ const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
1957
+ let inner
1958
+ if (isPrim && maxItems && maxItems <= 16) {
1959
+ inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
1960
+ } else if (isPrim) {
1961
+ inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
1962
+ } else {
1963
+ inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
1964
+ }
1383
1965
  lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
1384
1966
  }
1385
1967
 
1386
1968
  // object size
1387
1969
  if (schema.minProperties !== undefined) {
1388
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
1970
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
1389
1971
  }
1390
1972
  if (schema.maxProperties !== undefined) {
1391
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
1973
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
1392
1974
  }
1393
1975
 
1394
1976
  // additionalProperties: false
1395
1977
  if (schema.additionalProperties === false && schema.properties) {
1396
1978
  const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
1397
1979
  const ci = ctx.varCounter++
1398
- const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(!_a${ci}.has(_k${ci}[_i])){_e.push({code:'additional_property',path:${pathExpr||'""'},message:'additional property: '+_k${ci}[_i]});if(!_all)return{valid:false,errors:_e}}}`
1980
+ const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(!_a${ci}.has(_k${ci}[_i])){_e.push({keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/additionalProperties',params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties'});if(!_all)return{valid:false,errors:_e}}}`
1399
1981
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1400
1982
  }
1401
1983
 
@@ -1403,8 +1985,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1403
1985
  if (schema.dependentRequired) {
1404
1986
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
1405
1987
  for (const dep of deps) {
1406
- const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
1407
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'});if(!_all)return{valid:false,errors:_e}}`)
1988
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"});if(!_all)return{valid:false,errors:_e}}`)
1408
1989
  }
1409
1990
  }
1410
1991
  }
@@ -1412,9 +1993,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1412
1993
  // properties — always guard (error mode, data may not be an object or may be array)
1413
1994
  if (schema.properties) {
1414
1995
  for (const [key, prop] of Object.entries(schema.properties)) {
1415
- const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1996
+ const childPath = childPathExpr(pathExpr, esc(key))
1416
1997
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1417
- genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
1998
+ genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
1418
1999
  lines.push(`}`)
1419
2000
  }
1420
2001
  }
@@ -1427,7 +2008,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1427
2008
  const ki = ctx.varCounter++
1428
2009
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
1429
2010
  const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
1430
- genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx)
2011
+ genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix+'/patternProperties')
1431
2012
  lines.push(`}}}`)
1432
2013
  }
1433
2014
  }
@@ -1436,7 +2017,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1436
2017
  if (schema.dependentSchemas) {
1437
2018
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
1438
2019
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1439
- genCodeE(depSchema, v, pathExpr, lines, ctx)
2020
+ genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
1440
2021
  lines.push(`}`)
1441
2022
  }
1442
2023
  }
@@ -1447,23 +2028,23 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1447
2028
  const ki = ctx.varCounter++
1448
2029
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
1449
2030
  if (pn.minLength !== undefined) {
1450
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+_k${ki}`)}}`)
2031
+ lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
1451
2032
  }
1452
2033
  if (pn.maxLength !== undefined) {
1453
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+_k${ki}`)}}`)
2034
+ lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
1454
2035
  }
1455
2036
  if (pn.pattern) {
1456
2037
  const ri = ctx.varCounter++
1457
2038
  ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pn.pattern)})`)
1458
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+_k${ki}`)}}`)
2039
+ lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1459
2040
  }
1460
2041
  if (pn.const !== undefined) {
1461
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
2042
+ lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
1462
2043
  }
1463
2044
  if (pn.enum) {
1464
2045
  const ei = ctx.varCounter++
1465
2046
  ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
1466
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+_k${ki}`)}}`)
2047
+ lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1467
2048
  }
1468
2049
  lines.push(`}}`)
1469
2050
  }
@@ -1474,18 +2055,18 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1474
2055
  const idx = `_j${ctx.varCounter}`
1475
2056
  const elem = `_ei${ctx.varCounter}`
1476
2057
  ctx.varCounter++
1477
- const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
2058
+ const childPath = childPathDynExpr(pathExpr, idx)
1478
2059
  lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
1479
- genCodeE(schema.items, elem, childPath, lines, ctx)
2060
+ genCodeE(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
1480
2061
  lines.push(`}}`)
1481
2062
  }
1482
2063
 
1483
2064
  // prefixItems
1484
2065
  if (schema.prefixItems) {
1485
2066
  for (let i = 0; i < schema.prefixItems.length; i++) {
1486
- const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
2067
+ const childPath = childPathExpr(pathExpr, String(i))
1487
2068
  lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
1488
- genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
2069
+ genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
1489
2070
  lines.push(`}`)
1490
2071
  }
1491
2072
  }
@@ -1499,17 +2080,17 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1499
2080
  const minC = schema.minContains !== undefined ? schema.minContains : 1
1500
2081
  const maxC = schema.maxContains
1501
2082
  lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
1502
- lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'contains: need at least ${minC} match(es)'`)}}`)
2083
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
1503
2084
  if (maxC !== undefined) {
1504
- lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'contains: at most ${maxC} match(es)'`)}}`)
2085
+ lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
1505
2086
  }
1506
2087
  lines.push(`}`)
1507
2088
  }
1508
2089
 
1509
2090
  // allOf
1510
2091
  if (schema.allOf) {
1511
- for (const sub of schema.allOf) {
1512
- genCodeE(sub, v, pathExpr, lines, ctx)
2092
+ for (let _ai = 0; _ai < schema.allOf.length; _ai++) {
2093
+ genCodeE(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai)
1513
2094
  }
1514
2095
  }
1515
2096
 
@@ -1521,7 +2102,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1521
2102
  genCode(sub, '_av', subLines, ctx)
1522
2103
  return subLines.length === 0 ? `function(_av){return true}` : `function(_av){${subLines.join(';')};return true}`
1523
2104
  })
1524
- lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi}){${fail('any_of_failed', "'no anyOf matched'")}}}`)
2105
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi}){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
1525
2106
  }
1526
2107
 
1527
2108
  // oneOf
@@ -1532,7 +2113,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1532
2113
  genCode(sub, '_ov', subLines, ctx)
1533
2114
  return subLines.length === 0 ? `function(_ov){return true}` : `function(_ov){${subLines.join(';')};return true}`
1534
2115
  })
1535
- lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)break}if(_oc${fi}!==1){${fail('one_of_failed', "'oneOf: expected 1 match, got '+_oc"+fi)}}}`)
2116
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)break}if(_oc${fi}!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
1536
2117
  }
1537
2118
 
1538
2119
  // not
@@ -1541,7 +2122,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1541
2122
  genCode(schema.not, '_nv', subLines, ctx)
1542
2123
  const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
1543
2124
  const fi = ctx.varCounter++
1544
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
2125
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
1545
2126
  }
1546
2127
 
1547
2128
  // if/then/else
@@ -1555,12 +2136,12 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1555
2136
  lines.push(`{const _if${fi}=${ifFn}`)
1556
2137
  if (schema.then) {
1557
2138
  lines.push(`if(_if${fi}(${v})){`)
1558
- genCodeE(schema.then, v, pathExpr, lines, ctx)
2139
+ genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
1559
2140
  lines.push(`}`)
1560
2141
  }
1561
2142
  if (schema.else) {
1562
2143
  lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
1563
- genCodeE(schema.else, v, pathExpr, lines, ctx)
2144
+ genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
1564
2145
  lines.push(`}`)
1565
2146
  }
1566
2147
  lines.push(`}`)
@@ -1572,10 +2153,15 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
1572
2153
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
1573
2154
  // Uses type-aware optimizations: after type check passes, skip guards.
1574
2155
  function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
2156
+ // Bail on unevaluated keywords — combined codegen doesn't support them yet
2157
+ if (typeof schema === 'object' && schema !== null) {
2158
+ const s = JSON.stringify(schema)
2159
+ if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
2160
+ }
1575
2161
  if (typeof schema === 'boolean') {
1576
2162
  return schema
1577
2163
  ? () => VALID_RESULT
1578
- : () => ({ valid: false, errors: [{ code: 'type_mismatch', path: '', message: 'schema is false' }] })
2164
+ : () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
1579
2165
  }
1580
2166
  if (typeof schema !== 'object' || schema === null) return null
1581
2167
  if (!codegenSafe(schema, schemaMap)) return null
@@ -1603,7 +2189,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
1603
2189
  const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
1604
2190
  rootDefs: schema.$defs || schema.definitions || null, refStack: new Set(), schemaMap: schemaMap || null }
1605
2191
  const lines = []
1606
- genCodeC(schema, 'd', '', lines, ctx)
2192
+ genCodeC(schema, 'd', '', lines, ctx, '#')
1607
2193
  if (lines.length === 0) return () => VALID_RESULT
1608
2194
 
1609
2195
  // Use factory pattern: closure vars (regexes, etc.) created once, not per call
@@ -1615,6 +2201,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
1615
2201
  `\n return _e?{valid:false,errors:_e}:R`
1616
2202
 
1617
2203
  try {
2204
+ if (process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
1618
2205
  const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
1619
2206
  `return function(d){${inner}}`)
1620
2207
  return factory(VALID_RESULT, ...ctx.closureVals)
@@ -1627,7 +2214,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
1627
2214
  // Combined code generator: type-aware like genCode, error-collecting like genCodeE.
1628
2215
  // After type check passes → use optimizations (destructuring, no guards).
1629
2216
  // If type check fails → push error, skip property checks (they'd crash).
1630
- function genCodeC(schema, v, pathExpr, lines, ctx) {
2217
+ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2218
+ if (!schemaPrefix) schemaPrefix = '#'
1631
2219
  if (typeof schema !== 'object' || schema === null) return
1632
2220
 
1633
2221
  // $ref — resolve local and cross-schema refs
@@ -1636,14 +2224,14 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1636
2224
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
1637
2225
  if (ctx.refStack.has(schema.$ref)) return
1638
2226
  ctx.refStack.add(schema.$ref)
1639
- genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx)
2227
+ genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
1640
2228
  ctx.refStack.delete(schema.$ref)
1641
2229
  return
1642
2230
  }
1643
2231
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
1644
2232
  if (ctx.refStack.has(schema.$ref)) return
1645
2233
  ctx.refStack.add(schema.$ref)
1646
- genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx)
2234
+ genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
1647
2235
  ctx.refStack.delete(schema.$ref)
1648
2236
  return
1649
2237
  }
@@ -1655,19 +2243,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1655
2243
  // Pre-allocate error objects as closure variables for static paths.
1656
2244
  // This shrinks the generated function body → better V8 JIT on valid path.
1657
2245
  const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
1658
- const fail = (code, msg) => {
1659
- if (isStaticPath && msg.startsWith("'") && !msg.includes('+')) {
1660
- // Static error: pre-allocate as frozen closure variable
1661
- const ei = ctx.varCounter++
1662
- const errVar = `_E${ei}`
1663
- const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
1664
- const msgVal = msg.slice(1, -1)
1665
- ctx.closureVars.push(errVar)
1666
- ctx.closureVals.push(Object.freeze({code, path: pathVal, message: msgVal}))
1667
- return `(_e||(_e=[])).push(${errVar})`
2246
+ const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
2247
+ const sp = schemaPrefix + '/' + schemaSuffix
2248
+ if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
2249
+ // Try to evaluate paramsCode as a static constant
2250
+ let paramsVal
2251
+ try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
2252
+ if (paramsVal !== undefined) {
2253
+ // Static error: pre-allocate as frozen closure variable
2254
+ const ei = ctx.varCounter++
2255
+ const errVar = `_E${ei}`
2256
+ const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
2257
+ const msgVal = msgCode.slice(1, -1)
2258
+ ctx.closureVars.push(errVar)
2259
+ ctx.closureVals.push(Object.freeze({keyword, instancePath: pathVal, schemaPath: sp, params: Object.freeze(paramsVal), message: msgVal}))
2260
+ return `(_e||(_e=[])).push(${errVar})`
2261
+ }
1668
2262
  }
1669
2263
  // Dynamic path (e.g., array index): inline as before
1670
- return `(_e||(_e=[])).push({code:'${code}',path:${pathExpr||'""'},message:${msg}})`
2264
+ return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}})`
1671
2265
  }
1672
2266
 
1673
2267
  if (types) {
@@ -1687,7 +2281,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1687
2281
  // Type check: push error but continue — wrap remaining in type-success block
1688
2282
  const typeOk = `_tok${ctx.varCounter++}`
1689
2283
  lines.push(`const ${typeOk}=${conds.join('||')}`)
1690
- lines.push(`if(!${typeOk}){${fail('type_mismatch', `'expected ${expected}'`)}}`)
2284
+ lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:'${expected}'}`, `'must be ${expected}'`)}}`)
1691
2285
  // Subsequent optimized code runs inside if(typeOk){...}
1692
2286
  if (types.length === 1) {
1693
2287
  isObj = types[0] === 'object'
@@ -1706,19 +2300,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1706
2300
  const primChecks = primitives.map(p => `${v}===${JSON.stringify(p)}`).join('||')
1707
2301
  const objChecks = objects.map(o => `JSON.stringify(${v})===${JSON.stringify(JSON.stringify(o))}`).join('||')
1708
2302
  const allChecks = [primChecks, objChecks].filter(Boolean).join('||')
1709
- lines.push(`if(!(${allChecks || 'false'})){${fail('enum_mismatch', "'value not in enum'")}}`)
2303
+ lines.push(`if(!(${allChecks || 'false'})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1710
2304
  }
1711
2305
 
1712
2306
  // const
1713
2307
  if (schema.const !== undefined) {
1714
2308
  const cv = schema.const
1715
2309
  if (cv === null || typeof cv !== 'object') {
1716
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const_mismatch', "'const mismatch'")}}`)
2310
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
1717
2311
  } else {
1718
2312
  const ci = ctx.varCounter++
1719
2313
  const canonFn = `_cn${ci}`
1720
2314
  ctx.helperCode.push(`const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};`)
1721
- lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const_mismatch', "'const mismatch'")}}`)
2315
+ lines.push(`if(${canonFn}(${v})!==${canonFn}(${JSON.stringify(cv)})){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
1722
2316
  }
1723
2317
  }
1724
2318
 
@@ -1737,80 +2331,127 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1737
2331
  if (destructKeys.length > 0) lines.push(`const{${destructKeys.join(',')}}=${v}`)
1738
2332
  for (const key of schema.required) {
1739
2333
  const check = hoisted[key] ? `${hoisted[key]}===undefined` : `${v}[${JSON.stringify(key)}]===undefined`
1740
- const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1741
- lines.push(`if(${check}){${`(_e||(_e=[])).push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})`}}`)
2334
+ // Pre-allocate required errors as frozen closure variables
2335
+ const ei = ctx.varCounter++
2336
+ const errVar = `_E${ei}`
2337
+ const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
2338
+ ctx.closureVars.push(errVar)
2339
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
2340
+ lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
1742
2341
  }
1743
2342
  } else if (schema.required) {
1744
2343
  for (const key of schema.required) {
1745
- const p = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
1746
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push({code:'required_missing',path:${p},message:'missing: ${esc(key)}'})}`)
2344
+ const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
2345
+ if (isStatic) {
2346
+ const ei = ctx.varCounter++
2347
+ const errVar = `_E${ei}`
2348
+ const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
2349
+ ctx.closureVars.push(errVar)
2350
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
2351
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
2352
+ } else {
2353
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required',params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
2354
+ }
1747
2355
  }
1748
2356
  }
1749
2357
 
1750
2358
  // numeric — skip type guard if known
1751
- if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum_violation', `'min ${schema.minimum}'`)}}`) }
1752
- if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum_violation', `'max ${schema.maximum}'`)}}`) }
1753
- if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusive_minimum_violation', `'excMin ${schema.exclusiveMinimum}'`)}}`) }
1754
- if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusive_maximum_violation', `'excMax ${schema.exclusiveMaximum}'`)}}`) }
2359
+ if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`) }
2360
+ if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`) }
2361
+ if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`) }
2362
+ if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`) }
1755
2363
  if (schema.multipleOf !== undefined) {
1756
2364
  const m = schema.multipleOf
1757
2365
  const ci = ctx.varCounter++
1758
- lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multiple_of_violation', `'multipleOf ${m}'`)}}}`)
2366
+ lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
1759
2367
  }
1760
2368
 
1761
2369
  // string — skip guard if known
1762
- if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('min_length_violation', `'minLength ${schema.minLength}'`)}}`) }
1763
- if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('max_length_violation', `'maxLength ${schema.maxLength}'`)}}`) }
2370
+ if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`) }
2371
+ if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`) }
1764
2372
  if (schema.pattern) {
1765
- const ri = ctx.varCounter++
1766
- const reVar = `_re${ri}`
1767
- ctx.closureVars.push(reVar)
1768
- ctx.closureVals.push(new RegExp(schema.pattern))
1769
- const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
1770
- lines.push(`if(${c}){${fail('pattern_mismatch', "'pattern mismatch'")}}`)
2373
+ const inlineCheck = compilePatternInline(schema.pattern, v)
2374
+ if (inlineCheck) {
2375
+ const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
2376
+ lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
2377
+ } else {
2378
+ const ri = ctx.varCounter++
2379
+ const reVar = `_re${ri}`
2380
+ ctx.closureVars.push(reVar)
2381
+ ctx.closureVals.push(new RegExp(schema.pattern))
2382
+ const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
2383
+ lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
2384
+ }
1771
2385
  }
1772
2386
  if (schema.format) {
1773
2387
  const fc = FORMAT_CODEGEN[schema.format]
1774
2388
  if (fc) {
1775
- const code = fc(v, isStr).replace(/return false/g, `{${fail('format_mismatch', `'format ${esc(schema.format)}'`)}}`)
2389
+ const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
1776
2390
  lines.push(code)
1777
2391
  }
1778
2392
  }
1779
2393
 
1780
2394
  // array size
1781
- if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('min_items_violation', `'minItems ${schema.minItems}'`)}}`) }
1782
- if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('max_items_violation', `'maxItems ${schema.maxItems}'`)}}`) }
2395
+ if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`) }
2396
+ if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`) }
1783
2397
 
1784
- // uniqueItems
2398
+ // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
1785
2399
  if (schema.uniqueItems) {
1786
2400
  const si = ctx.varCounter++
1787
2401
  const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
1788
2402
  const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
1789
- const inner = isPrim
1790
- ? `const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){if(_s${si}.has(${v}[_i])){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(${v}[_i])}`
1791
- : `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k)){${fail('unique_items_violation', "'duplicates'")};break};_s${si}.add(_k)}`
2403
+ const maxItems = schema.maxItems
2404
+ const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
2405
+ let inner
2406
+ if (isPrim && maxItems && maxItems <= 16) {
2407
+ // Small primitive arrays: O(n²) nested loop, zero allocation
2408
+ inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
2409
+ } else if (isPrim) {
2410
+ inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
2411
+ } else {
2412
+ inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
2413
+ }
1792
2414
  lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
1793
2415
  }
1794
2416
 
1795
2417
  // object size
1796
- if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('min_properties_violation', `'minProperties ${schema.minProperties}'`)}}`)
1797
- if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('max_properties_violation', `'maxProperties ${schema.maxProperties}'`)}}`)
2418
+ if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
2419
+ if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
1798
2420
 
1799
2421
  // additionalProperties — skip if patternProperties present (handled in unified loop below)
2422
+ // Small property sets: direct === chain (no Set allocation)
1800
2423
  if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
1801
- const allowed = Object.keys(schema.properties).map(k => JSON.stringify(k)).join(',')
2424
+ const propKeys = Object.keys(schema.properties)
1802
2425
  const ci = ctx.varCounter++
1803
- lines.push(isObj
1804
- ? `{const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`
1805
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additional_property', `'extra: '+_k${ci}[_i]`)}}}`)
2426
+ if (propKeys.length <= 8) {
2427
+ // Direct chain: no Set allocation for small schemas
2428
+ const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&')
2429
+ lines.push(isObj
2430
+ ? `{const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
2431
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
2432
+ } else {
2433
+ const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
2434
+ lines.push(isObj
2435
+ ? `{const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
2436
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
2437
+ }
1806
2438
  }
1807
2439
 
1808
2440
  // dependentRequired
1809
2441
  if (schema.dependentRequired) {
1810
2442
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
1811
2443
  for (const dep of deps) {
1812
- const p = pathExpr ? `${pathExpr}+'/${esc(dep)}'` : `'/${esc(dep)}'`
1813
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push({code:'required_missing',path:${p},message:'${esc(key)} requires ${esc(dep)}'})}`)
2444
+ const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
2445
+ if (isStatic) {
2446
+ const ei = ctx.varCounter++
2447
+ const errVar = `_E${ei}`
2448
+ const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
2449
+ ctx.closureVars.push(errVar)
2450
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/dependentRequired`, params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
2451
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
2452
+ } else {
2453
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired',params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
2454
+ }
1814
2455
  }
1815
2456
  }
1816
2457
  }
@@ -1819,19 +2460,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1819
2460
  if (schema.properties) {
1820
2461
  for (const [key, prop] of Object.entries(schema.properties)) {
1821
2462
  const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
1822
- const childPath = pathExpr ? `${pathExpr}+'/${esc(key)}'` : `'/${esc(key)}'`
2463
+ const childPath = childPathExpr(pathExpr, esc(key))
1823
2464
  if (requiredSet.has(key) && isObj) {
1824
2465
  lines.push(`if(${pv}!==undefined){`)
1825
- genCodeC(prop, pv, childPath, lines, ctx)
2466
+ genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
1826
2467
  lines.push(`}`)
1827
2468
  } else if (isObj) {
1828
2469
  const oi = ctx.varCounter++
1829
2470
  lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
1830
- genCodeC(prop, `_o${oi}`, childPath, lines, ctx)
2471
+ genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
1831
2472
  lines.push(`}}`)
1832
2473
  } else {
1833
2474
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1834
- genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx)
2475
+ genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
1835
2476
  lines.push(`}`)
1836
2477
  }
1837
2478
  }
@@ -1888,61 +2529,61 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1888
2529
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
1889
2530
  // propertyNames checks (merged)
1890
2531
  if (pn) {
1891
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+${kVar}`)}}`)
1892
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+${kVar}`)}}`)
2532
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
2533
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
1893
2534
  if (pn.pattern) {
1894
2535
  const fast = fastPrefixCheck(pn.pattern, kVar)
1895
2536
  if (fast) {
1896
- lines.push(`if(!(${fast})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
2537
+ lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1897
2538
  } else {
1898
2539
  const ri = ctx.varCounter++
1899
2540
  ctx.closureVars.push(`_re${ri}`)
1900
2541
  ctx.closureVals.push(new RegExp(pn.pattern))
1901
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
2542
+ lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1902
2543
  }
1903
2544
  }
1904
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
2545
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
1905
2546
  if (pn.enum) {
1906
2547
  const ei = ctx.varCounter++
1907
2548
  ctx.closureVars.push(`_es${ei}`)
1908
2549
  ctx.closureVals.push(new Set(pn.enum))
1909
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+${kVar}`)}}`)
2550
+ lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1910
2551
  }
1911
2552
  }
1912
2553
  const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
1913
2554
  lines.push(`let _m${pi}=${matchExpr}`)
1914
2555
  for (let i = 0; i < ppEntries.length; i++) {
1915
- lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern_mismatch', `'patternProperties: value invalid for key '+${kVar}`)}}}`)
2556
+ lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}}`)
1916
2557
  }
1917
- lines.push(`if(!_m${pi}){${fail('additional_property', `'extra: '+${kVar}`)}}`)
2558
+ lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
1918
2559
  lines.push(`}}`)
1919
2560
  } else {
1920
2561
  ctx._ppHandledPropertyNamesC = !!pn
1921
2562
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
1922
2563
  if (pn) {
1923
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+${kVar}`)}}`)
1924
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+${kVar}`)}}`)
2564
+ if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
2565
+ if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
1925
2566
  if (pn.pattern) {
1926
2567
  const fast = fastPrefixCheck(pn.pattern, kVar)
1927
2568
  if (fast) {
1928
- lines.push(`if(!(${fast})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
2569
+ lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1929
2570
  } else {
1930
2571
  const ri = ctx.varCounter++
1931
2572
  ctx.closureVars.push(`_re${ri}`)
1932
2573
  ctx.closureVals.push(new RegExp(pn.pattern))
1933
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+${kVar}`)}}`)
2574
+ lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1934
2575
  }
1935
2576
  }
1936
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
2577
+ if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
1937
2578
  if (pn.enum) {
1938
2579
  const ei = ctx.varCounter++
1939
2580
  ctx.closureVars.push(`_es${ei}`)
1940
2581
  ctx.closureVals.push(new Set(pn.enum))
1941
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+${kVar}`)}}`)
2582
+ lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1942
2583
  }
1943
2584
  }
1944
2585
  for (let i = 0; i < ppEntries.length; i++) {
1945
- lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern_mismatch', `'patternProperties: value invalid for key '+${kVar}`)}}`)
2586
+ lines.push(`if(${matchers[i].check}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}`)
1946
2587
  }
1947
2588
  lines.push(`}}`)
1948
2589
  }
@@ -1952,7 +2593,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1952
2593
  if (schema.dependentSchemas) {
1953
2594
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
1954
2595
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
1955
- genCodeC(depSchema, v, pathExpr, lines, ctx)
2596
+ genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
1956
2597
  lines.push(`}`)
1957
2598
  }
1958
2599
  }
@@ -1963,25 +2604,25 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1963
2604
  const ki = ctx.varCounter++
1964
2605
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
1965
2606
  if (pn.minLength !== undefined) {
1966
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('min_length_violation', `'propertyNames: key too short: '+_k${ki}`)}}`)
2607
+ lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
1967
2608
  }
1968
2609
  if (pn.maxLength !== undefined) {
1969
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('max_length_violation', `'propertyNames: key too long: '+_k${ki}`)}}`)
2610
+ lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
1970
2611
  }
1971
2612
  if (pn.pattern) {
1972
2613
  const ri = ctx.varCounter++
1973
2614
  ctx.closureVars.push(`_re${ri}`)
1974
2615
  ctx.closureVals.push(new RegExp(pn.pattern))
1975
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern_mismatch', `'propertyNames: pattern mismatch: '+_k${ki}`)}}`)
2616
+ lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
1976
2617
  }
1977
2618
  if (pn.const !== undefined) {
1978
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const_mismatch', `'propertyNames: expected '+${JSON.stringify(pn.const)}`)}}`)
2619
+ lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
1979
2620
  }
1980
2621
  if (pn.enum) {
1981
2622
  const ei = ctx.varCounter++
1982
2623
  ctx.closureVars.push(`_es${ei}`)
1983
2624
  ctx.closureVals.push(new Set(pn.enum))
1984
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum_mismatch', `'propertyNames: key not in enum: '+_k${ki}`)}}`)
2625
+ lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1985
2626
  }
1986
2627
  lines.push(`}}`)
1987
2628
  }
@@ -1991,18 +2632,18 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
1991
2632
  const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
1992
2633
  const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
1993
2634
  ctx.varCounter++
1994
- const childPath = pathExpr ? `${pathExpr}+'/'+${idx}` : `'/'+${idx}`
2635
+ const childPath = childPathDynExpr(pathExpr, idx)
1995
2636
  lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
1996
- genCodeC(schema.items, elem, childPath, lines, ctx)
2637
+ genCodeC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
1997
2638
  lines.push(`}}`)
1998
2639
  }
1999
2640
 
2000
2641
  // prefixItems
2001
2642
  if (schema.prefixItems) {
2002
2643
  for (let i = 0; i < schema.prefixItems.length; i++) {
2003
- const childPath = pathExpr ? `${pathExpr}+'/${i}'` : `'/${i}'`
2644
+ const childPath = childPathExpr(pathExpr, String(i))
2004
2645
  lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
2005
- genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx)
2646
+ genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
2006
2647
  lines.push(`}`)
2007
2648
  }
2008
2649
  }
@@ -2016,26 +2657,26 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
2016
2657
  const minC = schema.minContains !== undefined ? schema.minContains : 1
2017
2658
  const maxC = schema.maxContains
2018
2659
  lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
2019
- lines.push(`if(_cc${ci}<${minC}){${fail('contains_violation', `'need ${minC}+ matches'`)}}`)
2020
- if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains_violation', `'max ${maxC} matches'`)}}`)
2660
+ lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
2661
+ if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
2021
2662
  lines.push(`}`)
2022
2663
  }
2023
2664
 
2024
2665
  // allOf
2025
- if (schema.allOf) { for (const sub of schema.allOf) genCodeC(sub, v, pathExpr, lines, ctx) }
2666
+ if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
2026
2667
 
2027
2668
  // anyOf
2028
2669
  if (schema.anyOf) {
2029
2670
  const fi = ctx.varCounter++
2030
2671
  const fns = schema.anyOf.map(sub => { const sl = []; genCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
2031
- lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('any_of_failed', "'no match'")}}}`)
2672
+ lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
2032
2673
  }
2033
2674
 
2034
2675
  // oneOf
2035
2676
  if (schema.oneOf) {
2036
2677
  const fi = ctx.varCounter++
2037
2678
  const fns = schema.oneOf.map(sub => { const sl = []; genCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
2038
- lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('one_of_failed', "'need exactly 1'")}}}`)
2679
+ lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
2039
2680
  }
2040
2681
 
2041
2682
  // not
@@ -2043,7 +2684,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
2043
2684
  const sl = []; genCode(schema.not, '_nv', sl, ctx)
2044
2685
  const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
2045
2686
  const fi = ctx.varCounter++
2046
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not_failed', "'should not match'")}}}`)
2687
+ lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
2047
2688
  }
2048
2689
 
2049
2690
  // if/then/else
@@ -2052,8 +2693,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
2052
2693
  const fi = ctx.varCounter++
2053
2694
  const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
2054
2695
  lines.push(`{const _if${fi}=${ifFn}`)
2055
- if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx); lines.push(`}`) }
2056
- if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx); lines.push(`}`) }
2696
+ if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
2697
+ if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
2057
2698
  lines.push(`}`)
2058
2699
  }
2059
2700
 
@@ -2063,4 +2704,124 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
2063
2704
  }
2064
2705
  }
2065
2706
 
2066
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined }
2707
+ // Collect statically-known evaluated properties/items from a schema.
2708
+ // Returns { props: string[], items: number|null, allProps: bool, allItems: bool, dynamic: bool }
2709
+ function collectEvaluated(schema, schemaMap, rootDefs) {
2710
+ if (typeof schema !== 'object' || schema === null) return { props: [], items: null, allProps: false, allItems: false, dynamic: false }
2711
+ const defs = rootDefs || schema.$defs || schema.definitions || null
2712
+ const result = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
2713
+ _collectEval(schema, result, defs, schemaMap, new Set(), true)
2714
+ return result
2715
+ }
2716
+
2717
+ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
2718
+ if (typeof schema !== 'object' || schema === null) return
2719
+ if (result.allProps && result.allItems) return
2720
+
2721
+ // $ref — inline
2722
+ if (schema.$ref) {
2723
+ const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
2724
+ if (m && defs && defs[m[1]]) {
2725
+ if (refStack.has(schema.$ref)) { result.dynamic = true; return }
2726
+ refStack.add(schema.$ref)
2727
+ _collectEval(defs[m[1]], result, defs, schemaMap, refStack)
2728
+ refStack.delete(schema.$ref)
2729
+ } else if (schemaMap && typeof schemaMap.get === 'function' && schemaMap.has(schema.$ref)) {
2730
+ if (refStack.has(schema.$ref)) { result.dynamic = true; return }
2731
+ refStack.add(schema.$ref)
2732
+ _collectEval(schemaMap.get(schema.$ref), result, defs, schemaMap, refStack)
2733
+ refStack.delete(schema.$ref)
2734
+ }
2735
+ // In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
2736
+ const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
2737
+ if (!hasOtherKeywords) return
2738
+ }
2739
+
2740
+ // properties → static keys
2741
+ if (schema.properties) {
2742
+ for (const k of Object.keys(schema.properties)) {
2743
+ if (!result.props.includes(k)) result.props.push(k)
2744
+ }
2745
+ }
2746
+
2747
+ // additionalProperties: true/schema → all props evaluated
2748
+ if (schema.additionalProperties !== undefined && schema.additionalProperties !== false) {
2749
+ result.allProps = true
2750
+ }
2751
+
2752
+ // patternProperties → dynamic
2753
+ if (schema.patternProperties) {
2754
+ result.dynamic = true
2755
+ }
2756
+
2757
+ // prefixItems → max index
2758
+ if (schema.prefixItems) {
2759
+ const count = schema.prefixItems.length
2760
+ result.items = result.items === null ? count : Math.max(result.items, count)
2761
+ }
2762
+
2763
+ // items: schema/true → all items evaluated
2764
+ if (schema.items && typeof schema.items === 'object') {
2765
+ result.allItems = true
2766
+ }
2767
+ if (schema.items === true) {
2768
+ result.allItems = true
2769
+ }
2770
+
2771
+ // contains interaction with unevaluatedItems is complex
2772
+ // At root level: contains + unevaluatedItems needs dynamic tracking
2773
+ // In nested schemas: contains marks all items as evaluated
2774
+ if (schema.contains) {
2775
+ if (isRoot && (schema.unevaluatedItems !== undefined)) {
2776
+ result.dynamic = true
2777
+ } else {
2778
+ result.allItems = true
2779
+ }
2780
+ }
2781
+
2782
+ // unevaluatedProperties: true/schema → all props evaluated (for nested schemas only)
2783
+ // At root level, unevaluatedProperties is what we're computing FOR, not a contributor
2784
+ if (!isRoot && (schema.unevaluatedProperties === true || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null))) {
2785
+ result.allProps = true
2786
+ }
2787
+ // unevaluatedItems: true/schema → all items evaluated (for nested schemas only)
2788
+ if (!isRoot && (schema.unevaluatedItems === true || (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null))) {
2789
+ result.allItems = true
2790
+ }
2791
+
2792
+ // allOf → merge all (unconditional)
2793
+ if (schema.allOf) {
2794
+ for (const sub of schema.allOf) {
2795
+ _collectEval(sub, result, defs, schemaMap, refStack)
2796
+ }
2797
+ }
2798
+
2799
+ // anyOf / oneOf → dynamic (conditional merge)
2800
+ if (schema.anyOf || schema.oneOf) {
2801
+ result.dynamic = true
2802
+ const branches = schema.anyOf || schema.oneOf
2803
+ for (const sub of branches) {
2804
+ _collectEval(sub, result, defs, schemaMap, refStack)
2805
+ }
2806
+ }
2807
+
2808
+ // if/then/else → dynamic (branch-dependent)
2809
+ if (schema.if && (schema.then || schema.else)) {
2810
+ result.dynamic = true
2811
+ _collectEval(schema.if, result, defs, schemaMap, refStack)
2812
+ if (schema.then) _collectEval(schema.then, result, defs, schemaMap, refStack)
2813
+ if (schema.else) _collectEval(schema.else, result, defs, schemaMap, refStack)
2814
+ }
2815
+
2816
+ // dependentSchemas → dynamic
2817
+ if (schema.dependentSchemas) {
2818
+ result.dynamic = true
2819
+ for (const sub of Object.values(schema.dependentSchemas)) {
2820
+ _collectEval(sub, result, defs, schemaMap, refStack)
2821
+ }
2822
+ }
2823
+
2824
+ // not → contributes nothing (spec: annotations from not are discarded)
2825
+ }
2826
+
2827
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, AJV_MESSAGES }