@altopelago/aeos-core 0.9.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 +143 -0
- package/dist/bin/aeos-validator.d.ts +16 -0
- package/dist/bin/aeos-validator.d.ts.map +1 -0
- package/dist/bin/aeos-validator.js +77 -0
- package/dist/bin/aeos-validator.js.map +1 -0
- package/dist/diag/codes.d.ts +55 -0
- package/dist/diag/codes.d.ts.map +1 -0
- package/dist/diag/codes.js +69 -0
- package/dist/diag/codes.js.map +1 -0
- package/dist/diag/emit.d.ts +34 -0
- package/dist/diag/emit.d.ts.map +1 -0
- package/dist/diag/emit.js +45 -0
- package/dist/diag/emit.js.map +1 -0
- package/dist/diag/index.d.ts +6 -0
- package/dist/diag/index.d.ts.map +1 -0
- package/dist/diag/index.js +6 -0
- package/dist/diag/index.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/index.d.ts +10 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/numericForm.d.ts +29 -0
- package/dist/rules/numericForm.d.ts.map +1 -0
- package/dist/rules/numericForm.js +74 -0
- package/dist/rules/numericForm.js.map +1 -0
- package/dist/rules/presence.d.ts +20 -0
- package/dist/rules/presence.d.ts.map +1 -0
- package/dist/rules/presence.js +29 -0
- package/dist/rules/presence.js.map +1 -0
- package/dist/rules/referenceForm.d.ts +17 -0
- package/dist/rules/referenceForm.d.ts.map +1 -0
- package/dist/rules/referenceForm.js +78 -0
- package/dist/rules/referenceForm.js.map +1 -0
- package/dist/rules/schemaIndex.d.ts +34 -0
- package/dist/rules/schemaIndex.d.ts.map +1 -0
- package/dist/rules/schemaIndex.js +167 -0
- package/dist/rules/schemaIndex.js.map +1 -0
- package/dist/rules/stringForm.d.ts +48 -0
- package/dist/rules/stringForm.d.ts.map +1 -0
- package/dist/rules/stringForm.js +96 -0
- package/dist/rules/stringForm.js.map +1 -0
- package/dist/rules/typeCheck.d.ts +29 -0
- package/dist/rules/typeCheck.d.ts.map +1 -0
- package/dist/rules/typeCheck.js +99 -0
- package/dist/rules/typeCheck.js.map +1 -0
- package/dist/types/aes.d.ts +14 -0
- package/dist/types/aes.d.ts.map +1 -0
- package/dist/types/aes.js +8 -0
- package/dist/types/aes.js.map +1 -0
- package/dist/types/envelope.d.ts +47 -0
- package/dist/types/envelope.d.ts.map +1 -0
- package/dist/types/envelope.js +29 -0
- package/dist/types/envelope.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/schema.d.ts +81 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +41 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/spans.d.ts +28 -0
- package/dist/types/spans.d.ts.map +1 -0
- package/dist/types/spans.js +16 -0
- package/dist/types/spans.js.map +1 -0
- package/dist/util/digits.d.ts +34 -0
- package/dist/util/digits.d.ts.map +1 -0
- package/dist/util/digits.js +66 -0
- package/dist/util/digits.js.map +1 -0
- package/dist/util/index.d.ts +5 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +5 -0
- package/dist/util/index.js.map +1 -0
- package/dist/validate.d.ts +46 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +633 -0
- package/dist/validate.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @altopelago/aeos-core - AEOS™ Validate
|
|
3
|
+
*
|
|
4
|
+
* Main validation orchestrator for AEOS™ (Another Easy Object Schema).
|
|
5
|
+
*/
|
|
6
|
+
import type { AES } from './types/aes.js';
|
|
7
|
+
import type { SchemaV1 } from './types/schema.js';
|
|
8
|
+
import type { ResultEnvelope } from './types/envelope.js';
|
|
9
|
+
/**
|
|
10
|
+
* Validation options
|
|
11
|
+
*/
|
|
12
|
+
export interface ValidateOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Enable strict mode (reserved for future use).
|
|
15
|
+
*/
|
|
16
|
+
readonly strict?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Optional policy for separator literal payloads that end with a declared separator.
|
|
19
|
+
* - off (default): ignore trailing delimiter payload
|
|
20
|
+
* - warn: emit warning
|
|
21
|
+
* - error: emit error
|
|
22
|
+
*/
|
|
23
|
+
readonly trailingSeparatorDelimiterPolicy?: 'off' | 'warn' | 'error';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate an AES against a schema.
|
|
27
|
+
*
|
|
28
|
+
* This is the main entry point for AEOS validation.
|
|
29
|
+
*
|
|
30
|
+
* AEOS validates representations, not values. It answers:
|
|
31
|
+
* "Is this AES structurally and representationally valid?"
|
|
32
|
+
*
|
|
33
|
+
* AEOS MUST NOT:
|
|
34
|
+
* - Mutate the input AES or schema
|
|
35
|
+
* - Resolve references
|
|
36
|
+
* - Coerce values
|
|
37
|
+
* - Compare numeric magnitudes
|
|
38
|
+
* - Inject defaults
|
|
39
|
+
*
|
|
40
|
+
* @param aes - Assignment Event Stream (readonly)
|
|
41
|
+
* @param schema - AEOS Schema v1 (readonly)
|
|
42
|
+
* @param options - Validation options
|
|
43
|
+
* @returns ResultEnvelope (never contains AES)
|
|
44
|
+
*/
|
|
45
|
+
export declare function validate(aes: AES, schema: SchemaV1, options?: ValidateOptions): ResultEnvelope;
|
|
46
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAkD1D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,gCAAgC,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;CAExE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CACpB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,QAAQ,EAChB,OAAO,GAAE,eAAoB,GAC9B,cAAc,CAoRhB"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @altopelago/aeos-core - AEOS™ Validate
|
|
3
|
+
*
|
|
4
|
+
* Main validation orchestrator for AEOS™ (Another Easy Object Schema).
|
|
5
|
+
*/
|
|
6
|
+
import { createPassingEnvelope, createFailingEnvelope } from './types/envelope.js';
|
|
7
|
+
import { createDiag, createDiagContext, emitError, emitWarning } from './diag/emit.js';
|
|
8
|
+
import { ErrorCodes } from './diag/codes.js';
|
|
9
|
+
import { spanToTuple } from './types/spans.js';
|
|
10
|
+
import { buildRuleIndex } from './rules/schemaIndex.js';
|
|
11
|
+
import { checkPresence } from './rules/presence.js';
|
|
12
|
+
import { checkTypes } from './rules/typeCheck.js';
|
|
13
|
+
import { checkReferenceForms } from './rules/referenceForm.js';
|
|
14
|
+
import { checkNumericForm } from './rules/numericForm.js';
|
|
15
|
+
import { checkStringForm, checkPatterns } from './rules/stringForm.js';
|
|
16
|
+
const TYPE_ALIASES = {
|
|
17
|
+
NumberLiteral: ['NumberLiteral'],
|
|
18
|
+
StringLiteral: ['StringLiteral'],
|
|
19
|
+
BooleanLiteral: ['BooleanLiteral'],
|
|
20
|
+
NullLiteral: ['NullLiteral'],
|
|
21
|
+
ObjectNode: ['ObjectNode'],
|
|
22
|
+
ListNode: ['ListNode'],
|
|
23
|
+
ListLiteral: ['ListNode', 'ListLiteral'],
|
|
24
|
+
TupleLiteral: ['TupleLiteral'],
|
|
25
|
+
CloneReference: ['CloneReference'],
|
|
26
|
+
PointerReference: ['PointerReference'],
|
|
27
|
+
NodeLiteral: ['NodeLiteral'],
|
|
28
|
+
};
|
|
29
|
+
function formatQuotedMemberSegment(key) {
|
|
30
|
+
return `.[${JSON.stringify(String(key))}]`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate an AES against a schema.
|
|
34
|
+
*
|
|
35
|
+
* This is the main entry point for AEOS validation.
|
|
36
|
+
*
|
|
37
|
+
* AEOS validates representations, not values. It answers:
|
|
38
|
+
* "Is this AES structurally and representationally valid?"
|
|
39
|
+
*
|
|
40
|
+
* AEOS MUST NOT:
|
|
41
|
+
* - Mutate the input AES or schema
|
|
42
|
+
* - Resolve references
|
|
43
|
+
* - Coerce values
|
|
44
|
+
* - Compare numeric magnitudes
|
|
45
|
+
* - Inject defaults
|
|
46
|
+
*
|
|
47
|
+
* @param aes - Assignment Event Stream (readonly)
|
|
48
|
+
* @param schema - AEOS Schema v1 (readonly)
|
|
49
|
+
* @param options - Validation options
|
|
50
|
+
* @returns ResultEnvelope (never contains AES)
|
|
51
|
+
*/
|
|
52
|
+
export function validate(aes, schema, options = {}) {
|
|
53
|
+
const trailingSeparatorPolicy = options.trailingSeparatorDelimiterPolicy ?? 'off';
|
|
54
|
+
// Phase 0 guardrail: inputs are readonly, we never mutate
|
|
55
|
+
// TypeScript enforces this at compile time via readonly types
|
|
56
|
+
// TODO: Phase 7 - String form constraints
|
|
57
|
+
// Phase 8a: schema-side datatype label allowlist during rule indexing
|
|
58
|
+
// Phase 8b: datatype-wide semantic rules via schema.datatype_rules
|
|
59
|
+
// TODO: Phase 9 - Guarantees
|
|
60
|
+
// Phase 1: Envelope plumbing
|
|
61
|
+
const ctx = createDiagContext();
|
|
62
|
+
// Phase 3: (moved to run after Phase 2)
|
|
63
|
+
// Helpers: format canonical path (local, no runtime AEON deps)
|
|
64
|
+
function formatCanonicalPath(path) {
|
|
65
|
+
if (!path || !Array.isArray(path.segments))
|
|
66
|
+
return '$';
|
|
67
|
+
let result = '';
|
|
68
|
+
for (const segment of path.segments) {
|
|
69
|
+
switch (segment.type) {
|
|
70
|
+
case 'root':
|
|
71
|
+
result = '$';
|
|
72
|
+
break;
|
|
73
|
+
case 'member':
|
|
74
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment.key)) {
|
|
75
|
+
result += `.${segment.key}`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
result += formatQuotedMemberSegment(segment.key);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case 'index':
|
|
82
|
+
result += `[${String(segment.index)}]`;
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
function toTuple(span) {
|
|
91
|
+
if (!span)
|
|
92
|
+
return null;
|
|
93
|
+
if (Array.isArray(span) && span.length === 2 && typeof span[0] === 'number')
|
|
94
|
+
return span;
|
|
95
|
+
if (span.start && span.end && typeof span.start.offset === 'number')
|
|
96
|
+
return spanToTuple(span);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function decodeSeparatorChars(datatype) {
|
|
100
|
+
if (!datatype)
|
|
101
|
+
return [];
|
|
102
|
+
const match = datatype.match(/\[([^\]]*)\]$/);
|
|
103
|
+
if (!match)
|
|
104
|
+
return [];
|
|
105
|
+
const payload = match[1] ?? '';
|
|
106
|
+
if (payload.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
const separators = [];
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (i < payload.length) {
|
|
111
|
+
separators.push(payload[i]);
|
|
112
|
+
i += 1;
|
|
113
|
+
if (i < payload.length) {
|
|
114
|
+
if (payload[i] !== ',')
|
|
115
|
+
return [];
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return separators;
|
|
120
|
+
}
|
|
121
|
+
// Phase 2 — Baseline invariants
|
|
122
|
+
const seen = new Map();
|
|
123
|
+
const eventsByPath = new Map();
|
|
124
|
+
const containerArity = new Map();
|
|
125
|
+
function hydrateIndexedFallback(basePath, value, fallbackSpan) {
|
|
126
|
+
const isContainer = value?.type === 'TupleLiteral' || value?.type === 'ListLiteral' || value?.type === 'ListNode' || value?.type === 'NodeLiteral';
|
|
127
|
+
const elements = Array.isArray(value?.elements) ? value.elements : Array.isArray(value?.children) ? value.children : null;
|
|
128
|
+
if (!isContainer || !elements)
|
|
129
|
+
return;
|
|
130
|
+
for (let i = 0; i < elements.length; i++) {
|
|
131
|
+
const elementPath = `${basePath}[${i}]`;
|
|
132
|
+
if (eventsByPath.has(elementPath))
|
|
133
|
+
continue;
|
|
134
|
+
const element = elements[i];
|
|
135
|
+
const attributes = buildAttributeInfoMap(element?.attributes);
|
|
136
|
+
const info = {
|
|
137
|
+
type: typeof element?.type === 'string' ? element.type : 'Unknown',
|
|
138
|
+
raw: typeof element?.raw === 'string' ? element.raw : '',
|
|
139
|
+
value: typeof element?.value === 'string' ? element.value : '',
|
|
140
|
+
span: toTuple(element?.span) ?? fallbackSpan,
|
|
141
|
+
...(Array.isArray(element?.path) ? { referencePath: element.path } : {}),
|
|
142
|
+
...(attributes ? { attributes } : {}),
|
|
143
|
+
};
|
|
144
|
+
eventsByPath.set(elementPath, info);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function buildAttributeInfoMap(attributes) {
|
|
148
|
+
const sourceEntries = attributes instanceof Map
|
|
149
|
+
? Array.from(attributes.entries())
|
|
150
|
+
: attributes && typeof attributes === 'object'
|
|
151
|
+
? Object.entries(attributes)
|
|
152
|
+
: [];
|
|
153
|
+
if (sourceEntries.length === 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
const mapped = new Map();
|
|
156
|
+
for (const [key, entry] of sourceEntries) {
|
|
157
|
+
const valueNode = entry?.value;
|
|
158
|
+
const nestedAttributes = buildAttributeInfoMap(entry?.annotations);
|
|
159
|
+
const info = {
|
|
160
|
+
type: typeof valueNode?.type === 'string' ? valueNode.type : 'Unknown',
|
|
161
|
+
raw: typeof valueNode?.raw === 'string' ? valueNode.raw : '',
|
|
162
|
+
value: typeof valueNode?.value === 'string' ? valueNode.value : '',
|
|
163
|
+
...(typeof entry?.datatype === 'string' ? { datatype: entry.datatype } : {}),
|
|
164
|
+
span: toTuple(valueNode?.span),
|
|
165
|
+
...(nestedAttributes ? { attributes: nestedAttributes } : {}),
|
|
166
|
+
};
|
|
167
|
+
mapped.set(String(key), info);
|
|
168
|
+
}
|
|
169
|
+
return mapped;
|
|
170
|
+
}
|
|
171
|
+
for (let i = 0; i < aes.length; i++) {
|
|
172
|
+
const event = aes[i];
|
|
173
|
+
const pathStr = formatCanonicalPath(event.path);
|
|
174
|
+
if (Array.isArray(event.path?.segments)) {
|
|
175
|
+
for (const seg of event.path.segments) {
|
|
176
|
+
if (seg?.type === 'index') {
|
|
177
|
+
const idx = seg.index;
|
|
178
|
+
const validNumeric = typeof idx === 'number' && Number.isInteger(idx) && idx >= 0;
|
|
179
|
+
if (!validNumeric) {
|
|
180
|
+
emitError(ctx, createDiag(pathStr, toTuple(event.span), `Invalid index segment format at ${pathStr}`, ErrorCodes.INVALID_INDEX_FORMAT));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Uniqueness
|
|
186
|
+
if (seen.has(pathStr)) {
|
|
187
|
+
const spanTuple = toTuple(event.span);
|
|
188
|
+
const diag = createDiag(pathStr, spanTuple, `Duplicate binding: ${pathStr}`, ErrorCodes.DUPLICATE_BINDING);
|
|
189
|
+
emitError(ctx, diag);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
seen.set(pathStr, event.span);
|
|
193
|
+
// Collect event info for Phase 5-7 checks
|
|
194
|
+
if (event.value && typeof event.value.type === 'string') {
|
|
195
|
+
const attributes = buildAttributeInfoMap(event.annotations);
|
|
196
|
+
const info = {
|
|
197
|
+
type: event.value.type,
|
|
198
|
+
raw: typeof event.value.raw === 'string' ? event.value.raw : '',
|
|
199
|
+
value: typeof event.value.value === 'string' ? event.value.value : '',
|
|
200
|
+
...(typeof event.datatype === 'string' ? { datatype: event.datatype } : {}),
|
|
201
|
+
span: toTuple(event.span),
|
|
202
|
+
...(Array.isArray(event.value.path) ? { referencePath: event.value.path } : {}),
|
|
203
|
+
...(attributes ? { attributes } : {}),
|
|
204
|
+
};
|
|
205
|
+
eventsByPath.set(pathStr, info);
|
|
206
|
+
if ((event.value.type === 'TupleLiteral' || event.value.type === 'ListLiteral' || event.value.type === 'ListNode')
|
|
207
|
+
&& Array.isArray(event.value.elements)) {
|
|
208
|
+
containerArity.set(pathStr, event.value.elements.length);
|
|
209
|
+
hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
|
|
210
|
+
}
|
|
211
|
+
else if (event.value.type === 'NodeLiteral' && Array.isArray(event.value.children)) {
|
|
212
|
+
containerArity.set(pathStr, event.value.children.length);
|
|
213
|
+
hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Register index even for first occurrence
|
|
218
|
+
}
|
|
219
|
+
// Optional separator literal trailing-delimiter policy
|
|
220
|
+
if (trailingSeparatorPolicy !== 'off') {
|
|
221
|
+
for (const event of aes) {
|
|
222
|
+
if (event?.value?.type !== 'SeparatorLiteral')
|
|
223
|
+
continue;
|
|
224
|
+
const payload = typeof event.value.value === 'string' ? event.value.value : '';
|
|
225
|
+
if (payload.length === 0)
|
|
226
|
+
continue;
|
|
227
|
+
const separators = decodeSeparatorChars(typeof event.datatype === 'string' ? event.datatype : undefined);
|
|
228
|
+
if (separators.length === 0)
|
|
229
|
+
continue;
|
|
230
|
+
const lastChar = payload[payload.length - 1];
|
|
231
|
+
if (!separators.includes(lastChar))
|
|
232
|
+
continue;
|
|
233
|
+
const pathStr = formatCanonicalPath(event.path);
|
|
234
|
+
const diag = createDiag(pathStr, toTuple(event.span), `Separator literal payload ends with declared separator '${lastChar}'`, ErrorCodes.TRAILING_SEPARATOR_DELIMITER);
|
|
235
|
+
if (trailingSeparatorPolicy === 'warn')
|
|
236
|
+
emitWarning(ctx, diag);
|
|
237
|
+
else
|
|
238
|
+
emitError(ctx, diag);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Phase 3: Build rule index from schema (run after baseline invariants)
|
|
242
|
+
const ruleIndex = buildRuleIndex(schema, ctx);
|
|
243
|
+
// Phase 4: Presence checks (required fields)
|
|
244
|
+
const boundPaths = new Set(seen.keys());
|
|
245
|
+
checkPresence(ruleIndex, boundPaths, ctx);
|
|
246
|
+
checkWorldPolicy(schema, aes, boundPaths, ctx);
|
|
247
|
+
// 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)
|
|
255
|
+
continue;
|
|
256
|
+
const actualLength = containerArity.get(path);
|
|
257
|
+
if (actualLength === undefined)
|
|
258
|
+
continue;
|
|
259
|
+
if (typeof expectedLength === 'number' && actualLength !== expectedLength) {
|
|
260
|
+
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));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Phase 6: Numeric form constraints (sign, digit count)
|
|
265
|
+
checkNumericForm(ruleIndex, effectiveEventsByPath, ctx);
|
|
266
|
+
// 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);
|
|
270
|
+
checkDatatypeRules(schema.datatype_rules, effectiveEventsByPath, ctx);
|
|
271
|
+
if (ctx.errors.length > 0) {
|
|
272
|
+
return createFailingEnvelope(ctx.errors, ctx.warnings, {});
|
|
273
|
+
}
|
|
274
|
+
// Phase 9: Guarantees (advisory, non-semantic)
|
|
275
|
+
const guarantees = {};
|
|
276
|
+
// Helper: add a tag to a path's guarantee list
|
|
277
|
+
function addGuarantee(path, tag) {
|
|
278
|
+
const existing = guarantees[path];
|
|
279
|
+
const list = existing ? [...existing] : [];
|
|
280
|
+
if (!list.includes(tag))
|
|
281
|
+
list.push(tag);
|
|
282
|
+
guarantees[path] = list;
|
|
283
|
+
}
|
|
284
|
+
// Mark presence for all bound paths
|
|
285
|
+
for (const p of Array.from(boundPaths)) {
|
|
286
|
+
addGuarantee(p, 'present');
|
|
287
|
+
}
|
|
288
|
+
// Representation guarantees based on literal forms
|
|
289
|
+
const intRe = /^[+-]?\d+$/;
|
|
290
|
+
const floatRe = /^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?$/;
|
|
291
|
+
for (const [path, info] of eventsByPath.entries()) {
|
|
292
|
+
const t = info.type;
|
|
293
|
+
const raw = typeof info.raw === 'string' ? info.raw : '';
|
|
294
|
+
const val = typeof info.value === 'string' ? info.value : '';
|
|
295
|
+
if (t === 'NumberLiteral') {
|
|
296
|
+
if (intRe.test(raw))
|
|
297
|
+
addGuarantee(path, 'integer-representable');
|
|
298
|
+
if (floatRe.test(raw))
|
|
299
|
+
addGuarantee(path, 'float-representable');
|
|
300
|
+
}
|
|
301
|
+
else if (t === 'StringLiteral') {
|
|
302
|
+
if (intRe.test(val))
|
|
303
|
+
addGuarantee(path, 'integer-representable');
|
|
304
|
+
if (floatRe.test(val))
|
|
305
|
+
addGuarantee(path, 'float-representable');
|
|
306
|
+
if (val === 'true' || val === 'false')
|
|
307
|
+
addGuarantee(path, 'boolean-representable');
|
|
308
|
+
if (val.length > 0)
|
|
309
|
+
addGuarantee(path, 'non-empty-string');
|
|
310
|
+
}
|
|
311
|
+
else if (t === 'BooleanLiteral') {
|
|
312
|
+
addGuarantee(path, 'boolean-representable');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return createPassingEnvelope(guarantees, ctx.warnings);
|
|
316
|
+
}
|
|
317
|
+
function checkWorldPolicy(schema, aes, boundPaths, ctx) {
|
|
318
|
+
if ((schema.world ?? 'open') !== 'closed')
|
|
319
|
+
return;
|
|
320
|
+
const allowedPaths = schema.rules.map((rule) => rule.path);
|
|
321
|
+
for (const event of aes) {
|
|
322
|
+
const key = typeof event.key === 'string' ? event.key : '';
|
|
323
|
+
if (key.startsWith('aeon:'))
|
|
324
|
+
continue;
|
|
325
|
+
const path = formatCanonicalPathLocal(event.path);
|
|
326
|
+
if (!boundPaths.has(path))
|
|
327
|
+
continue;
|
|
328
|
+
if (allowedPaths.some((allowedPath) => matchesAllowedPath(path, allowedPath)))
|
|
329
|
+
continue;
|
|
330
|
+
emitError(ctx, createDiag(path, toTupleLocal(event.span), `Binding '${path}' is not allowed by closed-world schema`, ErrorCodes.UNEXPECTED_BINDING));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
|
|
334
|
+
const resolved = new Map(eventsByPath);
|
|
335
|
+
for (const [path, rule] of ruleIndex.entries()) {
|
|
336
|
+
if (rule.constraints.resolve_reference_form !== true)
|
|
337
|
+
continue;
|
|
338
|
+
const event = eventsByPath.get(path);
|
|
339
|
+
if (!event || !isReferenceType(event.type) || !event.referencePath)
|
|
340
|
+
continue;
|
|
341
|
+
const terminal = resolveTerminalReferenceEvent(event, eventsByPath, new Set());
|
|
342
|
+
if (!terminal) {
|
|
343
|
+
resolved.delete(path);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
resolved.set(path, {
|
|
347
|
+
...terminal,
|
|
348
|
+
span: event.span,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return resolved;
|
|
352
|
+
}
|
|
353
|
+
function resolveTerminalReferenceEvent(event, eventsByPath, activePaths) {
|
|
354
|
+
if (!isReferenceType(event.type) || !event.referencePath) {
|
|
355
|
+
return event;
|
|
356
|
+
}
|
|
357
|
+
const targetPath = formatReferenceLookupPath(event.referencePath);
|
|
358
|
+
if (activePaths.has(targetPath)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const target = eventsByPath.get(targetPath);
|
|
362
|
+
if (!target) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
activePaths.add(targetPath);
|
|
366
|
+
const resolved = isReferenceType(target.type)
|
|
367
|
+
? resolveTerminalReferenceEvent(target, eventsByPath, activePaths)
|
|
368
|
+
: target;
|
|
369
|
+
activePaths.delete(targetPath);
|
|
370
|
+
return resolved;
|
|
371
|
+
}
|
|
372
|
+
function formatReferenceLookupPath(segments) {
|
|
373
|
+
if (segments.length === 0)
|
|
374
|
+
return '$';
|
|
375
|
+
let out = '$';
|
|
376
|
+
for (const segment of segments) {
|
|
377
|
+
if (typeof segment === 'number') {
|
|
378
|
+
out += `[${segment}]`;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (typeof segment === 'string') {
|
|
382
|
+
out += /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment)
|
|
383
|
+
? `.${segment}`
|
|
384
|
+
: formatQuotedMemberSegment(segment);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
out += /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment.key)
|
|
388
|
+
? `@${segment.key}`
|
|
389
|
+
: `@[${JSON.stringify(segment.key)}]`;
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
function matchesAllowedPath(actualPath, allowedPath) {
|
|
394
|
+
if (actualPath === allowedPath)
|
|
395
|
+
return true;
|
|
396
|
+
// Closed-world schemas may allow list descendants via canonical wildcard paths
|
|
397
|
+
// such as `$.items[*]` or `$.items[*].x`.
|
|
398
|
+
if (!allowedPath.includes('[*]'))
|
|
399
|
+
return false;
|
|
400
|
+
const escaped = allowedPath
|
|
401
|
+
.split('[*]')
|
|
402
|
+
.map((part) => part.replace(/[|\\{}()[\]^$+?.]/g, '\\$&'))
|
|
403
|
+
.join('\\[\\d+\\]');
|
|
404
|
+
const pattern = `^${escaped}$`;
|
|
405
|
+
return new RegExp(pattern).test(actualPath);
|
|
406
|
+
}
|
|
407
|
+
function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
|
|
408
|
+
if (!datatypeRules)
|
|
409
|
+
return;
|
|
410
|
+
for (const [path, event] of eventsByPath.entries()) {
|
|
411
|
+
if (!event.datatype)
|
|
412
|
+
continue;
|
|
413
|
+
const constraints = datatypeRules[datatypeBase(event.datatype).toLowerCase()];
|
|
414
|
+
if (!constraints)
|
|
415
|
+
continue;
|
|
416
|
+
if (constraints.type && !datatypeTypeMatches(event.type, constraints.type, event.raw)) {
|
|
417
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule mismatch for ':${event.datatype}': expected ${constraints.type}, got ${event.type}`, ErrorCodes.TYPE_MISMATCH));
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (event.type !== 'NumberLiteral')
|
|
421
|
+
continue;
|
|
422
|
+
const raw = event.raw;
|
|
423
|
+
const digitCount = countIntegerDigits(raw);
|
|
424
|
+
if (constraints.sign === 'unsigned' && isNegative(raw)) {
|
|
425
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected unsigned numeric form`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (constraints.min_digits !== undefined && digitCount < constraints.min_digits) {
|
|
429
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected min ${constraints.min_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (constraints.max_digits !== undefined && digitCount > constraints.max_digits) {
|
|
433
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected max ${constraints.max_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
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));
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
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));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
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));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function checkAttributeConstraints(ruleIndex, eventsByPath, datatypeRules, ctx) {
|
|
454
|
+
for (const [path, rule] of ruleIndex) {
|
|
455
|
+
if (!rule.constraints.attributes && rule.constraints.closed_attributes !== true)
|
|
456
|
+
continue;
|
|
457
|
+
const event = eventsByPath.get(path);
|
|
458
|
+
if (!event)
|
|
459
|
+
continue;
|
|
460
|
+
validateAttributeMap(path, event.attributes, rule.constraints, datatypeRules, ctx);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function validateAttributeMap(basePath, attributes, constraints, datatypeRules, ctx) {
|
|
464
|
+
const requiredAttributes = constraints.attributes ?? {};
|
|
465
|
+
for (const [key, childConstraints] of Object.entries(requiredAttributes)) {
|
|
466
|
+
const childPath = `${basePath}@${key}`;
|
|
467
|
+
const entry = attributes?.get(key);
|
|
468
|
+
if (childConstraints.required === true && !entry) {
|
|
469
|
+
emitError(ctx, createDiag(childPath, null, `Missing required field: ${childPath}`, ErrorCodes.MISSING_REQUIRED_FIELD));
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (!entry)
|
|
473
|
+
continue;
|
|
474
|
+
validateAttributeEntry(childPath, entry, childConstraints, datatypeRules, ctx);
|
|
475
|
+
}
|
|
476
|
+
if (constraints.closed_attributes === true && attributes) {
|
|
477
|
+
const allowed = new Set(Object.keys(requiredAttributes));
|
|
478
|
+
for (const key of attributes.keys()) {
|
|
479
|
+
if (allowed.has(key))
|
|
480
|
+
continue;
|
|
481
|
+
const childPath = `${basePath}@${key}`;
|
|
482
|
+
emitError(ctx, createDiag(childPath, attributes.get(key)?.span ?? null, `Attribute '${childPath}' is not allowed by closed attribute constraints`, ErrorCodes.UNEXPECTED_ATTRIBUTE_ENTRY));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
|
|
487
|
+
const effectiveConstraints = mergeDatatypeRuleConstraints(constraints, entry.datatype, datatypeRules);
|
|
488
|
+
if (effectiveConstraints.type_is !== undefined) {
|
|
489
|
+
const containerOk = effectiveConstraints.type_is === 'list'
|
|
490
|
+
? (entry.type === 'ListLiteral' || entry.type === 'ListNode')
|
|
491
|
+
: entry.type === 'TupleLiteral';
|
|
492
|
+
if (!containerOk) {
|
|
493
|
+
emitError(ctx, createDiag(path, entry.span, `Container kind mismatch: expected ${effectiveConstraints.type_is}, got ${entry.type}`, ErrorCodes.WRONG_CONTAINER_KIND));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (effectiveConstraints.type !== undefined && !constraintTypeMatches(entry.type, effectiveConstraints.type, entry.raw)) {
|
|
497
|
+
emitError(ctx, createDiag(path, entry.span, `Type mismatch: expected ${effectiveConstraints.type}, got ${entry.type}`, ErrorCodes.TYPE_MISMATCH));
|
|
498
|
+
}
|
|
499
|
+
if (effectiveConstraints.datatype !== undefined && entry.datatype !== effectiveConstraints.datatype) {
|
|
500
|
+
emitError(ctx, createDiag(path, entry.span, `Datatype mismatch: expected ${effectiveConstraints.datatype}, got ${entry.datatype ?? '<none>'}`, ErrorCodes.TYPE_MISMATCH));
|
|
501
|
+
}
|
|
502
|
+
if (effectiveConstraints.reference === 'require' && !isReferenceType(entry.type)) {
|
|
503
|
+
emitError(ctx, createDiag(path, entry.span, `Reference required at ${path}, got ${entry.type}`, ErrorCodes.REFERENCE_REQUIRED));
|
|
504
|
+
}
|
|
505
|
+
else if (effectiveConstraints.reference === 'forbid' && isReferenceType(entry.type)) {
|
|
506
|
+
emitError(ctx, createDiag(path, entry.span, `Reference not allowed at ${path}, got ${entry.type}`, ErrorCodes.REFERENCE_FORBIDDEN));
|
|
507
|
+
}
|
|
508
|
+
if (effectiveConstraints.reference === 'require' && effectiveConstraints.reference_kind && effectiveConstraints.reference_kind !== 'either') {
|
|
509
|
+
const expectedType = effectiveConstraints.reference_kind === 'clone' ? 'CloneReference' : 'PointerReference';
|
|
510
|
+
if (entry.type !== expectedType) {
|
|
511
|
+
emitError(ctx, createDiag(path, entry.span, `Reference kind mismatch at ${path}: expected ${expectedType}, got ${entry.type}`, ErrorCodes.REFERENCE_KIND_MISMATCH));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (entry.type === 'NumberLiteral') {
|
|
515
|
+
const digitCount = countIntegerDigits(entry.raw);
|
|
516
|
+
if (effectiveConstraints.sign === 'unsigned' && isNegative(entry.raw)) {
|
|
517
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected unsigned, got negative`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
518
|
+
}
|
|
519
|
+
if (effectiveConstraints.min_digits !== undefined && digitCount < effectiveConstraints.min_digits) {
|
|
520
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected min ${effectiveConstraints.min_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
521
|
+
}
|
|
522
|
+
if (effectiveConstraints.max_digits !== undefined && digitCount > effectiveConstraints.max_digits) {
|
|
523
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected max ${effectiveConstraints.max_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (entry.type === 'StringLiteral') {
|
|
527
|
+
if (effectiveConstraints.min_length !== undefined && entry.value.length < effectiveConstraints.min_length) {
|
|
528
|
+
emitError(ctx, createDiag(path, entry.span, `String length violation: expected min length ${effectiveConstraints.min_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
|
|
529
|
+
}
|
|
530
|
+
if (effectiveConstraints.max_length !== undefined && entry.value.length > effectiveConstraints.max_length) {
|
|
531
|
+
emitError(ctx, createDiag(path, entry.span, `String length violation: expected max length ${effectiveConstraints.max_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
|
|
532
|
+
}
|
|
533
|
+
if (effectiveConstraints.pattern !== undefined && !(new RegExp(effectiveConstraints.pattern).test(entry.value))) {
|
|
534
|
+
emitError(ctx, createDiag(path, entry.span, `Pattern mismatch: value does not match ${effectiveConstraints.pattern}`, ErrorCodes.PATTERN_MISMATCH));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (effectiveConstraints.attributes || effectiveConstraints.closed_attributes === true) {
|
|
538
|
+
validateAttributeMap(path, entry.attributes, effectiveConstraints, datatypeRules, ctx);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function datatypeBase(datatype) {
|
|
542
|
+
const genericIdx = datatype.indexOf('<');
|
|
543
|
+
const separatorIdx = datatype.indexOf('[');
|
|
544
|
+
const endIdx = [genericIdx, separatorIdx]
|
|
545
|
+
.filter((idx) => idx >= 0)
|
|
546
|
+
.reduce((min, idx) => Math.min(min, idx), datatype.length);
|
|
547
|
+
return datatype.slice(0, endIdx);
|
|
548
|
+
}
|
|
549
|
+
function mergeDatatypeRuleConstraints(constraints, datatype, datatypeRules) {
|
|
550
|
+
if (!datatype || !datatypeRules)
|
|
551
|
+
return constraints;
|
|
552
|
+
const datatypeRule = datatypeRules[datatypeBase(datatype).toLowerCase()];
|
|
553
|
+
if (!datatypeRule)
|
|
554
|
+
return constraints;
|
|
555
|
+
return { ...datatypeRule, ...constraints };
|
|
556
|
+
}
|
|
557
|
+
function constraintTypeMatches(actualType, expectedType, raw) {
|
|
558
|
+
if (actualType === expectedType)
|
|
559
|
+
return true;
|
|
560
|
+
if (actualType === 'NumberLiteral') {
|
|
561
|
+
if (expectedType === 'IntegerLiteral')
|
|
562
|
+
return /^[+-]?\d[\d_]*$/.test(raw);
|
|
563
|
+
if (expectedType === 'FloatLiteral')
|
|
564
|
+
return /^[+-]?(?:\d[\d_]*\.\d[\d_]*|\d[\d_]*\.|\.\d[\d_]*|\d[\d_]*[eE][+-]?\d[\d_]*)$/.test(raw);
|
|
565
|
+
}
|
|
566
|
+
const satisfies = TYPE_ALIASES[actualType];
|
|
567
|
+
return Boolean(satisfies?.includes(expectedType));
|
|
568
|
+
}
|
|
569
|
+
function isReferenceType(type) {
|
|
570
|
+
return type === 'CloneReference' || type === 'PointerReference';
|
|
571
|
+
}
|
|
572
|
+
function datatypeTypeMatches(actualType, expectedType, raw) {
|
|
573
|
+
if (actualType === expectedType)
|
|
574
|
+
return true;
|
|
575
|
+
if (actualType === 'NumberLiteral') {
|
|
576
|
+
if (expectedType === 'IntegerLiteral') {
|
|
577
|
+
return /^[+-]?\d[\d_]*$/.test(raw);
|
|
578
|
+
}
|
|
579
|
+
if (expectedType === 'FloatLiteral') {
|
|
580
|
+
return /^[+-]?(?:\d[\d_]*\.\d[\d_]*|\d[\d_]*\.|\.\d[\d_]*|\d[\d_]*[eE][+-]?\d[\d_]*)$/.test(raw);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (actualType === 'NumberLiteral' && expectedType === 'NumberLiteral')
|
|
584
|
+
return true;
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
function normalizeIntegerLiteral(raw) {
|
|
588
|
+
if (!/^[+-]?\d[\d_]*$/.test(raw))
|
|
589
|
+
return null;
|
|
590
|
+
return raw.replace(/_/g, '');
|
|
591
|
+
}
|
|
592
|
+
function countIntegerDigits(raw) {
|
|
593
|
+
return raw.replace(/^[+-]/, '').replace(/_/g, '').split('.')[0]?.length ?? 0;
|
|
594
|
+
}
|
|
595
|
+
function isNegative(raw) {
|
|
596
|
+
return raw.startsWith('-');
|
|
597
|
+
}
|
|
598
|
+
function formatCanonicalPathLocal(path) {
|
|
599
|
+
if (!path || !Array.isArray(path.segments))
|
|
600
|
+
return '$';
|
|
601
|
+
let result = '';
|
|
602
|
+
for (const segment of path.segments) {
|
|
603
|
+
switch (segment.type) {
|
|
604
|
+
case 'root':
|
|
605
|
+
result = '$';
|
|
606
|
+
break;
|
|
607
|
+
case 'member':
|
|
608
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment.key)) {
|
|
609
|
+
result += `.${segment.key}`;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
result += formatQuotedMemberSegment(segment.key);
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
case 'index':
|
|
616
|
+
result += `[${String(segment.index)}]`;
|
|
617
|
+
break;
|
|
618
|
+
default:
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function toTupleLocal(span) {
|
|
625
|
+
if (!span)
|
|
626
|
+
return null;
|
|
627
|
+
if (Array.isArray(span) && span.length === 2 && typeof span[0] === 'number')
|
|
628
|
+
return span;
|
|
629
|
+
if (span.start && span.end && typeof span.start.offset === 'number')
|
|
630
|
+
return spanToTuple(span);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=validate.js.map
|