@arkadia/ai-data-format 0.1.4

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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/config.d.ts +75 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +28 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/core/Decoder.d.ts +86 -0
  8. package/dist/core/Decoder.d.ts.map +1 -0
  9. package/dist/core/Decoder.js +951 -0
  10. package/dist/core/Decoder.js.map +1 -0
  11. package/dist/core/Encoder.d.ts +26 -0
  12. package/dist/core/Encoder.d.ts.map +1 -0
  13. package/dist/core/Encoder.js +368 -0
  14. package/dist/core/Encoder.js.map +1 -0
  15. package/dist/core/Parser.d.ts +6 -0
  16. package/dist/core/Parser.d.ts.map +1 -0
  17. package/dist/core/Parser.js +132 -0
  18. package/dist/core/Parser.js.map +1 -0
  19. package/dist/index.d.ts +22 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +46 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/models/Meta.d.ts +48 -0
  24. package/dist/models/Meta.d.ts.map +1 -0
  25. package/dist/models/Meta.js +113 -0
  26. package/dist/models/Meta.js.map +1 -0
  27. package/dist/models/Node.d.ts +42 -0
  28. package/dist/models/Node.d.ts.map +1 -0
  29. package/dist/models/Node.js +179 -0
  30. package/dist/models/Node.js.map +1 -0
  31. package/dist/models/Schema.d.ts +55 -0
  32. package/dist/models/Schema.d.ts.map +1 -0
  33. package/dist/models/Schema.js +175 -0
  34. package/dist/models/Schema.js.map +1 -0
  35. package/package.json +32 -0
  36. package/scripts/verify-build.js +202 -0
  37. package/src/config.ts +102 -0
  38. package/src/core/Decoder.ts +1057 -0
  39. package/src/core/Encoder.ts +443 -0
  40. package/src/core/Parser.ts +150 -0
  41. package/src/index.ts +46 -0
  42. package/src/models/Meta.ts +135 -0
  43. package/src/models/Node.ts +212 -0
  44. package/src/models/Schema.ts +222 -0
  45. package/tests/00.meta.test.ts +31 -0
  46. package/tests/00.node.test.ts +54 -0
  47. package/tests/00.primitive.test.ts +108 -0
  48. package/tests/00.schema.test.ts +41 -0
  49. package/tests/01.schema.test.ts +70 -0
  50. package/tests/02.data.test.ts +89 -0
  51. package/tests/03.errors.test.ts +71 -0
  52. package/tests/04.list.test.ts +225 -0
  53. package/tests/05.record.test.ts +82 -0
  54. package/tests/06.meta.test.ts +506 -0
  55. package/tests/utils.ts +69 -0
  56. package/tsconfig.json +46 -0
  57. package/vitest.config.ts +9 -0
