@globaltypesystem/gts-ts 0.1.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 (81) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/workflows/ci.yml +198 -0
  3. package/.gitmodules +3 -0
  4. package/.prettierrc +7 -0
  5. package/LICENSE +201 -0
  6. package/Makefile +64 -0
  7. package/README.md +298 -0
  8. package/dist/cast.d.ts +9 -0
  9. package/dist/cast.d.ts.map +1 -0
  10. package/dist/cast.js +153 -0
  11. package/dist/cast.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +318 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/compatibility.d.ts +11 -0
  17. package/dist/compatibility.d.ts.map +1 -0
  18. package/dist/compatibility.js +176 -0
  19. package/dist/compatibility.js.map +1 -0
  20. package/dist/extract.d.ts +13 -0
  21. package/dist/extract.d.ts.map +1 -0
  22. package/dist/extract.js +194 -0
  23. package/dist/extract.js.map +1 -0
  24. package/dist/gts.d.ts +18 -0
  25. package/dist/gts.d.ts.map +1 -0
  26. package/dist/gts.js +472 -0
  27. package/dist/gts.js.map +1 -0
  28. package/dist/index.d.ts +29 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +97 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/query.d.ts +10 -0
  33. package/dist/query.d.ts.map +1 -0
  34. package/dist/query.js +171 -0
  35. package/dist/query.js.map +1 -0
  36. package/dist/relationships.d.ts +7 -0
  37. package/dist/relationships.d.ts.map +1 -0
  38. package/dist/relationships.js +80 -0
  39. package/dist/relationships.js.map +1 -0
  40. package/dist/server/index.d.ts +2 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +132 -0
  43. package/dist/server/index.js.map +1 -0
  44. package/dist/server/server.d.ts +33 -0
  45. package/dist/server/server.d.ts.map +1 -0
  46. package/dist/server/server.js +678 -0
  47. package/dist/server/server.js.map +1 -0
  48. package/dist/server/types.d.ts +61 -0
  49. package/dist/server/types.d.ts.map +1 -0
  50. package/dist/server/types.js +3 -0
  51. package/dist/server/types.js.map +1 -0
  52. package/dist/store.d.ts +39 -0
  53. package/dist/store.d.ts.map +1 -0
  54. package/dist/store.js +1026 -0
  55. package/dist/store.js.map +1 -0
  56. package/dist/types.d.ts +111 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +29 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/x-gts-ref.d.ts +35 -0
  61. package/dist/x-gts-ref.d.ts.map +1 -0
  62. package/dist/x-gts-ref.js +304 -0
  63. package/dist/x-gts-ref.js.map +1 -0
  64. package/jest.config.js +13 -0
  65. package/package.json +54 -0
  66. package/src/cast.ts +179 -0
  67. package/src/cli/index.ts +315 -0
  68. package/src/compatibility.ts +201 -0
  69. package/src/extract.ts +213 -0
  70. package/src/gts.ts +550 -0
  71. package/src/index.ts +97 -0
  72. package/src/query.ts +191 -0
  73. package/src/relationships.ts +91 -0
  74. package/src/server/index.ts +112 -0
  75. package/src/server/server.ts +771 -0
  76. package/src/server/types.ts +74 -0
  77. package/src/store.ts +1178 -0
  78. package/src/types.ts +138 -0
  79. package/src/x-gts-ref.ts +349 -0
  80. package/tests/gts.test.ts +525 -0
  81. package/tsconfig.json +32 -0
