@altopelago/aeos-core 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/validate.js CHANGED
@@ -12,7 +12,7 @@ import { checkPresence } from './rules/presence.js';
12
12
  import { checkTypes } from './rules/typeCheck.js';
13
13
  import { checkReferenceForms } from './rules/referenceForm.js';
14
14
  import { checkNumericForm } from './rules/numericForm.js';
15
- import { checkStringForm, checkPatterns } from './rules/stringForm.js';
15
+ import { checkStringForm, checkPatterns, matchesPortablePattern } from './rules/stringForm.js';
16
16
  const TYPE_ALIASES = {
17
17
  NumberLiteral: ['NumberLiteral'],
18
18
  StringLiteral: ['StringLiteral'],
@@ -22,6 +22,17 @@ const TYPE_ALIASES = {
22
22
  ListNode: ['ListNode'],
23
23
  ListLiteral: ['ListNode', 'ListLiteral'],
24
24
  TupleLiteral: ['TupleLiteral'],
25
+ ToggleLiteral: ['ToggleLiteral'],
26
+ InfinityLiteral: ['InfinityLiteral'],
27
+ NaNLiteral: ['NaNLiteral'],
28
+ HexLiteral: ['HexLiteral'],
29
+ RadixLiteral: ['RadixLiteral'],
30
+ EncodingLiteral: ['EncodingLiteral'],
31
+ SeparatorLiteral: ['SeparatorLiteral'],
32
+ DateLiteral: ['DateLiteral'],
33
+ TimeLiteral: ['TimeLiteral'],
34
+ DateTimeLiteral: ['DateTimeLiteral'],
35
+ ZRUTDateTimeLiteral: ['ZRUTDateTimeLiteral'],
25
36
  CloneReference: ['CloneReference'],
26
37
  PointerReference: ['PointerReference'],
27
38
  NodeLiteral: ['NodeLiteral'],
@@ -29,6 +40,113 @@ const TYPE_ALIASES = {
29
40
  function formatQuotedMemberSegment(key) {
30
41
  return `.[${JSON.stringify(String(key))}]`;
31
42
  }
43
+ const DEFAULT_RESOURCE_POLICY = {
44
+ max_events: 100_000,
45
+ max_rules: 10_000,
46
+ max_any_of_cases: 64,
47
+ max_schema_depth: 64,
48
+ max_path_length: 4_096,
49
+ max_reference_resolution_steps: 64,
50
+ max_selector_expansions: 100_000,
51
+ max_string_length_default: 10_000_000,
52
+ max_container_children_default: 1_000_000,
53
+ };
54
+ function normalizeResourcePolicy(policy, source, ctx) {
55
+ if (policy === undefined)
56
+ return {};
57
+ if (policy === null || typeof policy !== 'object') {
58
+ emitResourceError(ctx, '$', `${source} resource policy must be an object`);
59
+ return {};
60
+ }
61
+ const normalized = {};
62
+ for (const key of Object.keys(policy)) {
63
+ if (!(key in DEFAULT_RESOURCE_POLICY)) {
64
+ emitResourceError(ctx, '$', `Unknown ${source} resource policy key: ${String(key)}`);
65
+ continue;
66
+ }
67
+ const value = policy[key];
68
+ if (value !== undefined && (typeof value !== 'number' || !Number.isInteger(value) || value < 0)) {
69
+ emitResourceError(ctx, '$', `${source} resource policy ${String(key)} must be a non-negative integer`);
70
+ continue;
71
+ }
72
+ if (value !== undefined) {
73
+ normalized[key] = value;
74
+ }
75
+ }
76
+ return normalized;
77
+ }
78
+ function resolveResourcePolicy(schemaPolicy, optionPolicy, ctx) {
79
+ return {
80
+ ...DEFAULT_RESOURCE_POLICY,
81
+ ...normalizeResourcePolicy(schemaPolicy, 'schema', ctx),
82
+ ...normalizeResourcePolicy(optionPolicy, 'option', ctx),
83
+ };
84
+ }
85
+ function emitResourceError(ctx, path, message, span = null) {
86
+ emitError(ctx, createDiag(path, span, message, ErrorCodes.INVALID_SCHEMA_POLICY));
87
+ }
88
+ const STRING_LIKE_VALUE_TYPES = new Set([
89
+ 'StringLiteral',
90
+ 'TrimtickLiteral',
91
+ 'SeparatorLiteral',
92
+ 'HexLiteral',
93
+ 'EncodingLiteral',
94
+ 'NullLiteral',
95
+ 'DateLiteral',
96
+ 'TimeLiteral',
97
+ 'DateTimeLiteral',
98
+ 'ZRUTDateTimeLiteral',
99
+ ]);
100
+ function stringLikePayloadLength(event) {
101
+ if (!STRING_LIKE_VALUE_TYPES.has(event.type))
102
+ return null;
103
+ const payload = event.value.length > 0 ? event.value : event.raw;
104
+ return payload.length;
105
+ }
106
+ function enforceStringLengthResourceBudget(info, path, policy, ctx) {
107
+ const payloadLength = stringLikePayloadLength(info);
108
+ if (payloadLength !== null && payloadLength > policy.max_string_length_default) {
109
+ emitResourceError(ctx, path, `String-like payload length ${payloadLength} exceeds max_string_length_default ${policy.max_string_length_default}`, info.span);
110
+ }
111
+ for (const [key, attribute] of info.attributes ?? []) {
112
+ enforceStringLengthResourceBudget(attribute, `${path}@${key}`, policy, ctx);
113
+ }
114
+ }
115
+ function inspectSchemaResourceShape(schema, policy, ctx) {
116
+ for (const rule of schema.rules) {
117
+ const rulePath = typeof rule.path === 'string' && rule.path.length > 0
118
+ ? rule.path
119
+ : typeof rule.selector === 'string' && rule.selector.length > 0
120
+ ? rule.selector
121
+ : '$';
122
+ if (rulePath.length > policy.max_path_length) {
123
+ emitResourceError(ctx, rulePath, `Rule path length ${rulePath.length} exceeds max_path_length ${policy.max_path_length}`);
124
+ }
125
+ inspectConstraintResourceShape(rule.constraints, rulePath, 1, policy, ctx);
126
+ }
127
+ for (const [datatype, constraints] of Object.entries(schema.datatype_rules ?? {})) {
128
+ inspectConstraintResourceShape(constraints, `datatype_rules.${datatype}`, 1, policy, ctx);
129
+ }
130
+ }
131
+ function inspectConstraintResourceShape(constraints, path, depth, policy, ctx) {
132
+ if (depth > policy.max_schema_depth) {
133
+ emitResourceError(ctx, path, `Schema constraint depth exceeds max_schema_depth ${policy.max_schema_depth}`);
134
+ return;
135
+ }
136
+ if (constraints.any_of !== undefined) {
137
+ if (constraints.any_of.length > policy.max_any_of_cases) {
138
+ emitResourceError(ctx, path, `any_of case count ${constraints.any_of.length} exceeds max_any_of_cases ${policy.max_any_of_cases}`);
139
+ }
140
+ constraints.any_of.forEach((branch, index) => {
141
+ inspectConstraintResourceShape(branch, `${path}.any_of[${index}]`, depth + 1, policy, ctx);
142
+ });
143
+ }
144
+ if (constraints.attributes !== undefined) {
145
+ for (const [key, child] of Object.entries(constraints.attributes)) {
146
+ inspectConstraintResourceShape(child, `${path}@${key}`, depth + 1, policy, ctx);
147
+ }
148
+ }
149
+ }
32
150
  /**
33
151
  * Validate an AES against a schema.
34
152
  *
@@ -59,6 +177,20 @@ export function validate(aes, schema, options = {}) {
59
177
  // TODO: Phase 9 - Guarantees
60
178
  // Phase 1: Envelope plumbing
61
179
  const ctx = createDiagContext();
180
+ const resourcePolicy = resolveResourcePolicy(schema.resource_policy, options.resourcePolicy, ctx);
181
+ if (ctx.errors.length > 0) {
182
+ return createFailingEnvelope(ctx.errors, ctx.warnings, {});
183
+ }
184
+ if (aes.length > resourcePolicy.max_events) {
185
+ emitResourceError(ctx, '$', `AES event count ${aes.length} exceeds max_events ${resourcePolicy.max_events}`);
186
+ }
187
+ if (schema.rules.length > resourcePolicy.max_rules) {
188
+ emitResourceError(ctx, '$', `Schema rule count ${schema.rules.length} exceeds max_rules ${resourcePolicy.max_rules}`);
189
+ }
190
+ inspectSchemaResourceShape(schema, resourcePolicy, ctx);
191
+ if (ctx.errors.length > 0) {
192
+ return createFailingEnvelope(ctx.errors, ctx.warnings, {});
193
+ }
62
194
  // Phase 3: (moved to run after Phase 2)
63
195
  // Helpers: format canonical path (local, no runtime AEON deps)
64
196
  function formatCanonicalPath(path) {
@@ -171,6 +303,9 @@ export function validate(aes, schema, options = {}) {
171
303
  for (let i = 0; i < aes.length; i++) {
172
304
  const event = aes[i];
173
305
  const pathStr = formatCanonicalPath(event.path);
306
+ if (pathStr.length > resourcePolicy.max_path_length) {
307
+ emitResourceError(ctx, pathStr, `Path length ${pathStr.length} exceeds max_path_length ${resourcePolicy.max_path_length}`, toTuple(event.span));
308
+ }
174
309
  if (Array.isArray(event.path?.segments)) {
175
310
  for (const seg of event.path.segments) {
176
311
  if (seg?.type === 'index') {
@@ -206,16 +341,31 @@ export function validate(aes, schema, options = {}) {
206
341
  if ((event.value.type === 'TupleLiteral' || event.value.type === 'ListLiteral' || event.value.type === 'ListNode')
207
342
  && Array.isArray(event.value.elements)) {
208
343
  containerArity.set(pathStr, event.value.elements.length);
344
+ if (event.value.elements.length > resourcePolicy.max_container_children_default) {
345
+ emitResourceError(ctx, pathStr, `Container child count ${event.value.elements.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
346
+ }
209
347
  hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
210
348
  }
349
+ else if (event.value.type === 'ObjectNode' && Array.isArray(event.value.bindings)) {
350
+ containerArity.set(pathStr, event.value.bindings.length);
351
+ if (event.value.bindings.length > resourcePolicy.max_container_children_default) {
352
+ emitResourceError(ctx, pathStr, `Container child count ${event.value.bindings.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
353
+ }
354
+ }
211
355
  else if (event.value.type === 'NodeLiteral' && Array.isArray(event.value.children)) {
212
356
  containerArity.set(pathStr, event.value.children.length);
357
+ if (event.value.children.length > resourcePolicy.max_container_children_default) {
358
+ emitResourceError(ctx, pathStr, `Container child count ${event.value.children.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
359
+ }
213
360
  hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
214
361
  }
215
362
  }
216
363
  }
217
364
  // Register index even for first occurrence
218
365
  }
366
+ for (const [path, info] of eventsByPath) {
367
+ enforceStringLengthResourceBudget(info, path, resourcePolicy, ctx);
368
+ }
219
369
  // Optional separator literal trailing-delimiter policy
220
370
  if (trailingSeparatorPolicy !== 'off') {
221
371
  for (const event of aes) {
@@ -240,33 +390,63 @@ export function validate(aes, schema, options = {}) {
240
390
  }
241
391
  // Phase 3: Build rule index from schema (run after baseline invariants)
242
392
  const ruleIndex = buildRuleIndex(schema, ctx);
393
+ const selectorExpansionBudget = { count: 0 };
394
+ const expandedRuleIndex = expandWildcardRules(expandSelectorRules(ruleIndex, schema, eventsByPath, ctx, resourcePolicy, selectorExpansionBudget), eventsByPath, ctx, resourcePolicy, selectorExpansionBudget);
395
+ const effectiveRuleIndex = mergeDatatypeRules(expandedRuleIndex, schema.datatype_rules, eventsByPath);
243
396
  // Phase 4: Presence checks (required fields)
244
397
  const boundPaths = new Set(seen.keys());
245
- checkPresence(ruleIndex, boundPaths, ctx);
398
+ checkPresence(effectiveRuleIndex, boundPaths, ctx);
246
399
  checkWorldPolicy(schema, aes, boundPaths, ctx);
247
400
  // Phase 5: Type checks (literal kind)
248
- checkReferenceForms(schema, ruleIndex, eventsByPath, ctx);
249
- const effectiveEventsByPath = resolveReferenceFormEvents(ruleIndex, eventsByPath);
250
- checkTypes(ruleIndex, effectiveEventsByPath, ctx);
251
- // Phase 5b: core v1 arity checks for tuple/list containers
252
- for (const [path, rule] of ruleIndex) {
253
- const expectedLength = rule.constraints.length_exact;
254
- if (expectedLength === undefined)
401
+ checkReferenceForms(schema, effectiveRuleIndex, eventsByPath, ctx);
402
+ const effectiveEventsByPath = resolveReferenceFormEvents(effectiveRuleIndex, eventsByPath, resourcePolicy, ctx);
403
+ const selectedRuleIndex = selectAnyOfRules(effectiveRuleIndex, effectiveEventsByPath, ctx);
404
+ checkTypes(selectedRuleIndex, effectiveEventsByPath, ctx);
405
+ // Phase 5b: core v1 arity/cardinality checks for tuple/list/node containers
406
+ for (const [path, rule] of selectedRuleIndex) {
407
+ const { length_exact, min_children, max_children } = rule.constraints;
408
+ if (length_exact === undefined && min_children === undefined && max_children === undefined)
255
409
  continue;
256
410
  const actualLength = containerArity.get(path);
257
411
  if (actualLength === undefined)
258
412
  continue;
259
- if (typeof expectedLength === 'number' && actualLength !== expectedLength) {
413
+ const span = eventsByPath.get(path)?.span ?? null;
414
+ if (typeof length_exact === 'number' && actualLength !== length_exact) {
415
+ emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected exactly ${length_exact} children, got ${actualLength}`, ErrorCodes.TUPLE_ARITY_MISMATCH));
416
+ }
417
+ if (typeof min_children === 'number' && actualLength < min_children) {
418
+ emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected at least ${min_children} children, got ${actualLength}`, ErrorCodes.CONTAINER_CARDINALITY_MISMATCH));
419
+ }
420
+ if (typeof max_children === 'number' && actualLength > max_children) {
421
+ emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected at most ${max_children} children, got ${actualLength}`, ErrorCodes.CONTAINER_CARDINALITY_MISMATCH));
422
+ }
423
+ }
424
+ checkLexicalLiteralConstraints(selectedRuleIndex, effectiveEventsByPath, ctx);
425
+ // Phase 5c: constraints that widen NumberLiteral type acceptance to infinity/NaN
426
+ for (const [path, rule] of selectedRuleIndex) {
427
+ const event = effectiveEventsByPath.get(path);
428
+ if (!event)
429
+ continue;
430
+ if (event.type === 'InfinityLiteral' && rule.constraints.allow_infinity !== true) {
431
+ continue;
432
+ }
433
+ if (event.type === 'NaNLiteral' && rule.constraints.allow_nan !== true) {
434
+ continue;
435
+ }
436
+ if ((event.type === 'InfinityLiteral' || event.type === 'NaNLiteral')
437
+ && rule.constraints.type !== undefined
438
+ && !isNumericExpectedType(rule.constraints.type)) {
260
439
  const span = eventsByPath.get(path)?.span ?? null;
261
- emitError(ctx, createDiag(path, span, `Tuple/List arity mismatch: expected ${expectedLength}, got ${actualLength}`, ErrorCodes.TUPLE_ARITY_MISMATCH));
440
+ emitError(ctx, createDiag(path, span, `Type mismatch: expected ${rule.constraints.type}, got ${event.type}`, ErrorCodes.TYPE_MISMATCH));
262
441
  }
263
442
  }
264
443
  // Phase 6: Numeric form constraints (sign, digit count)
265
- checkNumericForm(ruleIndex, effectiveEventsByPath, ctx);
444
+ checkNumericForm(selectedRuleIndex, effectiveEventsByPath, ctx);
266
445
  // Phase 7: String form constraints (length, pattern)
267
- checkStringForm(ruleIndex, effectiveEventsByPath, ctx);
268
- checkPatterns(ruleIndex, effectiveEventsByPath, ctx);
269
- checkAttributeConstraints(ruleIndex, effectiveEventsByPath, schema.datatype_rules, ctx);
446
+ checkStringForm(selectedRuleIndex, effectiveEventsByPath, ctx);
447
+ checkPatterns(selectedRuleIndex, effectiveEventsByPath, ctx);
448
+ checkAttributePolicy(schema, selectedRuleIndex, effectiveEventsByPath, ctx);
449
+ checkAttributeConstraints(selectedRuleIndex, effectiveEventsByPath, schema.datatype_rules, ctx);
270
450
  checkDatatypeRules(schema.datatype_rules, effectiveEventsByPath, ctx);
271
451
  if (ctx.errors.length > 0) {
272
452
  return createFailingEnvelope(ctx.errors, ctx.warnings, {});
@@ -317,7 +497,13 @@ export function validate(aes, schema, options = {}) {
317
497
  function checkWorldPolicy(schema, aes, boundPaths, ctx) {
318
498
  if ((schema.world ?? 'open') !== 'closed')
319
499
  return;
320
- const allowedPaths = schema.rules.map((rule) => rule.path);
500
+ const allowedRules = schema.rules
501
+ .map((rule) => typeof rule.path === 'string' && rule.path.length > 0
502
+ ? { kind: 'path', value: rule.path }
503
+ : typeof rule.selector === 'string' && rule.selector.length > 0
504
+ ? { kind: 'selector', value: rule.selector }
505
+ : null)
506
+ .filter((rule) => rule !== null);
321
507
  for (const event of aes) {
322
508
  const key = typeof event.key === 'string' ? event.key : '';
323
509
  if (key.startsWith('aeon:'))
@@ -325,12 +511,14 @@ function checkWorldPolicy(schema, aes, boundPaths, ctx) {
325
511
  const path = formatCanonicalPathLocal(event.path);
326
512
  if (!boundPaths.has(path))
327
513
  continue;
328
- if (allowedPaths.some((allowedPath) => matchesAllowedPath(path, allowedPath)))
514
+ if (allowedRules.some((rule) => rule.kind === 'selector'
515
+ ? matchesSelectorPath(path, rule.value)
516
+ : matchesAllowedPath(path, rule.value)))
329
517
  continue;
330
518
  emitError(ctx, createDiag(path, toTupleLocal(event.span), `Binding '${path}' is not allowed by closed-world schema`, ErrorCodes.UNEXPECTED_BINDING));
331
519
  }
332
520
  }
333
- function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
521
+ function resolveReferenceFormEvents(ruleIndex, eventsByPath, resourcePolicy, ctx) {
334
522
  const resolved = new Map(eventsByPath);
335
523
  for (const [path, rule] of ruleIndex.entries()) {
336
524
  if (rule.constraints.resolve_reference_form !== true)
@@ -338,8 +526,12 @@ function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
338
526
  const event = eventsByPath.get(path);
339
527
  if (!event || !isReferenceType(event.type) || !event.referencePath)
340
528
  continue;
341
- const terminal = resolveTerminalReferenceEvent(event, eventsByPath, new Set());
529
+ const resolutionState = { exhausted: false };
530
+ const terminal = resolveTerminalReferenceEvent(event, eventsByPath, new Set(), resourcePolicy.max_reference_resolution_steps, resolutionState);
342
531
  if (!terminal) {
532
+ if (resolutionState.exhausted) {
533
+ emitResourceError(ctx, path, `Reference resolution exceeded max_reference_resolution_steps ${resourcePolicy.max_reference_resolution_steps}`, event.span);
534
+ }
343
535
  resolved.delete(path);
344
536
  continue;
345
537
  }
@@ -350,10 +542,135 @@ function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
350
542
  }
351
543
  return resolved;
352
544
  }
353
- function resolveTerminalReferenceEvent(event, eventsByPath, activePaths) {
545
+ function expandSelectorRules(ruleIndex, schema, eventsByPath, ctx, resourcePolicy, expansionBudget) {
546
+ const expanded = new Map(ruleIndex);
547
+ for (const rule of schema.rules) {
548
+ if (typeof rule.selector !== 'string' || rule.selector.length === 0)
549
+ continue;
550
+ if (typeof rule.path === 'string' && rule.path.length > 0)
551
+ continue;
552
+ let matched = false;
553
+ for (const actualPath of eventsByPath.keys()) {
554
+ if (!matchesSelectorPath(actualPath, rule.selector))
555
+ continue;
556
+ matched = true;
557
+ expansionBudget.count += 1;
558
+ if (expansionBudget.count > resourcePolicy.max_selector_expansions) {
559
+ emitResourceError(ctx, rule.selector, `Selector expansion count exceeds max_selector_expansions ${resourcePolicy.max_selector_expansions}`);
560
+ return expanded;
561
+ }
562
+ if (!expanded.has(actualPath)) {
563
+ expanded.set(actualPath, { ...rule, path: actualPath });
564
+ }
565
+ }
566
+ if (!matched && rule.constraints.required === true) {
567
+ emitError(ctx, createDiag(rule.selector, null, `Missing required field: ${rule.selector}`, ErrorCodes.MISSING_REQUIRED_FIELD));
568
+ }
569
+ }
570
+ return expanded;
571
+ }
572
+ function expandWildcardRules(ruleIndex, eventsByPath, ctx, resourcePolicy, expansionBudget) {
573
+ const expanded = new Map(ruleIndex);
574
+ for (const [path, rule] of ruleIndex.entries()) {
575
+ if (!path.includes('[*]'))
576
+ continue;
577
+ expanded.delete(path);
578
+ for (const actualPath of eventsByPath.keys()) {
579
+ if (matchesAllowedPath(actualPath, path)) {
580
+ expansionBudget.count += 1;
581
+ if (expansionBudget.count > resourcePolicy.max_selector_expansions) {
582
+ emitResourceError(ctx, path, `Wildcard expansion count exceeds max_selector_expansions ${resourcePolicy.max_selector_expansions}`);
583
+ return expanded;
584
+ }
585
+ expanded.set(actualPath, { ...rule, path: actualPath });
586
+ }
587
+ }
588
+ }
589
+ return expanded;
590
+ }
591
+ function selectAnyOfRules(ruleIndex, eventsByPath, ctx) {
592
+ const selected = new Map(ruleIndex);
593
+ for (const [path, rule] of ruleIndex.entries()) {
594
+ if (!Array.isArray(rule.constraints.any_of))
595
+ continue;
596
+ const event = eventsByPath.get(path);
597
+ if (!event)
598
+ continue;
599
+ const outer = withoutAnyOf(rule.constraints);
600
+ const branch = rule.constraints.any_of.find((candidate) => constraintBranchMatchesEvent(candidate, event));
601
+ if (!branch) {
602
+ emitError(ctx, createDiag(path, event.span, `Value does not match any allowed constraint branch at ${path}`, ErrorCodes.TYPE_MISMATCH));
603
+ selected.set(path, { ...rule, constraints: outer });
604
+ continue;
605
+ }
606
+ selected.set(path, { ...rule, constraints: { ...outer, ...branch } });
607
+ }
608
+ return selected;
609
+ }
610
+ function withoutAnyOf(constraints) {
611
+ const { any_of: _anyOf, ...rest } = constraints;
612
+ return rest;
613
+ }
614
+ function patternMatches(pattern, value) {
615
+ return matchesPortablePattern(pattern, value);
616
+ }
617
+ function constraintBranchMatchesEvent(constraints, event) {
618
+ if (constraints.type_is !== undefined) {
619
+ const containerOk = constraints.type_is === 'list'
620
+ ? (event.type === 'ListLiteral' || event.type === 'ListNode')
621
+ : event.type === 'TupleLiteral';
622
+ if (!containerOk)
623
+ return false;
624
+ }
625
+ if (constraints.type !== undefined && !constraintTypeMatches(event.type, constraints.type, event.raw, constraints)) {
626
+ return false;
627
+ }
628
+ if (constraints.datatype !== undefined && event.datatype !== constraints.datatype) {
629
+ return false;
630
+ }
631
+ if (event.type === 'NullLiteral' && !nullValueMatches(event.value, constraints)) {
632
+ return false;
633
+ }
634
+ if (event.type === 'ToggleLiteral' && constraints.toggle_pair !== undefined && constraints.toggle_pair !== 'any') {
635
+ const value = (event.raw || event.value).toLowerCase();
636
+ const allowed = constraints.toggle_pair === 'yes_no'
637
+ ? ['yes', 'no']
638
+ : constraints.toggle_pair === 'on_off'
639
+ ? ['on', 'off']
640
+ : [];
641
+ if (allowed.length > 0 && !allowed.includes(value))
642
+ return false;
643
+ }
644
+ if (isStringType(event.type)) {
645
+ const valueLength = event.value.length;
646
+ if (constraints.min_length !== undefined && valueLength < constraints.min_length)
647
+ return false;
648
+ if (constraints.max_length !== undefined && valueLength > constraints.max_length)
649
+ return false;
650
+ if (!patternMatches(constraints.pattern, event.value))
651
+ return false;
652
+ }
653
+ if (hasDigitFormConstraints(constraints) && isDigitFormLiteral(event.type)) {
654
+ const digitCount = countFormDigits(event.type, event.raw);
655
+ if (constraints.sign === 'unsigned' && isFormNegative(event.raw))
656
+ return false;
657
+ if (constraints.min_digits !== undefined && digitCount < constraints.min_digits)
658
+ return false;
659
+ if (constraints.max_digits !== undefined && digitCount > constraints.max_digits)
660
+ return false;
661
+ if (event.type === 'RadixLiteral' && constraints.radix !== undefined && !radixConstraintMatches(event.datatype, event.raw, constraints))
662
+ return false;
663
+ }
664
+ return true;
665
+ }
666
+ function resolveTerminalReferenceEvent(event, eventsByPath, activePaths, remainingSteps, state) {
354
667
  if (!isReferenceType(event.type) || !event.referencePath) {
355
668
  return event;
356
669
  }
670
+ if (remainingSteps <= 0) {
671
+ state.exhausted = true;
672
+ return null;
673
+ }
357
674
  const targetPath = formatReferenceLookupPath(event.referencePath);
358
675
  if (activePaths.has(targetPath)) {
359
676
  return null;
@@ -364,7 +681,7 @@ function resolveTerminalReferenceEvent(event, eventsByPath, activePaths) {
364
681
  }
365
682
  activePaths.add(targetPath);
366
683
  const resolved = isReferenceType(target.type)
367
- ? resolveTerminalReferenceEvent(target, eventsByPath, activePaths)
684
+ ? resolveTerminalReferenceEvent(target, eventsByPath, activePaths, remainingSteps - 1, state)
368
685
  : target;
369
686
  activePaths.delete(targetPath);
370
687
  return resolved;
@@ -404,6 +721,179 @@ function matchesAllowedPath(actualPath, allowedPath) {
404
721
  const pattern = `^${escaped}$`;
405
722
  return new RegExp(pattern).test(actualPath);
406
723
  }
724
+ function tokenizeCanonicalLikePath(path) {
725
+ if (!path.startsWith('$'))
726
+ return null;
727
+ const segments = [];
728
+ let index = 1;
729
+ while (index < path.length) {
730
+ const marker = path[index];
731
+ if (marker === '.') {
732
+ index += 1;
733
+ if (path[index] === '[') {
734
+ const end = findBracketEnd(path, index);
735
+ if (end < 0)
736
+ return null;
737
+ segments.push(path.slice(index, end + 1));
738
+ index = end + 1;
739
+ continue;
740
+ }
741
+ const start = index;
742
+ while (index < path.length && !['.', '[', '@'].includes(path[index])) {
743
+ index += 1;
744
+ }
745
+ if (start === index)
746
+ return null;
747
+ segments.push(path.slice(start, index));
748
+ continue;
749
+ }
750
+ if (marker === '[') {
751
+ const end = findBracketEnd(path, index);
752
+ if (end < 0)
753
+ return null;
754
+ segments.push(path.slice(index, end + 1));
755
+ index = end + 1;
756
+ continue;
757
+ }
758
+ if (marker === '@') {
759
+ index += 1;
760
+ if (path[index] === '[') {
761
+ const end = findBracketEnd(path, index);
762
+ if (end < 0)
763
+ return null;
764
+ segments.push(`@${path.slice(index, end + 1)}`);
765
+ index = end + 1;
766
+ continue;
767
+ }
768
+ const start = index;
769
+ while (index < path.length && !['.', '[', '@'].includes(path[index])) {
770
+ index += 1;
771
+ }
772
+ if (start === index)
773
+ return null;
774
+ segments.push(`@${path.slice(start, index)}`);
775
+ continue;
776
+ }
777
+ return null;
778
+ }
779
+ return segments;
780
+ }
781
+ function findBracketEnd(path, start) {
782
+ let quote = null;
783
+ let escaped = false;
784
+ for (let index = start + 1; index < path.length; index++) {
785
+ const ch = path[index];
786
+ if (escaped) {
787
+ escaped = false;
788
+ continue;
789
+ }
790
+ if (quote) {
791
+ if (ch === '\\') {
792
+ escaped = true;
793
+ }
794
+ else if (ch === quote) {
795
+ quote = null;
796
+ }
797
+ continue;
798
+ }
799
+ if (ch === '"' || ch === "'") {
800
+ quote = ch;
801
+ continue;
802
+ }
803
+ if (ch === ']')
804
+ return index;
805
+ }
806
+ return -1;
807
+ }
808
+ function matchesSelectorPath(actualPath, selector) {
809
+ if (actualPath === selector)
810
+ return true;
811
+ const actualSegments = tokenizeCanonicalLikePath(actualPath);
812
+ const selectorSegments = tokenizeCanonicalLikePath(selector);
813
+ if (!actualSegments || !selectorSegments)
814
+ return false;
815
+ const matchFrom = (actualIndex, selectorIndex) => {
816
+ if (selectorIndex === selectorSegments.length) {
817
+ return actualIndex === actualSegments.length;
818
+ }
819
+ const selectorSegment = selectorSegments[selectorIndex];
820
+ if (selectorSegment === '**') {
821
+ if (selectorIndex === selectorSegments.length - 1)
822
+ return true;
823
+ for (let nextActual = actualIndex; nextActual <= actualSegments.length; nextActual++) {
824
+ if (matchFrom(nextActual, selectorIndex + 1))
825
+ return true;
826
+ }
827
+ return false;
828
+ }
829
+ if (actualIndex >= actualSegments.length)
830
+ return false;
831
+ if (selectorSegment === '*') {
832
+ return matchFrom(actualIndex + 1, selectorIndex + 1);
833
+ }
834
+ if (selectorSegment === '[*]') {
835
+ return /^\[\d+\]$/.test(actualSegments[actualIndex])
836
+ && matchFrom(actualIndex + 1, selectorIndex + 1);
837
+ }
838
+ return selectorSegment === actualSegments[actualIndex]
839
+ && matchFrom(actualIndex + 1, selectorIndex + 1);
840
+ };
841
+ return matchFrom(0, 0);
842
+ }
843
+ function collectAllowedAttributePaths(ruleIndex) {
844
+ const allowed = [];
845
+ function visit(basePath, constraints) {
846
+ if (basePath.includes('@')) {
847
+ allowed.push(basePath);
848
+ }
849
+ const attributes = constraints.attributes;
850
+ if (!attributes)
851
+ return;
852
+ for (const [key, childConstraints] of Object.entries(attributes)) {
853
+ visit(`${basePath}@${key}`, childConstraints);
854
+ }
855
+ }
856
+ for (const [path, rule] of ruleIndex) {
857
+ visit(path, rule.constraints);
858
+ }
859
+ return allowed;
860
+ }
861
+ function collectAttributeEntries(eventsByPath) {
862
+ const entries = [];
863
+ function visit(basePath, attributes) {
864
+ if (!attributes)
865
+ return;
866
+ for (const [key, entry] of attributes.entries()) {
867
+ const path = `${basePath}@${key}`;
868
+ entries.push({ path, span: entry.span });
869
+ visit(path, entry.attributes);
870
+ }
871
+ }
872
+ for (const [path, event] of eventsByPath) {
873
+ visit(path, event.attributes);
874
+ }
875
+ return entries;
876
+ }
877
+ function checkAttributePolicy(schema, ruleIndex, eventsByPath, ctx) {
878
+ const policy = schema.attribute_policy ?? 'inherit_world';
879
+ if (policy === 'inherit_world' && (schema.world ?? 'open') !== 'closed')
880
+ return;
881
+ if (policy !== 'inherit_world' && policy !== 'forbid')
882
+ return;
883
+ const attributeEntries = collectAttributeEntries(eventsByPath);
884
+ if (attributeEntries.length === 0)
885
+ return;
886
+ const allowedPaths = policy === 'inherit_world'
887
+ ? collectAllowedAttributePaths(ruleIndex)
888
+ : [];
889
+ for (const entry of attributeEntries) {
890
+ if (allowedPaths.some((allowedPath) => matchesAllowedPath(entry.path, allowedPath)))
891
+ continue;
892
+ emitError(ctx, createDiag(entry.path, entry.span, policy === 'forbid'
893
+ ? `Attribute '${entry.path}' is forbidden by schema attribute_policy`
894
+ : `Attribute '${entry.path}' is not allowed by closed-world schema`, ErrorCodes.UNEXPECTED_ATTRIBUTE_ENTRY));
895
+ }
896
+ }
407
897
  function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
408
898
  if (!datatypeRules)
409
899
  return;
@@ -434,22 +924,43 @@ function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
434
924
  continue;
435
925
  }
436
926
  if (constraints.min_value !== undefined || constraints.max_value !== undefined) {
437
- const normalized = normalizeIntegerLiteral(raw);
438
- if (!normalized) {
439
- emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': exact integer range requires integer literal form`, ErrorCodes.NUMERIC_FORM_VIOLATION));
927
+ const range = normalizeRangeLiteral(event.type, raw);
928
+ if (!range) {
929
+ emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': range constraints require numeric literal form`, ErrorCodes.NUMERIC_FORM_VIOLATION));
440
930
  continue;
441
931
  }
442
- const numeric = BigInt(normalized);
443
- if (constraints.min_value !== undefined && numeric < BigInt(constraints.min_value)) {
444
- emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
932
+ if (constraints.min_value !== undefined && isBelowRange(range, constraints.min_value)) {
933
+ emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${range.raw}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
445
934
  continue;
446
935
  }
447
- if (constraints.max_value !== undefined && numeric > BigInt(constraints.max_value)) {
448
- emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
936
+ if (constraints.max_value !== undefined && isAboveRange(range, constraints.max_value)) {
937
+ emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${range.raw}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
449
938
  }
450
939
  }
451
940
  }
452
941
  }
942
+ function mergeDatatypeRules(ruleIndex, datatypeRules, eventsByPath) {
943
+ if (!datatypeRules)
944
+ return ruleIndex;
945
+ const merged = new Map();
946
+ for (const [path, rule] of ruleIndex.entries()) {
947
+ merged.set(path, { ...rule, constraints: { ...rule.constraints } });
948
+ }
949
+ for (const [path, event] of eventsByPath.entries()) {
950
+ if (!event.datatype)
951
+ continue;
952
+ const datatypeRule = datatypeRules[datatypeBase(event.datatype).toLowerCase()];
953
+ if (!datatypeRule)
954
+ continue;
955
+ const existing = merged.get(path);
956
+ const constraints = existing?.constraints ?? {};
957
+ merged.set(path, {
958
+ path,
959
+ constraints: { ...datatypeRule, ...constraints },
960
+ });
961
+ }
962
+ return merged;
963
+ }
453
964
  function checkAttributeConstraints(ruleIndex, eventsByPath, datatypeRules, ctx) {
454
965
  for (const [path, rule] of ruleIndex) {
455
966
  if (!rule.constraints.attributes && rule.constraints.closed_attributes !== true)
@@ -493,9 +1004,10 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
493
1004
  emitError(ctx, createDiag(path, entry.span, `Container kind mismatch: expected ${effectiveConstraints.type_is}, got ${entry.type}`, ErrorCodes.WRONG_CONTAINER_KIND));
494
1005
  }
495
1006
  }
496
- if (effectiveConstraints.type !== undefined && !constraintTypeMatches(entry.type, effectiveConstraints.type, entry.raw)) {
1007
+ if (effectiveConstraints.type !== undefined && !constraintTypeMatches(entry.type, effectiveConstraints.type, entry.raw, effectiveConstraints)) {
497
1008
  emitError(ctx, createDiag(path, entry.span, `Type mismatch: expected ${effectiveConstraints.type}, got ${entry.type}`, ErrorCodes.TYPE_MISMATCH));
498
1009
  }
1010
+ checkLexicalLiteralConstraint(path, entry, effectiveConstraints, ctx);
499
1011
  if (effectiveConstraints.datatype !== undefined && entry.datatype !== effectiveConstraints.datatype) {
500
1012
  emitError(ctx, createDiag(path, entry.span, `Datatype mismatch: expected ${effectiveConstraints.datatype}, got ${entry.datatype ?? '<none>'}`, ErrorCodes.TYPE_MISMATCH));
501
1013
  }
@@ -511,9 +1023,9 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
511
1023
  emitError(ctx, createDiag(path, entry.span, `Reference kind mismatch at ${path}: expected ${expectedType}, got ${entry.type}`, ErrorCodes.REFERENCE_KIND_MISMATCH));
512
1024
  }
513
1025
  }
514
- if (entry.type === 'NumberLiteral') {
515
- const digitCount = countIntegerDigits(entry.raw);
516
- if (effectiveConstraints.sign === 'unsigned' && isNegative(entry.raw)) {
1026
+ if (hasDigitFormConstraints(effectiveConstraints) && isDigitFormLiteral(entry.type)) {
1027
+ const digitCount = countFormDigits(entry.type, entry.raw);
1028
+ if ((entry.type === 'NumberLiteral' || entry.type === 'RadixLiteral') && effectiveConstraints.sign === 'unsigned' && isFormNegative(entry.raw)) {
517
1029
  emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected unsigned, got negative`, ErrorCodes.NUMERIC_FORM_VIOLATION));
518
1030
  }
519
1031
  if (effectiveConstraints.min_digits !== undefined && digitCount < effectiveConstraints.min_digits) {
@@ -522,15 +1034,27 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
522
1034
  if (effectiveConstraints.max_digits !== undefined && digitCount > effectiveConstraints.max_digits) {
523
1035
  emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected max ${effectiveConstraints.max_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
524
1036
  }
1037
+ if (entry.type === 'RadixLiteral' && effectiveConstraints.radix !== undefined) {
1038
+ const declaredRadix = declaredRadixFromDatatype(entry.datatype);
1039
+ if ((declaredRadix === null && effectiveConstraints.allow_unspecified_radix !== true)
1040
+ || (declaredRadix !== null && declaredRadix !== effectiveConstraints.radix)) {
1041
+ emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected declared radix ${effectiveConstraints.radix}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
1042
+ return;
1043
+ }
1044
+ const invalidDigit = firstInvalidRadixDigit(entry.raw, effectiveConstraints.radix);
1045
+ if (invalidDigit !== null) {
1046
+ emitError(ctx, createDiag(path, entry.span, `Numeric form violation: radix literal digit '${invalidDigit}' is outside radix ${effectiveConstraints.radix}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
1047
+ }
1048
+ }
525
1049
  }
526
- if (entry.type === 'StringLiteral') {
1050
+ if (isStringType(entry.type)) {
527
1051
  if (effectiveConstraints.min_length !== undefined && entry.value.length < effectiveConstraints.min_length) {
528
1052
  emitError(ctx, createDiag(path, entry.span, `String length violation: expected min length ${effectiveConstraints.min_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
529
1053
  }
530
1054
  if (effectiveConstraints.max_length !== undefined && entry.value.length > effectiveConstraints.max_length) {
531
1055
  emitError(ctx, createDiag(path, entry.span, `String length violation: expected max length ${effectiveConstraints.max_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
532
1056
  }
533
- if (effectiveConstraints.pattern !== undefined && !(new RegExp(effectiveConstraints.pattern).test(entry.value))) {
1057
+ if (effectiveConstraints.pattern !== undefined && !patternMatches(effectiveConstraints.pattern, entry.value)) {
534
1058
  emitError(ctx, createDiag(path, entry.span, `Pattern mismatch: value does not match ${effectiveConstraints.pattern}`, ErrorCodes.PATTERN_MISMATCH));
535
1059
  }
536
1060
  }
@@ -554,7 +1078,13 @@ function mergeDatatypeRuleConstraints(constraints, datatype, datatypeRules) {
554
1078
  return constraints;
555
1079
  return { ...datatypeRule, ...constraints };
556
1080
  }
557
- function constraintTypeMatches(actualType, expectedType, raw) {
1081
+ function constraintTypeMatches(actualType, expectedType, raw, constraints) {
1082
+ if (constraints?.nullable === true && actualType === 'NullLiteral')
1083
+ return true;
1084
+ if (constraints?.allow_infinity === true && actualType === 'InfinityLiteral' && isNumericExpectedType(expectedType))
1085
+ return true;
1086
+ if (constraints?.allow_nan === true && actualType === 'NaNLiteral' && isNumericExpectedType(expectedType))
1087
+ return true;
558
1088
  if (actualType === expectedType)
559
1089
  return true;
560
1090
  if (actualType === 'NumberLiteral') {
@@ -566,6 +1096,49 @@ function constraintTypeMatches(actualType, expectedType, raw) {
566
1096
  const satisfies = TYPE_ALIASES[actualType];
567
1097
  return Boolean(satisfies?.includes(expectedType));
568
1098
  }
1099
+ function isNumericExpectedType(expectedType) {
1100
+ return expectedType === 'NumberLiteral' || expectedType === 'IntegerLiteral' || expectedType === 'FloatLiteral';
1101
+ }
1102
+ function checkLexicalLiteralConstraints(ruleIndex, events, ctx) {
1103
+ for (const [path, rule] of ruleIndex) {
1104
+ const event = events.get(path);
1105
+ if (!event)
1106
+ continue;
1107
+ checkLexicalLiteralConstraint(path, event, rule.constraints, ctx);
1108
+ }
1109
+ }
1110
+ function checkLexicalLiteralConstraint(path, event, constraints, ctx) {
1111
+ if (event.type === 'NullLiteral' && !nullValueMatches(event.value, constraints)) {
1112
+ emitError(ctx, createDiag(path, event.span, `Null value mismatch: expected ${formatExpectedNullValues(constraints)}, got ${event.value || '<none>'}`, ErrorCodes.NULL_VALUE_MISMATCH));
1113
+ }
1114
+ if (event.type === 'ToggleLiteral' && constraints.toggle_pair !== undefined && constraints.toggle_pair !== 'any') {
1115
+ const value = (event.raw || event.value).toLowerCase();
1116
+ const allowed = constraints.toggle_pair === 'yes_no'
1117
+ ? ['yes', 'no']
1118
+ : constraints.toggle_pair === 'on_off'
1119
+ ? ['on', 'off']
1120
+ : [];
1121
+ if (allowed.length > 0 && !allowed.includes(value)) {
1122
+ emitError(ctx, createDiag(path, event.span, `Toggle pair mismatch: expected ${constraints.toggle_pair}, got ${value || '<none>'}`, ErrorCodes.TOGGLE_PAIR_MISMATCH));
1123
+ }
1124
+ }
1125
+ }
1126
+ function nullValueMatches(value, constraints) {
1127
+ const expected = expectedNullValues(constraints);
1128
+ return expected.length === 0 || expected.includes(value);
1129
+ }
1130
+ function expectedNullValues(constraints) {
1131
+ const values = [];
1132
+ if (constraints.null_value !== undefined)
1133
+ values.push(constraints.null_value);
1134
+ if (constraints.null_values !== undefined)
1135
+ values.push(...constraints.null_values);
1136
+ return values;
1137
+ }
1138
+ function formatExpectedNullValues(constraints) {
1139
+ const values = expectedNullValues(constraints);
1140
+ return values.length > 0 ? values.join(' | ') : '<any>';
1141
+ }
569
1142
  function isReferenceType(type) {
570
1143
  return type === 'CloneReference' || type === 'PointerReference';
571
1144
  }
@@ -584,17 +1157,115 @@ function datatypeTypeMatches(actualType, expectedType, raw) {
584
1157
  return true;
585
1158
  return false;
586
1159
  }
587
- function normalizeIntegerLiteral(raw) {
588
- if (!/^[+-]?\d[\d_]*$/.test(raw))
1160
+ function normalizeRangeLiteral(type, raw) {
1161
+ const normalized = raw.replace(/_/g, '');
1162
+ if (type === 'FloatLiteral' || /[.eE]/.test(normalized)) {
1163
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized))
1164
+ return null;
1165
+ const value = Number(normalized);
1166
+ return Number.isFinite(value) ? { kind: 'float', raw: normalized, value } : null;
1167
+ }
1168
+ if (!/^[+-]?\d+$/.test(normalized))
589
1169
  return null;
590
- return raw.replace(/_/g, '');
1170
+ return { kind: 'integer', raw: normalized, value: BigInt(normalized) };
1171
+ }
1172
+ function isBelowRange(range, bound) {
1173
+ if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
1174
+ return range.value < BigInt(bound);
1175
+ }
1176
+ return rangeAsNumber(range) < Number(bound);
1177
+ }
1178
+ function isAboveRange(range, bound) {
1179
+ if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
1180
+ return range.value > BigInt(bound);
1181
+ }
1182
+ return rangeAsNumber(range) > Number(bound);
1183
+ }
1184
+ function rangeAsNumber(range) {
1185
+ return range.kind === 'integer' ? Number(range.value) : range.value;
591
1186
  }
592
1187
  function countIntegerDigits(raw) {
593
1188
  return raw.replace(/^[+-]/, '').replace(/_/g, '').split('.')[0]?.length ?? 0;
594
1189
  }
1190
+ function hasDigitFormConstraints(constraints) {
1191
+ return constraints.sign !== undefined || constraints.min_digits !== undefined || constraints.max_digits !== undefined || constraints.radix !== undefined;
1192
+ }
1193
+ function isDigitFormLiteral(type) {
1194
+ return type === 'NumberLiteral' || type === 'HexLiteral' || type === 'RadixLiteral';
1195
+ }
1196
+ function radixConstraintMatches(datatype, raw, constraints) {
1197
+ if (constraints.radix === undefined)
1198
+ return true;
1199
+ const declaredRadix = declaredRadixFromDatatype(datatype);
1200
+ if (declaredRadix === null && constraints.allow_unspecified_radix !== true)
1201
+ return false;
1202
+ if (declaredRadix !== null && declaredRadix !== constraints.radix)
1203
+ return false;
1204
+ return firstInvalidRadixDigit(raw, constraints.radix) === null;
1205
+ }
1206
+ function declaredRadixFromDatatype(datatype) {
1207
+ if (datatype === undefined)
1208
+ return null;
1209
+ const match = /^radix(?:\[(\d+)\]|(\d+))$/i.exec(datatype.trim());
1210
+ if (!match)
1211
+ return null;
1212
+ const value = Number(match[1] ?? match[2]);
1213
+ return Number.isInteger(value) && value >= 0 ? value : null;
1214
+ }
1215
+ function isStringType(type) {
1216
+ return type === 'StringLiteral'
1217
+ || type === 'TrimtickLiteral'
1218
+ || type === 'TrimtickStringLiteral'
1219
+ || type === 'SeparatorLiteral'
1220
+ || type === 'NullLiteral'
1221
+ || type === 'EncodingLiteral'
1222
+ || type === 'DateLiteral'
1223
+ || type === 'TimeLiteral'
1224
+ || type === 'DateTimeLiteral'
1225
+ || type === 'ZRUTDateTimeLiteral';
1226
+ }
1227
+ function countFormDigits(type, raw) {
1228
+ if (type === 'NumberLiteral')
1229
+ return countIntegerDigits(raw);
1230
+ const body = raw
1231
+ .replace(/^[#%^]/, '')
1232
+ .replace(/^[+-]/, '')
1233
+ .replace(/_/g, '');
1234
+ let count = 0;
1235
+ for (const char of body) {
1236
+ if ((char >= '0' && char <= '9') || ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char === '&' || char === '!')) {
1237
+ count++;
1238
+ }
1239
+ }
1240
+ return count;
1241
+ }
1242
+ function firstInvalidRadixDigit(raw, radix) {
1243
+ const body = raw.replace(/^%/, '').replace(/^[+-]/, '').replace(/_/g, '');
1244
+ for (const char of body) {
1245
+ const value = radixDigitValue(char);
1246
+ if (value !== null && value >= radix)
1247
+ return char;
1248
+ }
1249
+ return null;
1250
+ }
1251
+ function radixDigitValue(char) {
1252
+ if (char >= '0' && char <= '9')
1253
+ return char.charCodeAt(0) - 48;
1254
+ const lower = char.toLowerCase();
1255
+ if (lower >= 'a' && lower <= 'z')
1256
+ return lower.charCodeAt(0) - 87;
1257
+ if (char === '&')
1258
+ return 36;
1259
+ if (char === '!')
1260
+ return 37;
1261
+ return null;
1262
+ }
595
1263
  function isNegative(raw) {
596
1264
  return raw.startsWith('-');
597
1265
  }
1266
+ function isFormNegative(raw) {
1267
+ return /^[$#%^]?-/.test(raw) || raw.startsWith('-');
1268
+ }
598
1269
  function formatCanonicalPathLocal(path) {
599
1270
  if (!path || !Array.isArray(path.segments))
600
1271
  return '$';