@@ -0,0 +1,506 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { decode, encode, Node, Schema, SchemaKind } from '../src/index';
3
+ import { assertRoundtrip } from './utils';
4
+
5
+ describe('AI Data Metadata (Meta)', () => {
6
+
7
+ // ==================================================================================
8
+ // 2. SCHEMA DEFINITION & TYPING
9
+ // ==================================================================================
10
+
11
+ it('should handle comments', () => {
12
+ /** Validates that comments /.../ are ignored or handled. */
13
+ const text = '@User<id:int /*primary key*/, name:string> @User(5, "Bob")';
14
+ // Note: int normalizes to number in TS
15
+ const expected = '@User</*primary key*/ id:number,name:string>(5,"Bob")';
16
+
17
+ const res = decode(text, { debug: false });
18
+ expect(res.errors).toHaveLength(0);
19
+
20
+ // If the parser attaches schema comments to fields correctly, good.
21
+ // Here we just ensure data parsing works despite comments.
22
+ expect(res.node.fields["id"].value).toBe(5);
23
+
24
+ assertRoundtrip(text, expected, false);
25
+ });
26
+
27
+ it('should handle meta header', () => {
28
+ const aidText = `
29
+ $a0=5
30
+ <
31
+ /* c1 */
32
+ / $a1 /* c0 *//
33
+ /* c2 */ $a2=2 /* c3 */ $a3=3 a:number
34
+ >
35
+ ($a6 /*a*/ 3)
36
+ `;
37
+
38
+ const expected = "<//*c0*/ $a0=5 $a1=true/ /*c1*/ /*c2*/ /*c3*/ $a2=2 $a3=3 a:number>(/*a*/ $a6=true 3)";
39
+ assertRoundtrip(aidText, expected, false);
40
+ });
41
+
42
+ it('should handle mixed meta', () => {
43
+ const aidText = `
44
+ $attr=5
45
+ <
46
+ /* comm2 */
47
+ / $schema1 /
48
+ /* comm1 */
49
+ [a:int]
50
+ >
51
+ $attr=3
52
+ [
53
+ / /*meta for list*/ $attr=4 /
54
+ /*item1*/ $attr5 (3 $attr6),
55
+ /*item2*/ {a:5},
56
+ ]
57
+ `;
58
+
59
+ // Note: int -> number
60
+ const expected = "<[//*comm2*/ /*comm1*/ $attr=5 $schema1=true/ a:number]>[//*meta for list*/ $attr=4/ (//*item1*/ $attr5=true/ $attr6=true 3),(//*item2*// 5)]";
61
+ assertRoundtrip(aidText, expected, false);
62
+ });
63
+
64
+ // ==============================================================================
65
+ // 1. SCHEMA DEFINITION META (Types defined in < ... >)
66
+ // ==============================================================================
67
+
68
+ it('should handle list schema with meta', () => {
69
+ /**
70
+ * Verifies that an empty schema definition < ... > correctly parses:
71
+ * 1. Comments (including nested ones).
72
+ * 2. Attributes ($key=val).
73
+ * 3. Tags (#tag).
74
+ */
75
+ const aidText = `
76
+ /* 0 */
77
+ <
78
+ /* commentm0 */ /* com1 /*com1.2*/ */
79
+ / $listAttr="GlobalList" $b=4 #tag /
80
+ /* comment4 */
81
+ id:number
82
+ >
83
+ /* a */
84
+ #tag1 $val=3
85
+ [
86
+ 1,
87
+ 2,
88
+ 3
89
+ ]
90
+ /* b */
91
+ `;
92
+
93
+ const result = decode(aidText, { debug: false });
94
+ const node = result.node;
95
+ const errors = result.errors;
96
+
97
+ // 1. Assert no syntax errors
98
+ expect(errors).toHaveLength(0);
99
+
100
+ // 2. Check Schema Basics
101
+ const schema = node.schema;
102
+ expect(schema).not.toBeNull();
103
+ expect(schema.kind).toBe(SchemaKind.LIST); // Default kind for <...>
104
+
105
+ // 3. Verify Attributes ($key=val)
106
+ // Parser casts to number if possible
107
+ expect(schema.attr["listAttr"]).toBe("GlobalList");
108
+ expect(schema.attr["b"]).toBe(4);
109
+
110
+ // 4. Verify Tags (#tag)
111
+ expect(schema.tags).toContain("tag");
112
+ expect(schema.tags).toHaveLength(1);
113
+
114
+ // 5. Verify Comments
115
+ // We expect multiple comments to be collected
116
+ expect(schema.comments.length).toBeGreaterThan(0);
117
+ expect(schema.comments.some(c => c.includes("0"))).toBe(true);
118
+
119
+ const expected = '<[//*0*/ $listAttr="GlobalList" $b=4 #tag/ number]>[//*a*/ /*b*/ $val=3 #tag1/ 1,2,3]';
120
+ assertRoundtrip(aidText, expected, false);
121
+ });
122
+
123
+ it('should encode manual schema with meta', () => {
124
+ /**
125
+ * Verifies that a Schema with meta/comments is encoded correctly.
126
+ * Expected format: < / $attr=val #tag / any >
127
+ */
128
+
129
+ // 1. Prepare Schema manually
130
+ const schema = new Schema(SchemaKind.RECORD);
131
+ schema.comments = ["comment1", "comment2"];
132
+ schema.attr = { "key": "value", "count": 10 };
133
+ schema.tags = ["myTag"];
134
+
135
+ // Create a node using this schema
136
+ const node = new Node(schema, { value: null });
137
+ const expected = '<//*comment1*/ /*comment2*/ $key="value" $count=10 #myTag/ any>(null)';
138
+
139
+ assertRoundtrip(node, expected, false);
140
+ });
141
+
142
+ it('should round trip schema encode decode', () => {
143
+ /**
144
+ * Verifies that a Schema with meta/comments can be encoded to text
145
+ * and then decoded back, preserving all metadata (Round-Trip).
146
+ */
147
+
148
+ // 1. Prepare Schema manually
149
+ const originalSchema = new Schema(SchemaKind.RECORD);
150
+ originalSchema.comments = ["comment1", "comment2"];
151
+ originalSchema.attr = { "key": "value", "count": 10, "isActive": true };
152
+ originalSchema.tags = ["myTag", "urgent"];
153
+
154
+ // Create a node using this schema
155
+ const originalNode = new Node(originalSchema, { value: null });
156
+
157
+ // 2. Encode to String
158
+ // Important: We must enable include_comments to verify them after decoding
159
+ const encodedText = encode(originalNode, {
160
+ includeComments: true,
161
+ compact: true, // Test compact mode (one line)
162
+ colorize: false
163
+ });
164
+
165
+ // 3. Decode back to Node
166
+ const result = decode(encodedText, { debug: false });
167
+ const decodedNode = result.node;
168
+ const errors = result.errors;
169
+
170
+ // 4. Verify No Errors
171
+ expect(errors).toHaveLength(0);
172
+ expect(decodedNode).not.toBeNull();
173
+
174
+ // 5. Verify Schema Integrity
175
+ const decodedSchema = decodedNode.schema;
176
+ expect(decodedSchema).not.toBeNull();
177
+ expect(decodedSchema.kind).toBe(SchemaKind.RECORD);
178
+
179
+ // 6. Verify Meta Data (Attributes)
180
+ expect(decodedSchema.attr["key"]).toBe("value");
181
+ expect(decodedSchema.attr["count"]).toBe(10);
182
+ expect(decodedSchema.attr["isActive"]).toBe(true);
183
+
184
+ // 7. Verify Tags
185
+ expect(decodedSchema.tags).toContain("myTag");
186
+ expect(decodedSchema.tags).toContain("urgent");
187
+ expect(decodedSchema.tags).toHaveLength(2);
188
+
189
+ // 8. Verify Comments
190
+ expect(decodedSchema.comments).toHaveLength(2);
191
+ expect(decodedSchema.comments).toContain("comment1");
192
+ expect(decodedSchema.comments).toContain("comment2");
193
+
194
+ // Additional checks for configuration flags
195
+ const decodedTextClean = encode(decodedNode, {
196
+ compact: true,
197
+ includeMeta: false,
198
+ includeComments: false
199
+ });
200
+
201
+ // Assertions for No Meta
202
+ expect(decodedTextClean).not.toContain("$key");
203
+ expect(decodedTextClean).not.toContain("#myTag");
204
+ expect(decodedTextClean).not.toContain("/"); // No meta block
205
+ // Empty schema without meta encodes to <any> or similar
206
+ expect(decodedTextClean).toContain("<any>");
207
+
208
+ // B. Encode WITH Meta (includeMeta=true) but NO Type (includeType=false)
209
+ const decodedTextWithMeta = encode(decodedNode, {
210
+ compact: true,
211
+ includeType: false,
212
+ includeMeta: true,
213
+ includeComments: false
214
+ });
215
+
216
+ // Assertions for With Meta
217
+ expect(decodedTextWithMeta).toContain("$key=");
218
+ expect(decodedTextWithMeta).toContain("#myTag");
219
+ expect(decodedTextWithMeta).toContain("$count=10");
220
+ expect(decodedTextWithMeta).toContain("/");
221
+
222
+ const expected = '<//*comment1*/ /*comment2*/ $key="value" $count=10 $isActive=true #myTag #urgent/ any>(null)';
223
+ assertRoundtrip(originalNode, expected, false);
224
+ });
225
+
226
+ it('should handle meta schema list vs element', () => {
227
+ /**
228
+ * Tests nested metadata within a type definition:
229
+ * Outer: / $listAttr="GlobalList" / -> Applies to the entire List
230
+ * Inner: / $elemAttr="InnerRecord" / -> Applies to the Element (Record) inside
231
+ */
232
+ const aidText = `
233
+ <
234
+ /* comm-header-0 */ /* comm-header-1 /* comm-header-1.1*/ */
235
+ / $listAttr="GlobalList" $b=4 /*com-in*/ /
236
+ /* comm-after-header-0 */
237
+ [
238
+ / $elemAttr="InnerRecord" #elem0 /* comm-inside-header-0 */ /
239
+ /* comm-inside-field-0 */ #elem1 id: int
240
+ ]
241
+ >
242
+ [ /* comm-data-v1 */ (1) /* comm-data-v2 */ ]
243
+ `;
244
+
245
+ const results = decode(aidText, { debug: false });
246
+ const node = results.node;
247
+ const errors = results.errors;
248
+
249
+ expect(errors).toHaveLength(0);
250
+
251
+ // 1. Check List Meta (Outer)
252
+ expect(node.isList).toBe(true);
253
+ // Access attributes via .attr (Mixin)
254
+ expect(node.schema.attr["listAttr"]).toBe("GlobalList");
255
+ expect(node.schema.attr["elemAttr"]).toBe("InnerRecord");
256
+
257
+ expect(node.schema.attr["b"]).toBe(4);
258
+
259
+ // 2. Check Element Meta (Inner Record)
260
+ const elemSchema = node.schema.element!;
261
+ expect(elemSchema.kind).toBe(SchemaKind.RECORD);
262
+ expect(elemSchema.attr).toStrictEqual({});
263
+
264
+ // Check if element meta propagated to actual data elements (depends on implementation)
265
+ // Usually schema meta is on schema, data meta is on data node.
266
+ // Here we check the schema attached to the element node.
267
+ expect(node.elements[0].schema.attr).toStrictEqual({});
268
+
269
+ const expected = '<[//*com-in*/ /*comm-header-0*/ /*comm-header-1 /* comm-header-1.1*/*/ /*comm-after-header-0*/ /*comm-inside-header-0*/ $listAttr="GlobalList" $b=4 $elemAttr="InnerRecord" #elem0/ /*comm-inside-field-0*/ #elem1 id:number]>[(//*comm-data-v1*/ /*comm-data-v2*// 1)]';
270
+ assertRoundtrip(aidText, expected, false);
271
+ });
272
+
273
+ it('should handle meta schema before fields', () => {
274
+ const aidText = `
275
+ <
276
+ /* header-com-0 */
277
+ / #tag_header /
278
+ /* comm-data-v1 */ #tag1 v1: number /* comm-data-v2 */ #tag2,
279
+ /* comm-data-v3 */ #tag3 v2: number /* comm-data-v3 */ #tag4
280
+ >
281
+ [
282
+ / #tag_list /
283
+ /* comm-data-v1 */ #tag1 1 /* comm-data-v2 */ #tag2
284
+ /* comm-data-v3 */ #tag3 2 /* comm-data-v3 */ #tag4
285
+ ]
286
+ `;
287
+
288
+ const results = decode(aidText, { debug: false });
289
+ const node = results.node;
290
+ expect(results.errors).toHaveLength(0);
291
+
292
+ const expected = '<[/#tag_header/ number]>[/#tag_list/ /*comm-data-v1*/ #tag1 1,/*comm-data-v2*/ /*comm-data-v3*/ /*comm-data-v3*/ #tag2 #tag3 #tag4 2]';
293
+ assertRoundtrip(aidText, expected, false);
294
+ });
295
+
296
+ it('should warn on meta schema with implicit values', () => {
297
+ /**
298
+ * Tests handling of malformed meta blocks.
299
+ * In the input: / listAttr="GlobalList" / is missing '$' prefix for attribute.
300
+ */
301
+ const aidText = `
302
+ <
303
+ / listAttr="GlobalList" /
304
+ [
305
+ /* Missing $ prefix */
306
+ / $elemAttr="InnerRecord" /* fixed input */ /
307
+ /* comments2 */ id: int
308
+ ]
309
+ >
310
+ [ (1) ]
311
+ `;
312
+
313
+ const results = decode(aidText, { debug: false });
314
+ const node = results.node;
315
+ const warnings = results.warnings;
316
+
317
+ // If the input was fixed above, errors should be 0.
318
+ // Because we have: listAttr="GlobalList" (implicit), it should be a warning.
319
+ expect(warnings.length).toBeGreaterThan(0);
320
+ expect(warnings[0].message).toContain("Implicit attribute 'listAttr'");
321
+
322
+ // 1. Check List Meta (Outer)
323
+ expect(node.isList).toBe(true);
324
+ expect(node.schema.attr["listAttr"]).toBe("GlobalList");
325
+
326
+ // 2. Check Element Meta (Inner Record)
327
+ const elemSchema = node.schema.element!;
328
+ expect(elemSchema.kind).toBe(SchemaKind.RECORD);
329
+ expect(node.schema.attr["elemAttr"]).toBe("InnerRecord");
330
+
331
+ const expected = '<[//*fixed input*/ $listAttr="GlobalList" $elemAttr="InnerRecord"/ /*Missing $ prefix*/ /*comments2*/ id:number]>[(1)]';
332
+ assertRoundtrip(aidText, expected, false);
333
+ });
334
+
335
+ it('should handle meta schema field modifiers', () => {
336
+ /**
337
+ * Tests field modifiers inside a record definition: !required, $key=value.
338
+ */
339
+ const aidText = `
340
+ <
341
+ /* comm0 */
342
+ / $id=0 /*comm2 /* comm2.5*/ */ /
343
+
344
+ /* comm3 */
345
+
346
+ /* Modifiers block before field name */
347
+ !required $key=101 id:int,
348
+
349
+ $desc="User Name"
350
+ name: string
351
+ >
352
+ ( /* comment0 */ / $id=3 /*comment2*/ / /*comment3*/ 1, "Alice" $id=65 #alice /*comment4*/ )
353
+ `;
354
+
355
+ const results = decode(aidText, { debug: false });
356
+ const node = results.node;
357
+ expect(results.errors).toHaveLength(0);
358
+
359
+ expect(node.isRecord).toBe(true);
360
+
361
+ // Retrieve field definitions from schema
362
+ const fields = node.schema.fields;
363
+
364
+ // Field 'id'
365
+ const fId = fields.find(f => f.name === "id")!;
366
+ expect(fId).toBeDefined();
367
+ expect(fId.required).toBe(true);
368
+ expect(fId.attr["key"]).toBe(101);
369
+
370
+ // Field 'name'
371
+ const fName = fields.find(f => f.name === "name")!;
372
+ expect(fName).toBeDefined();
373
+ expect(fName.required).toBe(false); // Default
374
+ expect(fName.attr["desc"]).toBe("User Name");
375
+
376
+ // Check Instance Data Meta (the node itself, not the schema)
377
+ // The record instance has / $id=3 /
378
+ expect(node.attr["id"]).toBe(3);
379
+
380
+ const expected = '<//*comm2 /* comm2.5*/*/ $id=0/ /*comm0*/ /*comm3*/ /*Modifiers block before field name*/ !required $key=101 id:number,$desc="User Name" name:string>(//*comment2*/ $id=3/ /*comment0*/ /*comment3*/ 1,/*comment4*/ $id=65 #alice "Alice")';
381
+ assertRoundtrip(aidText, expected, false);
382
+ });
383
+
384
+
385
+ // ==============================================================================
386
+ // 2. DATA BLOCK META (Metadata inside data blocks [ ... ])
387
+ // ==============================================================================
388
+
389
+ it('should handle meta data block list primitive', () => {
390
+ /**
391
+ * Tests metadata inside a data block for a simple list.
392
+ * Syntax: [ / @size=3 / 1, 2, 3 ]
393
+ */
394
+ const aidText = '[ / $size=3 $author="me" / 1, 2, 3 ]';
395
+
396
+ const results = decode(aidText, { debug: false });
397
+ const node = results.node;
398
+ expect(results.errors).toHaveLength(0);
399
+
400
+ expect(node.isList).toBe(true);
401
+ // Meta should go to this specific node's attributes
402
+ expect(node.attr["size"]).toBe(3);
403
+ expect(node.attr["author"]).toBe("me");
404
+
405
+ // Check content
406
+ expect(node.elements).toHaveLength(3);
407
+ expect(node.elements[0].value).toBe(1);
408
+
409
+ const expected = '<[number]>[/$size=3 $author="me"/ 1,2,3]';
410
+ assertRoundtrip(aidText, expected, false);
411
+ });
412
+
413
+ // ==============================================================================
414
+ // 3. NESTED META (Lists within lists)
415
+ // ==============================================================================
416
+
417
+ it('should handle meta nested lists', () => {
418
+ /**
419
+ * Tests metadata assignment in nested lists.
420
+ */
421
+ const aidText = `
422
+ [
423
+ / $level=0 /
424
+ [
425
+ / $level=1 /
426
+ 1, 2
427
+ ],
428
+ [
429
+ / $level=2 /
430
+ 3, 4
431
+ ]
432
+ ]
433
+ `;
434
+
435
+ const results = decode(aidText, { debug: false });
436
+ const node = results.node;
437
+ expect(results.errors).toHaveLength(0);
438
+
439
+ // Root Node
440
+ expect(node.isList).toBe(true);
441
+ expect(node.attr["level"]).toBe(0);
442
+
443
+ // Inner Node 1
444
+ const inner1 = node.elements[0];
445
+ expect(inner1.isList).toBe(true);
446
+ expect(inner1.attr["level"]).toBe(1);
447
+
448
+ // Inner Node 2
449
+ const inner2 = node.elements[1];
450
+ expect(inner2.isList).toBe(true);
451
+ expect(inner2.attr["level"]).toBe(2);
452
+
453
+ const expected = '<[[number]]>[/$level=0/ [/$level=1/ 1,2],[/$level=2/ 3,4]]';
454
+ assertRoundtrip(aidText, expected, false);
455
+ });
456
+
457
+ // ==============================================================================
458
+ // 4. EDGE CASES & OVERRIDES
459
+ // ==============================================================================
460
+
461
+ it('should handle meta mixed with type override', () => {
462
+ /**
463
+ * Tests a scenario where we have metadata for the list AND a type override for an element.
464
+ */
465
+ const aidText = '[ / $info="mixed" / 1, 2, <string> "3" ]';
466
+ const expected = '<[number]>[/$info="mixed"/ 1,2,<string> "3"]';
467
+
468
+ const result = decode(aidText, { debug: false });
469
+ const node = result.node;
470
+ expect(result.errors).toHaveLength(0);
471
+
472
+ // List Meta
473
+ expect(node.attr["info"]).toBe("mixed");
474
+
475
+ // List Type Inference (Should be Number based on first element '1')
476
+ expect(node.schema.element?.typeName).toBe("number");
477
+
478
+ // Element Override
479
+ const elLast = node.elements[2];
480
+ expect(elLast.schema.typeName).toBe("string");
481
+ expect(elLast.value).toBe("3");
482
+
483
+ assertRoundtrip(node, expected, false);
484
+ });
485
+
486
+ it('should handle meta and explicit type in data', () => {
487
+ /**
488
+ * Tests a scenario where an explicit type is provided inside the / ... / block.
489
+ * The parser must understand that type is the list type, and @tag is metadata.
490
+ */
491
+ const aidText = '[ / $tag=1 / 1, 2 ]';
492
+
493
+ const result = decode(aidText, { debug: false });
494
+ const node = result.node;
495
+ expect(result.errors).toHaveLength(0);
496
+
497
+ expect(node.isList).toBe(true);
498
+ // Inferred type
499
+ expect(node.schema.element?.typeName).toBe("number");
500
+ expect(Object.keys(node.attr)).toHaveLength(1);
501
+
502
+ const expected = "<[number]>[/$tag=1/ 1,2]";
503
+ assertRoundtrip(node, expected, false);
504
+ });
505
+
506
+ });
package/tests/utils.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { expect } from 'vitest';
2
+ import { Node } from '../src/models/Node';
3
+ // Zakładam, że masz te funkcje wyeksportowane z index.ts (szczegóły niżej)
4
+ import { decode, encode } from '../src/index';
5
+
6
+ /**
7
+ * Validates encoding consistency (Round-Trip):
8
+ * 1. If source is text -> decode it to a Node.
9
+ * 2. Encode Node -> check if it matches expectedOutput.
10
+ * 3. Decode the result (encoded_txt) -> check if it remains a valid Node.
11
+ * 4. Re-encode -> check if the result is stable (idempotent).
12
+ * * Returns the Node so that further logical assertions (field checking) can be performed.
13
+ */
14
+ export function assertRoundtrip(
15
+ source: string | Node | any,
16
+ expectedOutput: string,
17
+ debug: boolean = false
18
+ ): Node {
19
+ let node: Node;
20
+
21
+ // 1. Prepare Node (if input is raw text)
22
+ if (typeof source === 'string') {
23
+ // Przekazujemy debug do decodera
24
+ const res = decode(source, { debug });
25
+
26
+ if (res.errors.length > 0) {
27
+ console.error("Input decoding errors:", res.errors);
28
+ }
29
+ expect(res.errors, `Input decoding errors: ${res.errors.join(', ')}`).toHaveLength(0);
30
+ node = res.node;
31
+ } else {
32
+ node = source;
33
+ }
34
+
35
+ // 2. First Encoding
36
+ // Wymuszamy compact: true zgodnie z pythonowym oryginałem
37
+ const encoded1 = encode(node, { compact: true });
38
+
39
+ // Debug print to visualize differences in case of failure
40
+ // (Vitest zrobi to automatycznie przy .toBe, ale zachowujemy logikę z Python)
41
+ if (encoded1 !== expectedOutput) {
42
+ console.log(`\n[ROUNDTRIP] Mismatch Pass 1:`);
43
+ console.log(`EXPECTED: "${expectedOutput}"`);
44
+ console.log(`ACTUAL: "${encoded1}"`);
45
+ }
46
+
47
+ expect(encoded1).toBe(expectedOutput);
48
+
49
+ // 3. Round Trip (Decode the result of the encoding)
50
+ const res2 = decode(encoded1, { debug });
51
+
52
+ if (res2.errors.length > 0) {
53
+ console.error("Re-decoding errors:", res2.errors);
54
+ }
55
+ expect(res2.errors, `Re-decoding errors: ${res2.errors.join(', ')}`).toHaveLength(0);
56
+
57
+ // 4. Second Encoding (Idempotency Check)
58
+ const encoded2 = encode(res2.node, { compact: true });
59
+
60
+ if (encoded2 !== expectedOutput) {
61
+ console.log(`\n[ROUNDTRIP] Mismatch Pass 2 (Consistency):`);
62
+ console.log(`EXPECTED: "${expectedOutput}"`);
63
+ console.log(`ACTUAL: "${encoded2}"`);
64
+ }
65
+
66
+ expect(encoded2).toBe(expectedOutput);
67
+
68
+ return node;
69
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* --- Basic Language Configuration --- */
4
+ "target": "ES2020", /* JavaScript version to generate. ES2020 is safe for Node 14+ */
5
+ "module": "CommonJS", /* Module standard (Node.js uses CommonJS by default) */
6
+ "lib": ["ES2020"], /* Available libraries (e.g., Promise, Map, Set) */
7
+ "moduleResolution": "node", /* Module resolution strategy */
8
+
9
+ /* --- Output File Structure --- */
10
+ "outDir": "./dist", /* Where compiled files should go */
11
+ "rootDir": "./src", /* Source directory. Ensures strictly that only files inside src are compiled */
12
+ "declaration": true, /* Generate .d.ts files (required for npm libraries) */
13
+ "declarationMap": true, /* Allows editors (VS Code) to go to source definition */
14
+ "sourceMap": true, /* Generate source maps (.js.map) for debugging */
15
+ "removeComments": false, /* Do not remove comments (useful for JSDoc in IDEs) */
16
+
17
+ /* --- Interoperability --- */
18
+ "esModuleInterop": true, /* Allows importing CommonJS modules as default imports */
19
+ "forceConsistentCasingInFileNames": true, /* Enforce consistent casing in file names */
20
+ "resolveJsonModule": true, /* Allows importing .json files in code */
21
+
22
+ /* --- Strict Type-Checking --- */
23
+ "strict": true, /* Enable all strict type-checking options */
24
+ "noImplicitAny": true, /* Error if type is not explicit and is 'any' */
25
+ "strictNullChecks": true, /* Error on unhandled null/undefined */
26
+ "strictPropertyInitialization": true, /* Check if class properties are initialized in the constructor */
27
+ "noImplicitReturns": true, /* Every code path in a function must return a value */
28
+ "noFallthroughCasesInSwitch": true, /* Prevent accidental fallthrough in switch cases */
29
+
30
+ /* --- Code Cleanliness --- */
31
+ "noUnusedLocals": true, /* Report error for unused local variables */
32
+ "noUnusedParameters": true, /* Report error for unused function parameters */
33
+
34
+ /* --- Compilation Optimization --- */
35
+ "skipLibCheck": true /* Skip type checking of declaration files (.d.ts) in external libs */
36
+ },
37
+ "include": [
38
+ "src/**/*" /* Compile everything in the src folder */
39
+ ],
40
+ "exclude": [
41
+ "node_modules",
42
+ "dist",
43
+ "**/*.test.ts", /* Do not include tests in the production build */
44
+ "**/*.spec.ts"
45
+ ]
46
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts', 'src/**/*.spec.ts'],
6
+ globals: false,
7
+ environment: 'node',
8
+ },
9
+ });