package/src/store.ts ADDED
@@ -0,0 +1,1178 @@
1
+ import Ajv from 'ajv';
2
+ import { GtsConfig, JsonEntity, ValidationResult, GTS_URI_PREFIX } from './types';
3
+ import { Gts } from './gts';
4
+ import { GtsExtractor } from './extract';
5
+ import { XGtsRefValidator } from './x-gts-ref';
6
+
7
+ export class GtsStore {
8
+ private byId: Map<string, JsonEntity> = new Map();
9
+ private config: GtsConfig;
10
+ private ajv: Ajv;
11
+
12
+ constructor(config?: Partial<GtsConfig>) {
13
+ this.config = {
14
+ validateRefs: config?.validateRefs ?? false,
15
+ strictMode: config?.strictMode ?? false,
16
+ };
17
+
18
+ this.ajv = new Ajv({
19
+ strict: false,
20
+ validateSchema: false,
21
+ addUsedSchema: false,
22
+ loadSchema: this.loadSchema.bind(this),
23
+ validateFormats: false, // Disable format validation to match Go implementation
24
+ });
25
+ // Don't add format validators since Go uses lenient validation
26
+ }
27
+
28
+ private async loadSchema(uri: string): Promise<any> {
29
+ const normalizedUri = uri.startsWith(GTS_URI_PREFIX) ? uri.substring(GTS_URI_PREFIX.length) : uri;
30
+
31
+ if (Gts.isValidGtsID(normalizedUri)) {
32
+ const entity = this.get(normalizedUri);
33
+ if (entity && entity.isSchema) {
34
+ return entity.content;
35
+ }
36
+ }
37
+ throw new Error(`Unresolvable GTS reference: ${uri}`);
38
+ }
39
+
40
+ register(entity: JsonEntity): void {
41
+ if (this.config.validateRefs) {
42
+ for (const ref of entity.references) {
43
+ if (!this.byId.has(ref)) {
44
+ throw new Error(`Unresolved reference: ${ref}`);
45
+ }
46
+ }
47
+ }
48
+ this.byId.set(entity.id, entity);
49
+
50
+ // If this is a schema, add it to AJV for reference resolution
51
+ if (entity.isSchema && entity.content) {
52
+ try {
53
+ const normalizedSchema = this.normalizeSchema(entity.content);
54
+ // Set $id to the GTS ID if not already set
55
+ if (!normalizedSchema.$id) {
56
+ normalizedSchema.$id = entity.id;
57
+ }
58
+ this.ajv.addSchema(normalizedSchema, entity.id);
59
+ } catch (err) {
60
+ // Ignore errors adding schema - it might already exist or be invalid
61
+ }
62
+ }
63
+ }
64
+
65
+ get(id: string): JsonEntity | undefined {
66
+ return this.byId.get(id);
67
+ }
68
+
69
+ getAll(): JsonEntity[] {
70
+ return Array.from(this.byId.values());
71
+ }
72
+
73
+ query(pattern: string, limit?: number): string[] {
74
+ const results: string[] = [];
75
+ const maxResults = limit ?? Number.MAX_SAFE_INTEGER;
76
+
77
+ for (const [id] of this.byId) {
78
+ if (results.length >= maxResults) break;
79
+
80
+ const matchResult = Gts.matchIDPattern(id, pattern);
81
+ if (matchResult.match) {
82
+ results.push(id);
83
+ }
84
+ }
85
+
86
+ return results;
87
+ }
88
+
89
+ validateInstance(gtsId: string): ValidationResult {
90
+ try {
91
+ const gid = Gts.parseGtsID(gtsId);
92
+
93
+ const obj = this.get(gid.id);
94
+ if (!obj) {
95
+ return {
96
+ id: gtsId,
97
+ ok: false,
98
+ valid: false,
99
+ error: `Entity not found: ${gtsId}`,
100
+ };
101
+ }
102
+
103
+ if (!obj.schemaId) {
104
+ return {
105
+ id: gtsId,
106
+ ok: false,
107
+ valid: false,
108
+ error: `No schema found for instance: ${gtsId}`,
109
+ };
110
+ }
111
+
112
+ const schemaEntity = this.get(obj.schemaId);
113
+ if (!schemaEntity) {
114
+ return {
115
+ id: gtsId,
116
+ ok: false,
117
+ valid: false,
118
+ error: `Schema not found: ${obj.schemaId}`,
119
+ };
120
+ }
121
+
122
+ if (!schemaEntity.isSchema) {
123
+ return {
124
+ id: gtsId,
125
+ ok: false,
126
+ valid: false,
127
+ error: `Entity '${obj.schemaId}' is not a schema`,
128
+ };
129
+ }
130
+
131
+ const validate = this.ajv.compile(this.normalizeSchema(schemaEntity.content));
132
+ const isValid = validate(obj.content);
133
+
134
+ if (!isValid) {
135
+ const errors =
136
+ validate.errors
137
+ ?.map((e) => {
138
+ if (e.keyword === 'required') {
139
+ return `${e.instancePath || '/'} must have required property '${(e.params as any)?.missingProperty}'`;
140
+ }
141
+ return `${e.instancePath} ${e.message}`;
142
+ })
143
+ .join('; ') || 'Validation failed';
144
+ return {
145
+ id: gtsId,
146
+ ok: false,
147
+ valid: false,
148
+ error: errors,
149
+ };
150
+ }
151
+
152
+ // Validate x-gts-ref constraints
153
+ const xGtsRefValidator = new XGtsRefValidator(this);
154
+ const xGtsRefErrors = xGtsRefValidator.validateInstance(obj.content, schemaEntity.content);
155
+ if (xGtsRefErrors.length > 0) {
156
+ const errorMsgs = xGtsRefErrors.map((err) => err.reason).join('; ');
157
+ return {
158
+ id: gtsId,
159
+ ok: false,
160
+ valid: false,
161
+ error: `x-gts-ref validation failed: ${errorMsgs}`,
162
+ };
163
+ }
164
+
165
+ return {
166
+ id: gtsId,
167
+ ok: true,
168
+ valid: true,
169
+ error: '',
170
+ };
171
+ } catch (error) {
172
+ return {
173
+ id: gtsId,
174
+ ok: false,
175
+ valid: false,
176
+ error: error instanceof Error ? error.message : String(error),
177
+ };
178
+ }
179
+ }
180
+
181
+ private normalizeSchema(schema: any): any {
182
+ return this.normalizeSchemaRecursive(schema);
183
+ }
184
+
185
+ private normalizeSchemaRecursive(obj: any): any {
186
+ if (obj === null || typeof obj !== 'object') {
187
+ return obj;
188
+ }
189
+
190
+ if (Array.isArray(obj)) {
191
+ return obj.map((item) => this.normalizeSchemaRecursive(item));
192
+ }
193
+
194
+ const normalized: any = {};
195
+
196
+ for (const [key, value] of Object.entries(obj)) {
197
+ let newKey = key;
198
+ let newValue = value;
199
+
200
+ // Convert $$ prefixed keys to $ prefixed keys
201
+ switch (key) {
202
+ case '$$id':
203
+ newKey = '$id';
204
+ break;
205
+ case '$$schema':
206
+ newKey = '$schema';
207
+ break;
208
+ case '$$ref':
209
+ newKey = '$ref';
210
+ break;
211
+ case '$$defs':
212
+ newKey = '$defs';
213
+ break;
214
+ }
215
+
216
+ // Recursively normalize nested objects
217
+ if (value && typeof value === 'object') {
218
+ newValue = this.normalizeSchemaRecursive(value);
219
+ }
220
+
221
+ normalized[newKey] = newValue;
222
+ }
223
+
224
+ // Normalize $id values
225
+ if (normalized['$id'] && typeof normalized['$id'] === 'string') {
226
+ if (normalized['$id'].startsWith(GTS_URI_PREFIX)) {
227
+ normalized['$id'] = normalized['$id'].substring(GTS_URI_PREFIX.length);
228
+ }
229
+ }
230
+
231
+ // Normalize $ref values
232
+ if (normalized['$ref'] && typeof normalized['$ref'] === 'string') {
233
+ if (normalized['$ref'].startsWith(GTS_URI_PREFIX)) {
234
+ normalized['$ref'] = normalized['$ref'].substring(GTS_URI_PREFIX.length);
235
+ }
236
+ }
237
+
238
+ return normalized;
239
+ }
240
+
241
+ resolveRelationships(gtsId: string): any {
242
+ const seen = new Set<string>();
243
+ return this.buildSchemaGraphNode(gtsId, seen);
244
+ }
245
+
246
+ private buildSchemaGraphNode(gtsId: string, seen: Set<string>): any {
247
+ const node: any = {
248
+ id: gtsId,
249
+ };
250
+
251
+ // Check for cycles
252
+ if (seen.has(gtsId)) {
253
+ return node;
254
+ }
255
+ seen.add(gtsId);
256
+
257
+ // Get the entity from store
258
+ const entity = this.get(gtsId);
259
+ if (!entity) {
260
+ node.errors = ['Entity not found'];
261
+ return node;
262
+ }
263
+
264
+ // Process GTS references found in the entity
265
+ const refs = this.extractGtsReferences(entity.content);
266
+ const nodeRefs: any = {};
267
+
268
+ for (const ref of refs) {
269
+ // Skip self-references
270
+ if (ref.id === gtsId) {
271
+ continue;
272
+ }
273
+ // Skip JSON Schema meta-schema references
274
+ if (this.isJsonSchemaUrl(ref.id)) {
275
+ continue;
276
+ }
277
+ // Recursively build node for this reference
278
+ nodeRefs[ref.sourcePath] = this.buildSchemaGraphNode(ref.id, seen);
279
+ }
280
+
281
+ if (Object.keys(nodeRefs).length > 0) {
282
+ node.refs = nodeRefs;
283
+ }
284
+
285
+ // Process schema ID if present
286
+ if (entity.schemaId) {
287
+ if (!this.isJsonSchemaUrl(entity.schemaId)) {
288
+ node.schema_id = this.buildSchemaGraphNode(entity.schemaId, seen);
289
+ }
290
+ } else if (!entity.isSchema) {
291
+ // Instance without schema ID is an error
292
+ node.errors = node.errors || [];
293
+ node.errors.push('Schema not recognized');
294
+ }
295
+
296
+ return node;
297
+ }
298
+
299
+ private extractGtsReferences(content: any): Array<{ id: string; sourcePath: string }> {
300
+ const refs: Array<{ id: string; sourcePath: string }> = [];
301
+ const seen = new Set<string>();
302
+
303
+ const walkAndCollectRefs = (node: any, path: string) => {
304
+ if (node === null || node === undefined) {
305
+ return;
306
+ }
307
+
308
+ // Check if current node is a GTS ID string
309
+ if (typeof node === 'string') {
310
+ if (Gts.isValidGtsID(node)) {
311
+ const sourcePath = path || 'root';
312
+ const key = `${node}|${sourcePath}`;
313
+ if (!seen.has(key)) {
314
+ refs.push({ id: node, sourcePath });
315
+ seen.add(key);
316
+ }
317
+ }
318
+ return;
319
+ }
320
+
321
+ // Recurse into object
322
+ if (typeof node === 'object' && !Array.isArray(node)) {
323
+ for (const [k, v] of Object.entries(node)) {
324
+ const nextPath = path ? `${path}.${k}` : k;
325
+ walkAndCollectRefs(v, nextPath);
326
+ }
327
+ return;
328
+ }
329
+
330
+ // Recurse into array
331
+ if (Array.isArray(node)) {
332
+ for (let i = 0; i < node.length; i++) {
333
+ const nextPath = path ? `${path}[${i}]` : `[${i}]`;
334
+ walkAndCollectRefs(node[i], nextPath);
335
+ }
336
+ }
337
+ };
338
+
339
+ walkAndCollectRefs(content, '');
340
+ return refs;
341
+ }
342
+
343
+ private isJsonSchemaUrl(s: string): boolean {
344
+ return (s.startsWith('http://') || s.startsWith('https://')) && s.includes('json-schema.org');
345
+ }
346
+
347
+ checkCompatibility(oldSchemaId: string, newSchemaId: string, _mode?: string): any {
348
+ const oldEntity = this.get(oldSchemaId);
349
+ const newEntity = this.get(newSchemaId);
350
+
351
+ if (!oldEntity || !newEntity) {
352
+ return {
353
+ from: oldSchemaId,
354
+ to: newSchemaId,
355
+ old: oldSchemaId,
356
+ new: newSchemaId,
357
+ direction: 'unknown',
358
+ added_properties: [],
359
+ removed_properties: [],
360
+ changed_properties: [],
361
+ is_fully_compatible: false,
362
+ is_backward_compatible: false,
363
+ is_forward_compatible: false,
364
+ incompatibility_reasons: [],
365
+ backward_errors: ['Schema not found'],
366
+ forward_errors: ['Schema not found'],
367
+ };
368
+ }
369
+
370
+ const oldSchema = oldEntity.content;
371
+ const newSchema = newEntity.content;
372
+
373
+ if (!oldSchema || !newSchema) {
374
+ return {
375
+ from: oldSchemaId,
376
+ to: newSchemaId,
377
+ old: oldSchemaId,
378
+ new: newSchemaId,
379
+ direction: 'unknown',
380
+ added_properties: [],
381
+ removed_properties: [],
382
+ changed_properties: [],
383
+ is_fully_compatible: false,
384
+ is_backward_compatible: false,
385
+ is_forward_compatible: false,
386
+ incompatibility_reasons: [],
387
+ backward_errors: ['Invalid schema content'],
388
+ forward_errors: ['Invalid schema content'],
389
+ };
390
+ }
391
+
392
+ // Check compatibility
393
+ const { isBackward, backwardErrors } = this.checkBackwardCompatibility(oldSchema, newSchema);
394
+ const { isForward, forwardErrors } = this.checkForwardCompatibility(oldSchema, newSchema);
395
+
396
+ // Determine direction
397
+ const direction = this.inferDirection(oldSchemaId, newSchemaId);
398
+
399
+ return {
400
+ from: oldSchemaId,
401
+ to: newSchemaId,
402
+ old: oldSchemaId,
403
+ new: newSchemaId,
404
+ direction,
405
+ added_properties: [],
406
+ removed_properties: [],
407
+ changed_properties: [],
408
+ is_fully_compatible: isBackward && isForward,
409
+ is_backward_compatible: isBackward,
410
+ is_forward_compatible: isForward,
411
+ incompatibility_reasons: [],
412
+ backward_errors: backwardErrors,
413
+ forward_errors: forwardErrors,
414
+ };
415
+ }
416
+
417
+ private inferDirection(fromId: string, toId: string): string {
418
+ try {
419
+ const fromGtsId = Gts.parseGtsID(fromId);
420
+ const toGtsId = Gts.parseGtsID(toId);
421
+
422
+ if (!fromGtsId.segments.length || !toGtsId.segments.length) {
423
+ return 'unknown';
424
+ }
425
+
426
+ const fromSeg = fromGtsId.segments[fromGtsId.segments.length - 1];
427
+ const toSeg = toGtsId.segments[toGtsId.segments.length - 1];
428
+
429
+ if (fromSeg.verMinor !== undefined && toSeg.verMinor !== undefined) {
430
+ if (toSeg.verMinor > fromSeg.verMinor) {
431
+ return 'up';
432
+ }
433
+ if (toSeg.verMinor < fromSeg.verMinor) {
434
+ return 'down';
435
+ }
436
+ return 'none';
437
+ }
438
+
439
+ return 'unknown';
440
+ } catch {
441
+ return 'unknown';
442
+ }
443
+ }
444
+
445
+ private checkBackwardCompatibility(
446
+ oldSchema: any,
447
+ newSchema: any
448
+ ): { isBackward: boolean; backwardErrors: string[] } {
449
+ return this.checkSchemaCompatibility(oldSchema, newSchema, true);
450
+ }
451
+
452
+ private checkForwardCompatibility(oldSchema: any, newSchema: any): { isForward: boolean; forwardErrors: string[] } {
453
+ return this.checkSchemaCompatibility(oldSchema, newSchema, false);
454
+ }
455
+
456
+ private checkSchemaCompatibility(oldSchema: any, newSchema: any, checkBackward: boolean): any {
457
+ const errors: string[] = [];
458
+
459
+ // Flatten schemas to handle allOf
460
+ const oldFlat = this.flattenSchema(oldSchema);
461
+ const newFlat = this.flattenSchema(newSchema);
462
+
463
+ const oldProps = oldFlat.properties || {};
464
+ const newProps = newFlat.properties || {};
465
+ const oldRequired = new Set(oldFlat.required || []);
466
+ const newRequired = new Set(newFlat.required || []);
467
+
468
+ // Check required properties changes
469
+ if (checkBackward) {
470
+ // Backward: cannot add required properties
471
+ const newlyRequired = Array.from(newRequired).filter((p) => !oldRequired.has(p));
472
+ if (newlyRequired.length > 0) {
473
+ errors.push(`Added required properties: ${newlyRequired.join(', ')}`);
474
+ }
475
+ } else {
476
+ // Forward: cannot remove required properties
477
+ const removedRequired = Array.from(oldRequired).filter((p) => !newRequired.has(p));
478
+ if (removedRequired.length > 0) {
479
+ errors.push(`Removed required properties: ${removedRequired.join(', ')}`);
480
+ }
481
+ }
482
+
483
+ // Check properties that exist in both schemas
484
+ const commonProps = Object.keys(oldProps).filter((k) => k in newProps);
485
+ for (const prop of commonProps) {
486
+ const oldPropSchema = oldProps[prop] || {};
487
+ const newPropSchema = newProps[prop] || {};
488
+
489
+ // Check if type changed
490
+ const oldType = oldPropSchema.type;
491
+ const newType = newPropSchema.type;
492
+ if (oldType && newType && oldType !== newType) {
493
+ errors.push(`Property '${prop}' type changed from ${oldType} to ${newType}`);
494
+ }
495
+
496
+ // Check enum constraints
497
+ const oldEnum = oldPropSchema.enum || [];
498
+ const newEnum = newPropSchema.enum || [];
499
+ if (oldEnum.length > 0 && newEnum.length > 0) {
500
+ const oldEnumSet = new Set(oldEnum);
501
+ const newEnumSet = new Set(newEnum);
502
+ if (checkBackward) {
503
+ // Backward: cannot add enum values
504
+ const addedEnumValues = newEnum.filter((v: any) => !oldEnumSet.has(v));
505
+ if (addedEnumValues.length > 0) {
506
+ errors.push(`Property '${prop}' added enum values: ${addedEnumValues.join(', ')}`);
507
+ }
508
+ } else {
509
+ // Forward: cannot remove enum values
510
+ const removedEnumValues = oldEnum.filter((v: any) => !newEnumSet.has(v));
511
+ if (removedEnumValues.length > 0) {
512
+ errors.push(`Property '${prop}' removed enum values: ${removedEnumValues.join(', ')}`);
513
+ }
514
+ }
515
+ }
516
+
517
+ // Check constraint compatibility
518
+ errors.push(...this.checkConstraintCompatibility(prop, oldPropSchema, newPropSchema, checkBackward));
519
+
520
+ // Recursively check nested object properties
521
+ if (oldType === 'object' && newType === 'object') {
522
+ const nestedResult = this.checkSchemaCompatibility(oldPropSchema, newPropSchema, checkBackward);
523
+ const nestedErrors = checkBackward ? nestedResult.backwardErrors : nestedResult.forwardErrors;
524
+ if (nestedErrors) {
525
+ errors.push(...nestedErrors.map((e: string) => `Property '${prop}': ${e}`));
526
+ }
527
+ }
528
+
529
+ // Recursively check array item schemas
530
+ if (oldType === 'array' && newType === 'array' && oldPropSchema.items && newPropSchema.items) {
531
+ const itemsResult = this.checkSchemaCompatibility(oldPropSchema.items, newPropSchema.items, checkBackward);
532
+ const itemsErrors = checkBackward ? itemsResult.backwardErrors : itemsResult.forwardErrors;
533
+ if (itemsErrors) {
534
+ errors.push(...itemsErrors.map((e: string) => `Property '${prop}' array items: ${e}`));
535
+ }
536
+ }
537
+ }
538
+
539
+ if (checkBackward) {
540
+ return { isBackward: errors.length === 0, backwardErrors: errors };
541
+ } else {
542
+ return { isForward: errors.length === 0, forwardErrors: errors };
543
+ }
544
+ }
545
+
546
+ private checkConstraintCompatibility(
547
+ prop: string,
548
+ oldPropSchema: any,
549
+ newPropSchema: any,
550
+ checkTightening: boolean
551
+ ): string[] {
552
+ const errors: string[] = [];
553
+ const propType = oldPropSchema.type;
554
+
555
+ // Numeric constraints
556
+ if (propType === 'number' || propType === 'integer') {
557
+ errors.push(
558
+ ...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minimum', 'maximum', checkTightening)
559
+ );
560
+ }
561
+
562
+ // String constraints
563
+ if (propType === 'string') {
564
+ errors.push(
565
+ ...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minLength', 'maxLength', checkTightening)
566
+ );
567
+ }
568
+
569
+ // Array constraints
570
+ if (propType === 'array') {
571
+ errors.push(
572
+ ...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minItems', 'maxItems', checkTightening)
573
+ );
574
+ }
575
+
576
+ return errors;
577
+ }
578
+
579
+ private checkMinMaxConstraint(
580
+ prop: string,
581
+ oldSchema: any,
582
+ newSchema: any,
583
+ minKey: string,
584
+ maxKey: string,
585
+ checkTightening: boolean
586
+ ): string[] {
587
+ const errors: string[] = [];
588
+
589
+ const oldMin = oldSchema[minKey];
590
+ const newMin = newSchema[minKey];
591
+ const oldMax = oldSchema[maxKey];
592
+ const newMax = newSchema[maxKey];
593
+
594
+ // Check minimum constraint
595
+ if (checkTightening) {
596
+ // Backward: cannot increase minimum (tighten)
597
+ if (oldMin !== undefined && newMin !== undefined && newMin > oldMin) {
598
+ errors.push(`Property '${prop}' ${minKey} increased from ${oldMin} to ${newMin}`);
599
+ } else if (oldMin === undefined && newMin !== undefined) {
600
+ errors.push(`Property '${prop}' added ${minKey} constraint: ${newMin}`);
601
+ }
602
+ } else {
603
+ // Forward: cannot decrease minimum (relax)
604
+ if (oldMin !== undefined && newMin !== undefined && newMin < oldMin) {
605
+ errors.push(`Property '${prop}' ${minKey} decreased from ${oldMin} to ${newMin}`);
606
+ } else if (oldMin !== undefined && newMin === undefined) {
607
+ errors.push(`Property '${prop}' removed ${minKey} constraint`);
608
+ }
609
+ }
610
+
611
+ // Check maximum constraint
612
+ if (checkTightening) {
613
+ // Backward: cannot decrease maximum (tighten)
614
+ if (oldMax !== undefined && newMax !== undefined && newMax < oldMax) {
615
+ errors.push(`Property '${prop}' ${maxKey} decreased from ${oldMax} to ${newMax}`);
616
+ } else if (oldMax === undefined && newMax !== undefined) {
617
+ errors.push(`Property '${prop}' added ${maxKey} constraint: ${newMax}`);
618
+ }
619
+ } else {
620
+ // Forward: cannot increase maximum (relax)
621
+ if (oldMax !== undefined && newMax !== undefined && newMax > oldMax) {
622
+ errors.push(`Property '${prop}' ${maxKey} increased from ${oldMax} to ${newMax}`);
623
+ } else if (oldMax !== undefined && newMax === undefined) {
624
+ errors.push(`Property '${prop}' removed ${maxKey} constraint`);
625
+ }
626
+ }
627
+
628
+ return errors;
629
+ }
630
+
631
+ private flattenSchema(schema: any): any {
632
+ const result: any = {
633
+ properties: {},
634
+ required: [],
635
+ };
636
+
637
+ // Merge allOf schemas
638
+ if (schema.allOf && Array.isArray(schema.allOf)) {
639
+ for (const subSchema of schema.allOf) {
640
+ const flattened = this.flattenSchema(subSchema);
641
+
642
+ // Merge properties
643
+ Object.assign(result.properties, flattened.properties || {});
644
+
645
+ // Merge required
646
+ if (flattened.required && Array.isArray(flattened.required)) {
647
+ result.required.push(...flattened.required);
648
+ }
649
+
650
+ // Preserve additionalProperties
651
+ if (flattened.additionalProperties !== undefined) {
652
+ result.additionalProperties = flattened.additionalProperties;
653
+ }
654
+ }
655
+ }
656
+
657
+ // Add direct properties
658
+ if (schema.properties) {
659
+ Object.assign(result.properties, schema.properties);
660
+ }
661
+
662
+ // Add direct required
663
+ if (schema.required && Array.isArray(schema.required)) {
664
+ result.required.push(...schema.required);
665
+ }
666
+
667
+ // Top level additionalProperties overrides
668
+ if (schema.additionalProperties !== undefined) {
669
+ result.additionalProperties = schema.additionalProperties;
670
+ }
671
+
672
+ return result;
673
+ }
674
+
675
+ castInstance(instanceId: string, toSchemaId: string): any {
676
+ try {
677
+ // Get instance entity
678
+ const instanceEntity = this.get(instanceId);
679
+ if (!instanceEntity) {
680
+ return {
681
+ instance_id: instanceId,
682
+ to_schema_id: toSchemaId,
683
+ ok: false,
684
+ error: `Entity not found: ${instanceId}`,
685
+ };
686
+ }
687
+
688
+ // Get target schema
689
+ const toSchema = this.get(toSchemaId);
690
+ if (!toSchema) {
691
+ return {
692
+ instance_id: instanceId,
693
+ to_schema_id: toSchemaId,
694
+ ok: false,
695
+ error: `Schema not found: ${toSchemaId}`,
696
+ };
697
+ }
698
+
699
+ // Determine source schema
700
+ let fromSchemaId: string;
701
+ let fromSchema: any;
702
+ if (instanceEntity.isSchema) {
703
+ // Not allowed to cast directly from a schema
704
+ return {
705
+ instance_id: instanceId,
706
+ to_schema_id: toSchemaId,
707
+ ok: false,
708
+ error: 'Source must be an instance, not a schema',
709
+ };
710
+ } else {
711
+ // Casting an instance - need to find its schema
712
+ fromSchemaId = instanceEntity.schemaId!;
713
+ if (!fromSchemaId) {
714
+ return {
715
+ instance_id: instanceId,
716
+ to_schema_id: toSchemaId,
717
+ ok: false,
718
+ error: `Schema not found for instance: ${instanceId}`,
719
+ };
720
+ }
721
+ // Don't try to get a JSON Schema URL as a GTS entity
722
+ if (fromSchemaId.startsWith('http://') || fromSchemaId.startsWith('https://')) {
723
+ return {
724
+ instance_id: instanceId,
725
+ to_schema_id: toSchemaId,
726
+ ok: false,
727
+ error: `Cannot cast instance with schema ${fromSchemaId}`,
728
+ };
729
+ }
730
+ fromSchema = this.get(fromSchemaId);
731
+ if (!fromSchema) {
732
+ return {
733
+ instance_id: instanceId,
734
+ to_schema_id: toSchemaId,
735
+ ok: false,
736
+ error: `Schema not found: ${fromSchemaId}`,
737
+ };
738
+ }
739
+ }
740
+
741
+ // Get content
742
+ const instanceContent = instanceEntity.content;
743
+ const fromSchemaContent = fromSchema.content;
744
+ const toSchemaContent = toSchema.content;
745
+
746
+ // Perform the cast
747
+ return this.performCast(instanceId, toSchemaId, instanceContent, fromSchemaContent, toSchemaContent);
748
+ } catch (error) {
749
+ return {
750
+ instance_id: instanceId,
751
+ to_schema_id: toSchemaId,
752
+ ok: false,
753
+ error: error instanceof Error ? error.message : String(error),
754
+ };
755
+ }
756
+ }
757
+
758
+ private performCast(
759
+ fromInstanceId: string,
760
+ toSchemaId: string,
761
+ fromInstanceContent: any,
762
+ fromSchemaContent: any,
763
+ toSchemaContent: any
764
+ ): any {
765
+ // Flatten target schema to merge allOf
766
+ const targetSchema = this.flattenSchema(toSchemaContent);
767
+
768
+ // Determine direction
769
+ const direction = this.inferDirection(fromInstanceId, toSchemaId);
770
+
771
+ // Determine which is old/new based on direction
772
+ let oldSchema: any;
773
+ let newSchema: any;
774
+ switch (direction) {
775
+ case 'up':
776
+ oldSchema = fromSchemaContent;
777
+ newSchema = toSchemaContent;
778
+ break;
779
+ case 'down':
780
+ oldSchema = toSchemaContent;
781
+ newSchema = fromSchemaContent;
782
+ break;
783
+ default:
784
+ oldSchema = fromSchemaContent;
785
+ newSchema = toSchemaContent;
786
+ break;
787
+ }
788
+
789
+ // Check compatibility
790
+ const { isBackward, backwardErrors } = this.checkBackwardCompatibility(oldSchema, newSchema);
791
+ const { isForward, forwardErrors } = this.checkForwardCompatibility(oldSchema, newSchema);
792
+
793
+ // Apply casting rules to transform the instance
794
+ const { casted, added, removed, incompatibilityReasons } = this.castInstanceToSchema(
795
+ this.deepCopy(fromInstanceContent),
796
+ targetSchema,
797
+ ''
798
+ );
799
+
800
+ // Validate the casted instance against the target schema
801
+ let isFullyCompatible = false;
802
+ if (casted) {
803
+ try {
804
+ const modifiedSchema = this.removeGtsConstConstraints(toSchemaContent);
805
+ const validate = this.ajv.compile(this.normalizeSchema(modifiedSchema));
806
+ const isValid = validate(casted);
807
+ if (!isValid) {
808
+ const errors =
809
+ validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join('; ') || 'Validation failed';
810
+ incompatibilityReasons.push(errors);
811
+ } else {
812
+ isFullyCompatible = true;
813
+ }
814
+ } catch (err) {
815
+ incompatibilityReasons.push(err instanceof Error ? err.message : String(err));
816
+ }
817
+ }
818
+
819
+ return {
820
+ from: fromInstanceId,
821
+ to: toSchemaId,
822
+ old: fromInstanceId,
823
+ new: toSchemaId,
824
+ direction,
825
+ added_properties: this.deduplicate(added),
826
+ removed_properties: this.deduplicate(removed),
827
+ changed_properties: [],
828
+ is_fully_compatible: isFullyCompatible,
829
+ is_backward_compatible: isBackward,
830
+ is_forward_compatible: isForward,
831
+ incompatibility_reasons: incompatibilityReasons,
832
+ backward_errors: backwardErrors,
833
+ forward_errors: forwardErrors,
834
+ casted_entity: casted,
835
+ instance_id: fromInstanceId,
836
+ to_schema_id: toSchemaId,
837
+ ok: isFullyCompatible,
838
+ error: isFullyCompatible ? '' : incompatibilityReasons.join('; '),
839
+ };
840
+ }
841
+
842
+ private castInstanceToSchema(
843
+ instance: any,
844
+ schema: any,
845
+ basePath: string
846
+ ): { casted: any; added: string[]; removed: string[]; incompatibilityReasons: string[] } {
847
+ const added: string[] = [];
848
+ const removed: string[] = [];
849
+ const incompatibilityReasons: string[] = [];
850
+
851
+ if (!instance || typeof instance !== 'object' || Array.isArray(instance)) {
852
+ incompatibilityReasons.push('Instance must be an object for casting');
853
+ return { casted: null, added, removed, incompatibilityReasons };
854
+ }
855
+
856
+ const targetProps = schema.properties || {};
857
+ const required = new Set<string>(schema.required || []);
858
+ const additional = schema.additionalProperties !== false;
859
+
860
+ // Start from current values
861
+ const result = this.deepCopy(instance);
862
+
863
+ // 1) Ensure required properties exist (fill defaults if provided)
864
+ for (const reqProp of Array.from(required)) {
865
+ if (!(reqProp in result)) {
866
+ const propSchema = targetProps[reqProp as string];
867
+ if (propSchema && propSchema.default !== undefined) {
868
+ result[reqProp as string] = this.deepCopy(propSchema.default);
869
+ const path = this.buildPath(basePath, reqProp as string);
870
+ added.push(path);
871
+ } else {
872
+ const path = this.buildPath(basePath, reqProp as string);
873
+ incompatibilityReasons.push(`Missing required property '${path}' and no default is defined`);
874
+ }
875
+ }
876
+ }
877
+
878
+ // 2) For optional properties with defaults, set if missing
879
+ for (const [prop, propSchema] of Object.entries(targetProps)) {
880
+ if (required.has(prop)) {
881
+ continue;
882
+ }
883
+ if (!(prop in result)) {
884
+ const ps = propSchema as any;
885
+ if (ps.default !== undefined) {
886
+ result[prop] = this.deepCopy(ps.default);
887
+ const path = this.buildPath(basePath, prop);
888
+ added.push(path);
889
+ }
890
+ }
891
+ }
892
+
893
+ // 2.5) Update const values to match target schema (for GTS ID fields)
894
+ for (const [prop, propSchema] of Object.entries(targetProps)) {
895
+ const ps = propSchema as any;
896
+ if (ps.const !== undefined) {
897
+ const constVal = ps.const;
898
+ const existingVal = result[prop];
899
+ if (typeof constVal === 'string' && typeof existingVal === 'string') {
900
+ // Only update if both are GTS IDs and they differ
901
+ if (Gts.isValidGtsID(constVal) && Gts.isValidGtsID(existingVal)) {
902
+ if (existingVal !== constVal) {
903
+ result[prop] = constVal;
904
+ }
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ // 3) Remove properties not in target schema when additionalProperties is false
911
+ if (!additional) {
912
+ for (const prop of Object.keys(result)) {
913
+ if (!(prop in targetProps)) {
914
+ delete result[prop];
915
+ const path = this.buildPath(basePath, prop);
916
+ removed.push(path);
917
+ }
918
+ }
919
+ }
920
+
921
+ // 4) Recurse into nested object properties
922
+ for (const [prop, propSchema] of Object.entries(targetProps)) {
923
+ const val = result[prop];
924
+ if (val === undefined) {
925
+ continue;
926
+ }
927
+ const ps = propSchema as any;
928
+ const propType = ps.type;
929
+
930
+ // Handle nested objects
931
+ if (propType === 'object') {
932
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
933
+ const nestedSchema = this.effectiveObjectSchema(ps);
934
+ const nestedResult = this.castInstanceToSchema(val, nestedSchema, this.buildPath(basePath, prop));
935
+ result[prop] = nestedResult.casted;
936
+ added.push(...nestedResult.added);
937
+ removed.push(...nestedResult.removed);
938
+ incompatibilityReasons.push(...nestedResult.incompatibilityReasons);
939
+ }
940
+ }
941
+
942
+ // Handle arrays of objects
943
+ if (propType === 'array') {
944
+ if (Array.isArray(val)) {
945
+ const itemsSchema = ps.items;
946
+ if (itemsSchema && itemsSchema.type === 'object') {
947
+ const nestedSchema = this.effectiveObjectSchema(itemsSchema);
948
+ const newList: any[] = [];
949
+ for (let idx = 0; idx < val.length; idx++) {
950
+ const item = val[idx];
951
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
952
+ const nestedResult = this.castInstanceToSchema(
953
+ item,
954
+ nestedSchema,
955
+ this.buildPath(basePath, `${prop}[${idx}]`)
956
+ );
957
+ newList.push(nestedResult.casted);
958
+ added.push(...nestedResult.added);
959
+ removed.push(...nestedResult.removed);
960
+ incompatibilityReasons.push(...nestedResult.incompatibilityReasons);
961
+ } else {
962
+ newList.push(item);
963
+ }
964
+ }
965
+ result[prop] = newList;
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ return { casted: result, added, removed, incompatibilityReasons };
972
+ }
973
+
974
+ private effectiveObjectSchema(schema: any): any {
975
+ if (!schema) {
976
+ return {};
977
+ }
978
+
979
+ // If it has properties or required directly, use it
980
+ if (schema.properties || schema.required) {
981
+ return schema;
982
+ }
983
+
984
+ // Check allOf for object schemas
985
+ if (schema.allOf && Array.isArray(schema.allOf)) {
986
+ for (const part of schema.allOf) {
987
+ if (part.properties || part.required) {
988
+ return part;
989
+ }
990
+ }
991
+ }
992
+
993
+ return schema;
994
+ }
995
+
996
+ private removeGtsConstConstraints(schema: any): any {
997
+ if (schema === null || schema === undefined) {
998
+ return schema;
999
+ }
1000
+
1001
+ if (typeof schema === 'object' && !Array.isArray(schema)) {
1002
+ const result: any = {};
1003
+ for (const [key, value] of Object.entries(schema)) {
1004
+ if (key === 'const') {
1005
+ if (typeof value === 'string' && Gts.isValidGtsID(value)) {
1006
+ // Replace const with type constraint instead
1007
+ result.type = 'string';
1008
+ continue;
1009
+ }
1010
+ }
1011
+ result[key] = this.removeGtsConstConstraints(value);
1012
+ }
1013
+ return result;
1014
+ }
1015
+
1016
+ if (Array.isArray(schema)) {
1017
+ return schema.map((item) => this.removeGtsConstConstraints(item));
1018
+ }
1019
+
1020
+ return schema;
1021
+ }
1022
+
1023
+ private buildPath(base: string, prop: string): string {
1024
+ if (!base) {
1025
+ return prop;
1026
+ }
1027
+ // Handle array indices that already have brackets
1028
+ if (prop.startsWith('[')) {
1029
+ return base + prop;
1030
+ }
1031
+ return base + '.' + prop;
1032
+ }
1033
+
1034
+ private deepCopy(obj: any): any {
1035
+ if (obj === null || obj === undefined) {
1036
+ return obj;
1037
+ }
1038
+ if (typeof obj !== 'object') {
1039
+ return obj;
1040
+ }
1041
+ if (Array.isArray(obj)) {
1042
+ return obj.map((item) => this.deepCopy(item));
1043
+ }
1044
+ const result: any = {};
1045
+ for (const [key, value] of Object.entries(obj)) {
1046
+ result[key] = this.deepCopy(value);
1047
+ }
1048
+ return result;
1049
+ }
1050
+
1051
+ private deduplicate(arr: string[]): string[] {
1052
+ const unique = Array.from(new Set(arr));
1053
+ return unique.sort();
1054
+ }
1055
+
1056
+ getAttribute(gtsId: string, path: string): any {
1057
+ const entity = this.get(gtsId);
1058
+ if (!entity) {
1059
+ return {
1060
+ gts_id: gtsId,
1061
+ path,
1062
+ resolved: false,
1063
+ error: `Entity not found: ${gtsId}`,
1064
+ };
1065
+ }
1066
+
1067
+ const value = this.getNestedValue(entity.content, path);
1068
+
1069
+ return {
1070
+ gts_id: gtsId,
1071
+ path,
1072
+ resolved: value !== undefined,
1073
+ value,
1074
+ };
1075
+ }
1076
+
1077
+ private getNestedValue(obj: any, path: string): any {
1078
+ // Split path by dots but handle array notation
1079
+ const parts: string[] = [];
1080
+ let current = '';
1081
+ let inBracket = false;
1082
+
1083
+ for (let i = 0; i < path.length; i++) {
1084
+ const char = path[i];
1085
+ if (char === '[') {
1086
+ if (current) {
1087
+ parts.push(current);
1088
+ current = '';
1089
+ }
1090
+ inBracket = true;
1091
+ } else if (char === ']') {
1092
+ if (current) {
1093
+ parts.push(`[${current}]`);
1094
+ current = '';
1095
+ }
1096
+ inBracket = false;
1097
+ } else if (char === '.' && !inBracket) {
1098
+ if (current) {
1099
+ parts.push(current);
1100
+ current = '';
1101
+ }
1102
+ } else {
1103
+ current += char;
1104
+ }
1105
+ }
1106
+ if (current) {
1107
+ parts.push(current);
1108
+ }
1109
+
1110
+ let result = obj;
1111
+ for (const part of parts) {
1112
+ if (result === null || result === undefined) {
1113
+ return undefined;
1114
+ }
1115
+
1116
+ // Handle array index notation
1117
+ if (part.startsWith('[') && part.endsWith(']')) {
1118
+ const index = parseInt(part.slice(1, -1), 10);
1119
+ if (Array.isArray(result) && !isNaN(index)) {
1120
+ result = result[index];
1121
+ } else {
1122
+ return undefined;
1123
+ }
1124
+ } else {
1125
+ // Regular property access
1126
+ if (typeof result === 'object' && part in result) {
1127
+ result = result[part];
1128
+ } else {
1129
+ return undefined;
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ return result;
1135
+ }
1136
+ }
1137
+
1138
+ export function createJsonEntity(content: any, _config?: Partial<GtsConfig>): JsonEntity {
1139
+ const extractResult = GtsExtractor.extractID(content);
1140
+
1141
+ const references = new Set<string>();
1142
+ findReferences(content, references);
1143
+
1144
+ return {
1145
+ id: extractResult.id,
1146
+ schemaId: extractResult.schema_id,
1147
+ content,
1148
+ isSchema: extractResult.is_schema,
1149
+ references,
1150
+ };
1151
+ }
1152
+
1153
+ function findReferences(obj: any, refs: Set<string>, visited = new Set()): void {
1154
+ if (!obj || typeof obj !== 'object' || visited.has(obj)) {
1155
+ return;
1156
+ }
1157
+
1158
+ visited.add(obj);
1159
+
1160
+ if ('$ref' in obj && typeof obj['$ref'] === 'string') {
1161
+ const ref = obj['$ref'];
1162
+ const normalized = ref.startsWith(GTS_URI_PREFIX) ? ref.substring(GTS_URI_PREFIX.length) : ref;
1163
+ if (Gts.isValidGtsID(normalized)) {
1164
+ refs.add(normalized);
1165
+ }
1166
+ }
1167
+
1168
+ if ('x-gts-ref' in obj && typeof obj['x-gts-ref'] === 'string') {
1169
+ const ref = obj['x-gts-ref'];
1170
+ if (Gts.isValidGtsID(ref)) {
1171
+ refs.add(ref);
1172
+ }
1173
+ }
1174
+
1175
+ for (const value of Object.values(obj)) {
1176
+ findReferences(value, refs, visited);
1177
+ }
1178
+ }