@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.
Files changed (82) hide show
  1. package/README.md +143 -0
  2. package/dist/bin/aeos-validator.d.ts +16 -0
  3. package/dist/bin/aeos-validator.d.ts.map +1 -0
  4. package/dist/bin/aeos-validator.js +77 -0
  5. package/dist/bin/aeos-validator.js.map +1 -0
  6. package/dist/diag/codes.d.ts +55 -0
  7. package/dist/diag/codes.d.ts.map +1 -0
  8. package/dist/diag/codes.js +69 -0
  9. package/dist/diag/codes.js.map +1 -0
  10. package/dist/diag/emit.d.ts +34 -0
  11. package/dist/diag/emit.d.ts.map +1 -0
  12. package/dist/diag/emit.js +45 -0
  13. package/dist/diag/emit.js.map +1 -0
  14. package/dist/diag/index.d.ts +6 -0
  15. package/dist/diag/index.d.ts.map +1 -0
  16. package/dist/diag/index.js +6 -0
  17. package/dist/diag/index.js.map +1 -0
  18. package/dist/index.d.ts +31 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +31 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/rules/index.d.ts +10 -0
  23. package/dist/rules/index.d.ts.map +1 -0
  24. package/dist/rules/index.js +10 -0
  25. package/dist/rules/index.js.map +1 -0
  26. package/dist/rules/numericForm.d.ts +29 -0
  27. package/dist/rules/numericForm.d.ts.map +1 -0
  28. package/dist/rules/numericForm.js +74 -0
  29. package/dist/rules/numericForm.js.map +1 -0
  30. package/dist/rules/presence.d.ts +20 -0
  31. package/dist/rules/presence.d.ts.map +1 -0
  32. package/dist/rules/presence.js +29 -0
  33. package/dist/rules/presence.js.map +1 -0
  34. package/dist/rules/referenceForm.d.ts +17 -0
  35. package/dist/rules/referenceForm.d.ts.map +1 -0
  36. package/dist/rules/referenceForm.js +78 -0
  37. package/dist/rules/referenceForm.js.map +1 -0
  38. package/dist/rules/schemaIndex.d.ts +34 -0
  39. package/dist/rules/schemaIndex.d.ts.map +1 -0
  40. package/dist/rules/schemaIndex.js +167 -0
  41. package/dist/rules/schemaIndex.js.map +1 -0
  42. package/dist/rules/stringForm.d.ts +48 -0
  43. package/dist/rules/stringForm.d.ts.map +1 -0
  44. package/dist/rules/stringForm.js +96 -0
  45. package/dist/rules/stringForm.js.map +1 -0
  46. package/dist/rules/typeCheck.d.ts +29 -0
  47. package/dist/rules/typeCheck.d.ts.map +1 -0
  48. package/dist/rules/typeCheck.js +99 -0
  49. package/dist/rules/typeCheck.js.map +1 -0
  50. package/dist/types/aes.d.ts +14 -0
  51. package/dist/types/aes.d.ts.map +1 -0
  52. package/dist/types/aes.js +8 -0
  53. package/dist/types/aes.js.map +1 -0
  54. package/dist/types/envelope.d.ts +47 -0
  55. package/dist/types/envelope.d.ts.map +1 -0
  56. package/dist/types/envelope.js +29 -0
  57. package/dist/types/envelope.js.map +1 -0
  58. package/dist/types/index.d.ts +10 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +10 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/types/schema.d.ts +81 -0
  63. package/dist/types/schema.d.ts.map +1 -0
  64. package/dist/types/schema.js +41 -0
  65. package/dist/types/schema.js.map +1 -0
  66. package/dist/types/spans.d.ts +28 -0
  67. package/dist/types/spans.d.ts.map +1 -0
  68. package/dist/types/spans.js +16 -0
  69. package/dist/types/spans.js.map +1 -0
  70. package/dist/util/digits.d.ts +34 -0
  71. package/dist/util/digits.d.ts.map +1 -0
  72. package/dist/util/digits.js +66 -0
  73. package/dist/util/digits.js.map +1 -0
  74. package/dist/util/index.d.ts +5 -0
  75. package/dist/util/index.d.ts.map +1 -0
  76. package/dist/util/index.js +5 -0
  77. package/dist/util/index.js.map +1 -0
  78. package/dist/validate.d.ts +46 -0
  79. package/dist/validate.d.ts.map +1 -0
  80. package/dist/validate.js +633 -0
  81. package/dist/validate.js.map +1 -0
  82. 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"}
@@ -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