@altopelago/aeos-core 0.12.0 → 0.13.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.
- package/README.md +21 -0
- package/dist/bin/aeos-validator.d.ts +1 -1
- package/dist/bin/aeos-validator.js +9 -5
- package/dist/bin/aeos-validator.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/rules/numericForm.d.ts.map +1 -1
- package/dist/rules/numericForm.js +9 -29
- package/dist/rules/numericForm.js.map +1 -1
- package/dist/rules/schemaIndex.d.ts.map +1 -1
- package/dist/rules/schemaIndex.js +19 -2
- package/dist/rules/schemaIndex.js.map +1 -1
- package/dist/schema-codec.d.ts.map +1 -1
- package/dist/schema-codec.js +79 -4
- package/dist/schema-codec.js.map +1 -1
- package/dist/telex.d.ts +14 -0
- package/dist/telex.d.ts.map +1 -0
- package/dist/telex.js +33 -0
- package/dist/telex.js.map +1 -0
- package/dist/types/aes.d.ts +9 -3
- package/dist/types/aes.d.ts.map +1 -1
- package/dist/types/schema.d.ts +2 -2
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/util/numericBounds.d.ts +7 -0
- package/dist/util/numericBounds.d.ts.map +1 -0
- package/dist/util/numericBounds.js +96 -0
- package/dist/util/numericBounds.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +244 -35
- package/dist/validate.js.map +1 -1
- package/package.json +6 -6
package/dist/validate.js
CHANGED
|
@@ -14,7 +14,9 @@ import { checkReferenceForms } from './rules/referenceForm.js';
|
|
|
14
14
|
import { checkNumericForm } from './rules/numericForm.js';
|
|
15
15
|
import { checkStringForm, checkPatterns, matchesPortablePattern } from './rules/stringForm.js';
|
|
16
16
|
import { datatypeBase, declaredRadixFromDatatype, parseClarifierValues } from './util/datatypes.js';
|
|
17
|
+
import { compareNumericValues } from './util/numericBounds.js';
|
|
17
18
|
import { parseAddress, resolveAddress, } from '@altopelago/sansa';
|
|
19
|
+
import { formatDatatypeDescriptor, } from '@altopelago/aeon-aes';
|
|
18
20
|
const TYPE_ALIASES = {
|
|
19
21
|
NumberLiteral: ['NumberLiteral'],
|
|
20
22
|
StringLiteral: ['StringLiteral'],
|
|
@@ -39,6 +41,7 @@ const TYPE_ALIASES = {
|
|
|
39
41
|
CloneReference: ['CloneReference'],
|
|
40
42
|
PointerReference: ['PointerReference'],
|
|
41
43
|
NodeLiteral: ['NodeLiteral'],
|
|
44
|
+
NodeHead: ['NodeHead'],
|
|
42
45
|
};
|
|
43
46
|
function formatQuotedMemberSegment(key) {
|
|
44
47
|
return `.[${JSON.stringify(String(key))}]`;
|
|
@@ -207,6 +210,8 @@ export function validate(aes, schema, options = {}) {
|
|
|
207
210
|
// Phase 3: (moved to run after Phase 2)
|
|
208
211
|
// Helpers: format canonical path (local, no runtime AEON deps)
|
|
209
212
|
function formatCanonicalPath(path) {
|
|
213
|
+
if (typeof path === 'string')
|
|
214
|
+
return path;
|
|
210
215
|
if (!path || !Array.isArray(path.segments))
|
|
211
216
|
return '$';
|
|
212
217
|
let result = '';
|
|
@@ -235,6 +240,10 @@ export function validate(aes, schema, options = {}) {
|
|
|
235
240
|
function toTuple(span) {
|
|
236
241
|
if (!span)
|
|
237
242
|
return null;
|
|
243
|
+
if (typeof span === 'string') {
|
|
244
|
+
const match = span.match(/^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/u);
|
|
245
|
+
return match === null ? null : [Number(match[1]), Number(match[2])];
|
|
246
|
+
}
|
|
238
247
|
if (Array.isArray(span) && span.length === 2 && typeof span[0] === 'number')
|
|
239
248
|
return span;
|
|
240
249
|
if (span.start && span.end && typeof span.start.offset === 'number')
|
|
@@ -264,6 +273,7 @@ export function validate(aes, schema, options = {}) {
|
|
|
264
273
|
const element = elements[i];
|
|
265
274
|
const attributes = buildAttributeInfoMap(element?.attributes);
|
|
266
275
|
const info = {
|
|
276
|
+
...(typeof element?.structuralId === 'string' ? { identity: element.structuralId } : {}),
|
|
267
277
|
type: typeof element?.type === 'string' ? element.type : 'Unknown',
|
|
268
278
|
raw: typeof element?.raw === 'string' ? element.raw : '',
|
|
269
279
|
value: typeof element?.value === 'string' ? element.value : '',
|
|
@@ -288,6 +298,7 @@ export function validate(aes, schema, options = {}) {
|
|
|
288
298
|
const valueNode = entry?.value;
|
|
289
299
|
const nestedAttributes = buildAttributeInfoMap(entry?.annotations);
|
|
290
300
|
const info = {
|
|
301
|
+
...(typeof entry?.structuralId === 'string' ? { identity: entry.structuralId } : {}),
|
|
291
302
|
type: typeof valueNode?.type === 'string' ? valueNode.type : 'Unknown',
|
|
292
303
|
raw: typeof valueNode?.raw === 'string' ? valueNode.raw : '',
|
|
293
304
|
value: typeof valueNode?.value === 'string' ? valueNode.value : '',
|
|
@@ -305,6 +316,7 @@ export function validate(aes, schema, options = {}) {
|
|
|
305
316
|
for (const [key, attribute] of attributes.entries()) {
|
|
306
317
|
const attributePath = appendAttributePath(basePath, key);
|
|
307
318
|
eventsByPath.set(attributePath, {
|
|
319
|
+
...(attribute.identity !== undefined ? { identity: attribute.identity } : {}),
|
|
308
320
|
type: attribute.type,
|
|
309
321
|
raw: attribute.raw,
|
|
310
322
|
value: attribute.value,
|
|
@@ -318,6 +330,7 @@ export function validate(aes, schema, options = {}) {
|
|
|
318
330
|
for (let i = 0; i < aes.length; i++) {
|
|
319
331
|
const event = aes[i];
|
|
320
332
|
const pathStr = formatCanonicalPath(event.path);
|
|
333
|
+
const portable = isPortableTelexRecord(event);
|
|
321
334
|
if (pathStr.length > resourcePolicy.max_path_length) {
|
|
322
335
|
emitResourceError(ctx, pathStr, `Path length ${pathStr.length} exceeds max_path_length ${resourcePolicy.max_path_length}`, toTuple(event.span));
|
|
323
336
|
}
|
|
@@ -341,9 +354,27 @@ export function validate(aes, schema, options = {}) {
|
|
|
341
354
|
else {
|
|
342
355
|
seen.set(pathStr, event.span);
|
|
343
356
|
// Collect event info for Phase 5-7 checks
|
|
344
|
-
if (
|
|
357
|
+
if (portable) {
|
|
358
|
+
const value = typeof event.value === 'string' ? event.value : '';
|
|
359
|
+
const datatype = portableDatatype(event);
|
|
360
|
+
const info = {
|
|
361
|
+
...(typeof event.identity === 'string' ? { identity: event.identity } : {}),
|
|
362
|
+
type: event.kind,
|
|
363
|
+
raw: value,
|
|
364
|
+
value,
|
|
365
|
+
...(datatype !== undefined ? { datatype } : {}),
|
|
366
|
+
span: toTuple(event.span),
|
|
367
|
+
...((event.kind === 'CloneReference' || event.kind === 'PointerReference')
|
|
368
|
+
&& typeof event.value === 'string'
|
|
369
|
+
? { referencePath: parsePortableReferencePath(event.value) }
|
|
370
|
+
: {}),
|
|
371
|
+
};
|
|
372
|
+
eventsByPath.set(pathStr, info);
|
|
373
|
+
}
|
|
374
|
+
else if (event.value && typeof event.value.type === 'string') {
|
|
345
375
|
const attributes = buildAttributeInfoMap(event.annotations);
|
|
346
376
|
const info = {
|
|
377
|
+
...(typeof event.structuralId === 'string' ? { identity: event.structuralId } : {}),
|
|
347
378
|
type: event.value.type,
|
|
348
379
|
raw: typeof event.value.raw === 'string' ? event.value.raw : '',
|
|
349
380
|
value: typeof event.value.value === 'string' ? event.value.value : '',
|
|
@@ -379,18 +410,26 @@ export function validate(aes, schema, options = {}) {
|
|
|
379
410
|
}
|
|
380
411
|
// Register index even for first occurrence
|
|
381
412
|
}
|
|
413
|
+
hydratePortableAttributes(eventsByPath, aes);
|
|
414
|
+
hydratePortableContainerArities(aes, containerArity, resourcePolicy, ctx, toTuple);
|
|
382
415
|
for (const [path, info] of eventsByPath) {
|
|
383
416
|
enforceStringLengthResourceBudget(info, path, resourcePolicy, ctx);
|
|
384
417
|
}
|
|
385
418
|
// Optional separator literal trailing-delimiter policy
|
|
386
419
|
if (trailingSeparatorPolicy !== 'off') {
|
|
387
420
|
for (const event of aes) {
|
|
388
|
-
|
|
421
|
+
const portable = isPortableTelexRecord(event);
|
|
422
|
+
const kind = portable ? event.kind : event?.value?.type;
|
|
423
|
+
if (kind !== 'SeparatorLiteral')
|
|
389
424
|
continue;
|
|
390
|
-
const payload =
|
|
425
|
+
const payload = portable
|
|
426
|
+
? (typeof event.value === 'string' ? event.value : '')
|
|
427
|
+
: (typeof event.value.value === 'string' ? event.value.value : '');
|
|
391
428
|
if (payload.length === 0)
|
|
392
429
|
continue;
|
|
393
|
-
const separators = decodeSeparatorChars(
|
|
430
|
+
const separators = decodeSeparatorChars(portable
|
|
431
|
+
? portableDatatype(event)
|
|
432
|
+
: (typeof event.datatype === 'string' ? event.datatype : undefined));
|
|
394
433
|
if (separators.length === 0)
|
|
395
434
|
continue;
|
|
396
435
|
const lastChar = payload[payload.length - 1];
|
|
@@ -405,7 +444,11 @@ export function validate(aes, schema, options = {}) {
|
|
|
405
444
|
}
|
|
406
445
|
}
|
|
407
446
|
// Phase 3: Build rule index from schema (run after baseline invariants)
|
|
447
|
+
const errorCountBeforeSchemaIndex = ctx.errors.length;
|
|
408
448
|
const ruleIndex = buildRuleIndex(schema, ctx);
|
|
449
|
+
if (ctx.errors.length > errorCountBeforeSchemaIndex) {
|
|
450
|
+
return createFailingEnvelope(ctx.errors, ctx.warnings, {});
|
|
451
|
+
}
|
|
409
452
|
const selectorExpansionBudget = { count: 0 };
|
|
410
453
|
const expandedRuleIndex = expandSelectorRules(ruleIndex, schema, eventsByPath, ctx, resourcePolicy, selectorExpansionBudget);
|
|
411
454
|
const effectiveRuleIndex = mergeDatatypeRules(expandedRuleIndex, schema.datatype_rules, eventsByPath);
|
|
@@ -528,7 +571,7 @@ function checkWorldPolicy(schema, aes, boundPaths, eventsByPath, ctx) {
|
|
|
528
571
|
}
|
|
529
572
|
for (const event of aes) {
|
|
530
573
|
const key = typeof event.key === 'string' ? event.key : '';
|
|
531
|
-
if (key.startsWith('aeon:'))
|
|
574
|
+
if (event.sourcePlane === 'header' || (event.sourcePlane === undefined && key.startsWith('aeon:')))
|
|
532
575
|
continue;
|
|
533
576
|
const path = formatCanonicalPathLocal(event.path);
|
|
534
577
|
if (!boundPaths.has(path))
|
|
@@ -664,6 +707,15 @@ function constraintBranchMatchesEvent(constraints, event) {
|
|
|
664
707
|
return false;
|
|
665
708
|
if (event.type === 'RadixLiteral' && constraints.radix !== undefined && !radixConstraintMatches(event.datatype, event.raw, constraints))
|
|
666
709
|
return false;
|
|
710
|
+
if (constraints.min_value !== undefined || constraints.max_value !== undefined) {
|
|
711
|
+
const normalized = normalizeRangeLiteral(event.type, event.raw);
|
|
712
|
+
if (normalized === null)
|
|
713
|
+
return false;
|
|
714
|
+
if (constraints.min_value !== undefined && compareNumericValues(normalized, constraints.min_value) === -1)
|
|
715
|
+
return false;
|
|
716
|
+
if (constraints.max_value !== undefined && compareNumericValues(normalized, constraints.max_value) === 1)
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
667
719
|
}
|
|
668
720
|
return true;
|
|
669
721
|
}
|
|
@@ -770,6 +822,8 @@ function insertAeosSansaResolvePath(root, path, info) {
|
|
|
770
822
|
}
|
|
771
823
|
}
|
|
772
824
|
current.info = info;
|
|
825
|
+
if (info.identity !== undefined)
|
|
826
|
+
current.identity = info.identity;
|
|
773
827
|
}
|
|
774
828
|
function getOrCreateChildBinding(parent, path, identity) {
|
|
775
829
|
const existing = parent.children.find((child) => identity.name !== undefined
|
|
@@ -865,17 +919,17 @@ function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
|
|
|
865
919
|
continue;
|
|
866
920
|
}
|
|
867
921
|
if (constraints.min_value !== undefined || constraints.max_value !== undefined) {
|
|
868
|
-
const
|
|
869
|
-
if (!
|
|
922
|
+
const normalized = normalizeRangeLiteral(event.type, raw);
|
|
923
|
+
if (!normalized) {
|
|
870
924
|
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': range constraints require numeric literal form`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
871
925
|
continue;
|
|
872
926
|
}
|
|
873
|
-
if (constraints.min_value !== undefined &&
|
|
874
|
-
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${
|
|
927
|
+
if (constraints.min_value !== undefined && compareNumericValues(normalized, constraints.min_value) === -1) {
|
|
928
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
875
929
|
continue;
|
|
876
930
|
}
|
|
877
|
-
if (constraints.max_value !== undefined &&
|
|
878
|
-
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${
|
|
931
|
+
if (constraints.max_value !== undefined && compareNumericValues(normalized, constraints.max_value) === 1) {
|
|
932
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
879
933
|
}
|
|
880
934
|
}
|
|
881
935
|
}
|
|
@@ -987,6 +1041,21 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
|
|
|
987
1041
|
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: radix literal digit '${invalidDigit}' is outside radix ${effectiveConstraints.radix}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
988
1042
|
}
|
|
989
1043
|
}
|
|
1044
|
+
if (effectiveConstraints.min_value !== undefined || effectiveConstraints.max_value !== undefined) {
|
|
1045
|
+
const normalized = normalizeRangeLiteral(entry.type, entry.raw);
|
|
1046
|
+
if (normalized === null) {
|
|
1047
|
+
emitError(ctx, createDiag(path, entry.span, 'Numeric form violation: range constraints require numeric literal form', ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (effectiveConstraints.min_value !== undefined
|
|
1051
|
+
&& compareNumericValues(normalized, effectiveConstraints.min_value) === -1) {
|
|
1052
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected value >= ${effectiveConstraints.min_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
1053
|
+
}
|
|
1054
|
+
if (effectiveConstraints.max_value !== undefined
|
|
1055
|
+
&& compareNumericValues(normalized, effectiveConstraints.max_value) === 1) {
|
|
1056
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected value <= ${effectiveConstraints.max_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
990
1059
|
}
|
|
991
1060
|
if (isStringType(entry.type)) {
|
|
992
1061
|
if (effectiveConstraints.min_length !== undefined && entry.value.length < effectiveConstraints.min_length) {
|
|
@@ -1092,36 +1161,20 @@ function datatypeTypeMatches(actualType, expectedType, raw) {
|
|
|
1092
1161
|
}
|
|
1093
1162
|
function normalizeRangeLiteral(type, raw) {
|
|
1094
1163
|
const normalized = raw.replace(/_/g, '');
|
|
1095
|
-
if (type
|
|
1096
|
-
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized))
|
|
1097
|
-
return null;
|
|
1098
|
-
const value = Number(normalized);
|
|
1099
|
-
return Number.isFinite(value) ? { kind: 'float', raw: normalized, value } : null;
|
|
1100
|
-
}
|
|
1101
|
-
if (!/^[+-]?\d+$/.test(normalized))
|
|
1164
|
+
if (type !== 'FloatLiteral' && type !== 'NumberLiteral' && type !== 'IntegerLiteral')
|
|
1102
1165
|
return null;
|
|
1103
|
-
return
|
|
1104
|
-
}
|
|
1105
|
-
function isBelowRange(range, bound) {
|
|
1106
|
-
if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
|
|
1107
|
-
return range.value < BigInt(bound);
|
|
1108
|
-
}
|
|
1109
|
-
return rangeAsNumber(range) < Number(bound);
|
|
1110
|
-
}
|
|
1111
|
-
function isAboveRange(range, bound) {
|
|
1112
|
-
if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
|
|
1113
|
-
return range.value > BigInt(bound);
|
|
1114
|
-
}
|
|
1115
|
-
return rangeAsNumber(range) > Number(bound);
|
|
1116
|
-
}
|
|
1117
|
-
function rangeAsNumber(range) {
|
|
1118
|
-
return range.kind === 'integer' ? Number(range.value) : range.value;
|
|
1166
|
+
return compareNumericValues(normalized, normalized) === null ? null : normalized;
|
|
1119
1167
|
}
|
|
1120
1168
|
function countIntegerDigits(raw) {
|
|
1121
1169
|
return raw.replace(/^[+-]/, '').replace(/_/g, '').split('.')[0]?.length ?? 0;
|
|
1122
1170
|
}
|
|
1123
1171
|
function hasDigitFormConstraints(constraints) {
|
|
1124
|
-
return constraints.sign !== undefined
|
|
1172
|
+
return constraints.sign !== undefined
|
|
1173
|
+
|| constraints.min_digits !== undefined
|
|
1174
|
+
|| constraints.max_digits !== undefined
|
|
1175
|
+
|| constraints.radix !== undefined
|
|
1176
|
+
|| constraints.min_value !== undefined
|
|
1177
|
+
|| constraints.max_value !== undefined;
|
|
1125
1178
|
}
|
|
1126
1179
|
function isDigitFormLiteral(type) {
|
|
1127
1180
|
return type === 'NumberLiteral' || type === 'HexLiteral' || type === 'RadixLiteral';
|
|
@@ -1191,7 +1244,159 @@ function isNegative(raw) {
|
|
|
1191
1244
|
function isFormNegative(raw) {
|
|
1192
1245
|
return /^[$#%^]?-/.test(raw) || raw.startsWith('-');
|
|
1193
1246
|
}
|
|
1247
|
+
function isPortableTelexRecord(value) {
|
|
1248
|
+
if (value === null || typeof value !== 'object')
|
|
1249
|
+
return false;
|
|
1250
|
+
const record = value;
|
|
1251
|
+
return typeof record.path === 'string' && typeof record.kind === 'string' && record.header === undefined;
|
|
1252
|
+
}
|
|
1253
|
+
function portableDatatype(record) {
|
|
1254
|
+
if (typeof record.datatype !== 'string')
|
|
1255
|
+
return undefined;
|
|
1256
|
+
if (!Array.isArray(record.generics) || !Array.isArray(record.clarifiers))
|
|
1257
|
+
return record.datatype;
|
|
1258
|
+
return formatDatatypeDescriptor({
|
|
1259
|
+
datatype: record.datatype,
|
|
1260
|
+
generics: record.generics,
|
|
1261
|
+
clarifiers: record.clarifiers,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function parsePortablePath(path) {
|
|
1265
|
+
if (!path.startsWith('$'))
|
|
1266
|
+
throw new TypeError(`Expected absolute portable AES path: ${path}`);
|
|
1267
|
+
const segments = [];
|
|
1268
|
+
const prefixes = [];
|
|
1269
|
+
let cursor = 1;
|
|
1270
|
+
while (cursor < path.length) {
|
|
1271
|
+
const start = cursor;
|
|
1272
|
+
let type;
|
|
1273
|
+
if (path.startsWith('.@.', cursor)) {
|
|
1274
|
+
type = 'attribute';
|
|
1275
|
+
cursor += 3;
|
|
1276
|
+
}
|
|
1277
|
+
else if (path[cursor] === '.') {
|
|
1278
|
+
type = 'member';
|
|
1279
|
+
cursor += 1;
|
|
1280
|
+
}
|
|
1281
|
+
else if (path[cursor] === '[') {
|
|
1282
|
+
const match = path.slice(cursor).match(/^\[(0|[1-9][0-9]*)\]/u);
|
|
1283
|
+
if (match === null)
|
|
1284
|
+
throw new TypeError(`Invalid portable AES index: ${path}`);
|
|
1285
|
+
cursor += match[0].length;
|
|
1286
|
+
segments.push({ type: 'index', index: Number(match[1]) });
|
|
1287
|
+
prefixes.push(path.slice(0, cursor));
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
throw new TypeError(`Invalid portable AES path: ${path}`);
|
|
1292
|
+
}
|
|
1293
|
+
let key;
|
|
1294
|
+
if (path[cursor] === '[' && path[cursor + 1] === '"') {
|
|
1295
|
+
let end = cursor + 2;
|
|
1296
|
+
let escaped = false;
|
|
1297
|
+
for (; end < path.length; end += 1) {
|
|
1298
|
+
const character = path[end];
|
|
1299
|
+
if (escaped)
|
|
1300
|
+
escaped = false;
|
|
1301
|
+
else if (character === '\\')
|
|
1302
|
+
escaped = true;
|
|
1303
|
+
else if (character === '"')
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
if (path[end] !== '"' || path[end + 1] !== ']')
|
|
1307
|
+
throw new TypeError(`Invalid quoted member: ${path}`);
|
|
1308
|
+
key = JSON.parse(path.slice(cursor + 1, end + 1));
|
|
1309
|
+
cursor = end + 2;
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
const match = path.slice(cursor).match(/^[A-Za-z_][A-Za-z0-9_]*/u);
|
|
1313
|
+
if (match === null)
|
|
1314
|
+
throw new TypeError(`Invalid portable AES member: ${path}`);
|
|
1315
|
+
key = match[0];
|
|
1316
|
+
cursor += key.length;
|
|
1317
|
+
}
|
|
1318
|
+
segments.push({ type, key });
|
|
1319
|
+
prefixes.push(path.slice(0, cursor));
|
|
1320
|
+
if (cursor === start)
|
|
1321
|
+
throw new TypeError(`Invalid portable AES path: ${path}`);
|
|
1322
|
+
}
|
|
1323
|
+
return { segments, prefixes };
|
|
1324
|
+
}
|
|
1325
|
+
function parsePortableReferencePath(path) {
|
|
1326
|
+
return parsePortablePath(path).segments.map((segment) => {
|
|
1327
|
+
if (segment.type === 'index')
|
|
1328
|
+
return segment.index;
|
|
1329
|
+
if (segment.type === 'attribute')
|
|
1330
|
+
return { type: 'attr', key: segment.key };
|
|
1331
|
+
return segment.key;
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
function hydratePortableAttributes(eventsByPath, aes) {
|
|
1335
|
+
const attributes = aes
|
|
1336
|
+
.filter(isPortableTelexRecord)
|
|
1337
|
+
.map((record) => {
|
|
1338
|
+
try {
|
|
1339
|
+
return { record, details: parsePortablePath(record.path) };
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
.filter((entry) => entry !== null)
|
|
1346
|
+
.filter(({ details }) => details.segments.at(-1)?.type === 'attribute')
|
|
1347
|
+
.sort((a, b) => a.details.segments.length - b.details.segments.length);
|
|
1348
|
+
for (const { record, details } of attributes) {
|
|
1349
|
+
const final = details.segments.at(-1);
|
|
1350
|
+
if (final?.type !== 'attribute')
|
|
1351
|
+
continue;
|
|
1352
|
+
const ownerPath = details.prefixes.at(-2) ?? '$';
|
|
1353
|
+
const owner = eventsByPath.get(ownerPath);
|
|
1354
|
+
const attribute = eventsByPath.get(record.path);
|
|
1355
|
+
if (owner === undefined || attribute === undefined)
|
|
1356
|
+
continue;
|
|
1357
|
+
const mapped = new Map(owner.attributes ?? []);
|
|
1358
|
+
mapped.set(final.key, attribute);
|
|
1359
|
+
owner.attributes = mapped;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function hydratePortableContainerArities(aes, containerArity, resourcePolicy, ctx, toTuple) {
|
|
1363
|
+
const records = aes.filter(isPortableTelexRecord);
|
|
1364
|
+
if (records.length === 0)
|
|
1365
|
+
return;
|
|
1366
|
+
const details = new Map();
|
|
1367
|
+
for (const record of records) {
|
|
1368
|
+
try {
|
|
1369
|
+
details.set(record.path, parsePortablePath(record.path));
|
|
1370
|
+
}
|
|
1371
|
+
catch {
|
|
1372
|
+
// The portable AES validation layer reports malformed paths.
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
for (const record of records) {
|
|
1376
|
+
if (!['ObjectNode', 'ListNode', 'TupleLiteral', 'NodeLiteral', 'NodeHead'].includes(record.kind))
|
|
1377
|
+
continue;
|
|
1378
|
+
const childOwner = record.kind === 'NodeLiteral' ? `${record.path}[0]` : record.path;
|
|
1379
|
+
let count = 0;
|
|
1380
|
+
for (const candidate of records) {
|
|
1381
|
+
const candidateDetails = details.get(candidate.path);
|
|
1382
|
+
if (candidateDetails === undefined)
|
|
1383
|
+
continue;
|
|
1384
|
+
const parent = candidateDetails.prefixes.at(-2) ?? '$';
|
|
1385
|
+
const last = candidateDetails.segments.at(-1);
|
|
1386
|
+
if (parent !== childOwner || last === undefined)
|
|
1387
|
+
continue;
|
|
1388
|
+
if (record.kind === 'ObjectNode' ? last.type === 'member' : last.type === 'index')
|
|
1389
|
+
count += 1;
|
|
1390
|
+
}
|
|
1391
|
+
containerArity.set(record.path, count);
|
|
1392
|
+
if (count > resourcePolicy.max_container_children_default) {
|
|
1393
|
+
emitResourceError(ctx, record.path, `Container child count ${count} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(record.span));
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1194
1397
|
function formatCanonicalPathLocal(path) {
|
|
1398
|
+
if (typeof path === 'string')
|
|
1399
|
+
return path;
|
|
1195
1400
|
if (!path || !Array.isArray(path.segments))
|
|
1196
1401
|
return '$';
|
|
1197
1402
|
let result = '';
|
|
@@ -1220,6 +1425,10 @@ function formatCanonicalPathLocal(path) {
|
|
|
1220
1425
|
function toTupleLocal(span) {
|
|
1221
1426
|
if (!span)
|
|
1222
1427
|
return null;
|
|
1428
|
+
if (typeof span === 'string') {
|
|
1429
|
+
const match = span.match(/^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/u);
|
|
1430
|
+
return match === null ? null : [Number(match[1]), Number(match[2])];
|
|
1431
|
+
}
|
|
1223
1432
|
if (Array.isArray(span) && span.length === 2 && typeof span[0] === 'number')
|
|
1224
1433
|
return span;
|
|
1225
1434
|
if (span.start && span.end && typeof span.start.offset === 'number')
|