@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/cast.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { CastResult } from './types';
2
+ import { GtsStore } from './store';
3
+ import { Gts } from './gts';
4
+ import { GtsCompatibility } from './compatibility';
5
+
6
+ export class GtsCast {
7
+ static castInstance(store: GtsStore, fromId: string, toSchemaId: string): CastResult {
8
+ try {
9
+ const fromGtsId = Gts.parseGtsID(fromId);
10
+ const fromEntity = store.get(fromGtsId.id);
11
+
12
+ if (!fromEntity) {
13
+ return {
14
+ ok: false,
15
+ fromId,
16
+ toId: toSchemaId,
17
+ error: `Instance not found: ${fromId}`,
18
+ };
19
+ }
20
+
21
+ if (!fromEntity.schemaId) {
22
+ return {
23
+ ok: false,
24
+ fromId,
25
+ toId: toSchemaId,
26
+ error: `No schema found for instance: ${fromId}`,
27
+ };
28
+ }
29
+
30
+ const toGtsId = Gts.parseGtsID(toSchemaId);
31
+ const toSchema = store.get(toGtsId.id);
32
+
33
+ if (!toSchema) {
34
+ return {
35
+ ok: false,
36
+ fromId,
37
+ toId: toSchemaId,
38
+ error: `Target schema not found: ${toSchemaId}`,
39
+ };
40
+ }
41
+
42
+ if (!toSchema.isSchema) {
43
+ return {
44
+ ok: false,
45
+ fromId,
46
+ toId: toSchemaId,
47
+ error: `Target is not a schema: ${toSchemaId}`,
48
+ };
49
+ }
50
+
51
+ const fromSchemaEntity = store.get(fromEntity.schemaId);
52
+ if (!fromSchemaEntity) {
53
+ return {
54
+ ok: false,
55
+ fromId,
56
+ toId: toSchemaId,
57
+ error: `Source schema not found: ${fromEntity.schemaId}`,
58
+ };
59
+ }
60
+
61
+ const compatCheck = GtsCompatibility.checkCompatibility(store, fromEntity.schemaId, toSchemaId, 'full');
62
+
63
+ if (!compatCheck.compatible) {
64
+ return {
65
+ ok: false,
66
+ fromId,
67
+ toId: toSchemaId,
68
+ error: `Schemas are not compatible: ${compatCheck.errors.join('; ')}`,
69
+ };
70
+ }
71
+
72
+ const castedInstance = this.performCast(
73
+ fromEntity.content,
74
+ fromSchemaEntity.content,
75
+ toSchema.content,
76
+ toSchemaId
77
+ );
78
+
79
+ return {
80
+ ok: true,
81
+ fromId,
82
+ toId: toSchemaId,
83
+ result: castedInstance,
84
+ };
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ fromId,
89
+ toId: toSchemaId,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ };
92
+ }
93
+ }
94
+
95
+ private static performCast(instance: any, fromSchema: any, toSchema: any, toSchemaId: string): any {
96
+ const result: any = { ...instance };
97
+
98
+ const fromSegments = Gts.parseID(fromSchema['$$id'] || fromSchema['$id']).segments;
99
+ const toSegments = Gts.parseID(toSchemaId).segments;
100
+
101
+ if (fromSegments.length > 0 && toSegments.length > 0) {
102
+ const fromVersion = `v${fromSegments[0].verMajor}.${fromSegments[0].verMinor ?? 0}`;
103
+ const toVersion = `v${toSegments[0].verMajor}.${toSegments[0].verMinor ?? 0}`;
104
+
105
+ if ('gtsId' in result) {
106
+ result.gtsId = result.gtsId.replace(fromVersion, toVersion);
107
+ }
108
+ }
109
+
110
+ if ('$schema' in result || '$$schema' in result) {
111
+ result['$schema'] = toSchemaId;
112
+ if ('$$schema' in result) {
113
+ result['$$schema'] = toSchemaId;
114
+ }
115
+ }
116
+
117
+ const toProps = toSchema.properties || {};
118
+ const toRequired = new Set(toSchema.required || []);
119
+
120
+ const filtered: any = {};
121
+ for (const [key, value] of Object.entries(result)) {
122
+ if (key in toProps || key === 'gtsId' || key === '$schema' || key === '$$schema') {
123
+ filtered[key] = value;
124
+ }
125
+ }
126
+
127
+ for (const prop of toRequired) {
128
+ if (!((prop as string) in filtered)) {
129
+ const propSchema = toProps[prop as string];
130
+ if (propSchema) {
131
+ filtered[prop as string] = this.getDefaultValue(propSchema);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Also add properties with default values that aren't required
137
+ for (const [propName, propSchema] of Object.entries(toProps)) {
138
+ if (!(propName in filtered) && propSchema && typeof propSchema === 'object' && 'default' in propSchema) {
139
+ filtered[propName] = propSchema.default;
140
+ }
141
+ }
142
+
143
+ return filtered;
144
+ }
145
+
146
+ private static getDefaultValue(schema: any): any {
147
+ if ('default' in schema) {
148
+ return schema.default;
149
+ }
150
+
151
+ const type = schema.type;
152
+ if (Array.isArray(type)) {
153
+ if (type.includes('null')) {
154
+ return null;
155
+ }
156
+ return this.getDefaultForType(type[0]);
157
+ }
158
+
159
+ return this.getDefaultForType(type);
160
+ }
161
+
162
+ private static getDefaultForType(type: string): any {
163
+ switch (type) {
164
+ case 'string':
165
+ return '';
166
+ case 'number':
167
+ case 'integer':
168
+ return 0;
169
+ case 'boolean':
170
+ return false;
171
+ case 'array':
172
+ return [];
173
+ case 'object':
174
+ return {};
175
+ default:
176
+ return null;
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { validateGtsID, parseGtsID, matchIDPattern, idToUUID, extractID, GTS, createJsonEntity } from '../index';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('gts')
11
+ .description('GTS CLI - Global Type System command-line interface')
12
+ .version('0.1.0')
13
+ .option('--path <path>', 'Path to JSON and schema files', process.env.GTS_PATH)
14
+ .option('--config <config>', 'Path to GTS config JSON file', process.env.GTS_CONFIG)
15
+ .option('-v, --verbose', 'Verbose output', false);
16
+
17
+ // OP#1 - Validate ID
18
+ program
19
+ .command('validate-id')
20
+ .description('Validate a GTS ID')
21
+ .requiredOption('-i, --id <id>', 'GTS ID to validate')
22
+ .action((options) => {
23
+ const result = validateGtsID(options.id);
24
+ console.log(JSON.stringify(result, null, 2));
25
+ process.exit(result.ok ? 0 : 1);
26
+ });
27
+
28
+ // OP#2 - Extract ID
29
+ program
30
+ .command('extract-id')
31
+ .description('Extract GTS ID from JSON content')
32
+ .requiredOption('-f, --file <file>', 'JSON file to extract ID from')
33
+ .action((options) => {
34
+ try {
35
+ const content = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
36
+ const result = extractID(content);
37
+ console.log(JSON.stringify(result, null, 2));
38
+ } catch (error) {
39
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
40
+ process.exit(1);
41
+ }
42
+ });
43
+
44
+ // OP#3 - Parse ID
45
+ program
46
+ .command('parse-id')
47
+ .description('Parse a GTS ID into components')
48
+ .requiredOption('-i, --id <id>', 'GTS ID to parse')
49
+ .action((options) => {
50
+ const result = parseGtsID(options.id);
51
+ console.log(JSON.stringify(result, null, 2));
52
+ process.exit(result.ok ? 0 : 1);
53
+ });
54
+
55
+ // OP#4 - Match ID
56
+ program
57
+ .command('match-id')
58
+ .description('Match GTS ID against pattern')
59
+ .requiredOption('-p, --pattern <pattern>', 'Pattern to match against')
60
+ .requiredOption('-c, --candidate <candidate>', 'Candidate ID to match')
61
+ .action((options) => {
62
+ const result = matchIDPattern(options.candidate, options.pattern);
63
+ console.log(JSON.stringify(result, null, 2));
64
+ process.exit(result.match ? 0 : 1);
65
+ });
66
+
67
+ // OP#5 - UUID
68
+ program
69
+ .command('uuid')
70
+ .description('Generate UUID from GTS ID')
71
+ .requiredOption('-i, --id <id>', 'GTS ID to convert')
72
+ .action((options) => {
73
+ const result = idToUUID(options.id);
74
+ if (result.error) {
75
+ console.error(`Error: ${result.error}`);
76
+ process.exit(1);
77
+ }
78
+ console.log(result.uuid);
79
+ });
80
+
81
+ // OP#6 - Validate Instance
82
+ program
83
+ .command('validate')
84
+ .description('Validate instance against schema')
85
+ .requiredOption('-i, --id <id>', 'Instance ID to validate')
86
+ .action((options, command) => {
87
+ const gts = loadStore(command.parent);
88
+ const result = gts.validateInstance(options.id);
89
+ console.log(JSON.stringify(result, null, 2));
90
+ process.exit(result.ok ? 0 : 1);
91
+ });
92
+
93
+ // OP#7 - Relationships
94
+ program
95
+ .command('relationships')
96
+ .description('Resolve relationships for an entity')
97
+ .requiredOption('-i, --id <id>', 'Entity ID')
98
+ .action((options, command) => {
99
+ const gts = loadStore(command.parent);
100
+ const result = gts.resolveRelationships(options.id);
101
+ console.log(JSON.stringify(result, null, 2));
102
+ });
103
+
104
+ // OP#8 - Compatibility
105
+ program
106
+ .command('compatibility')
107
+ .description('Check schema compatibility')
108
+ .requiredOption('-o, --old <old>', 'Old schema ID')
109
+ .requiredOption('-n, --new <new>', 'New schema ID')
110
+ .option('-m, --mode <mode>', 'Compatibility mode (backward|forward|full)', 'full')
111
+ .action((options, command) => {
112
+ const gts = loadStore(command.parent);
113
+ const result = gts.checkCompatibility(options.old, options.new, options.mode);
114
+ console.log(JSON.stringify(result, null, 2));
115
+ process.exit(result.compatible ? 0 : 1);
116
+ });
117
+
118
+ // OP#9 - Cast
119
+ program
120
+ .command('cast')
121
+ .description('Cast instance to different schema version')
122
+ .requiredOption('-f, --from <from>', 'Source instance ID')
123
+ .requiredOption('-t, --to <to>', 'Target schema ID')
124
+ .action((options, command) => {
125
+ const gts = loadStore(command.parent);
126
+ const result = gts.castInstance(options.from, options.to);
127
+ console.log(JSON.stringify(result, null, 2));
128
+ process.exit(result.ok ? 0 : 1);
129
+ });
130
+
131
+ // OP#10 - Query
132
+ program
133
+ .command('query')
134
+ .description('Query entities')
135
+ .requiredOption('-e, --expr <expr>', 'Query expression')
136
+ .option('-l, --limit <limit>', 'Maximum results', '100')
137
+ .action((options, command) => {
138
+ const gts = loadStore(command.parent);
139
+ const result = gts.query(options.expr, parseInt(options.limit, 10));
140
+ console.log(JSON.stringify(result, null, 2));
141
+ });
142
+
143
+ // OP#11 - Attribute
144
+ program
145
+ .command('attr')
146
+ .description('Get attribute value')
147
+ .requiredOption('-p, --path <path>', 'Attribute path (e.g., gts.vendor.pkg.ns.type.v1.0@name)')
148
+ .action((options, command) => {
149
+ const gts = loadStore(command.parent);
150
+ const result = gts.getAttribute(options.path);
151
+ console.log(JSON.stringify(result, null, 2));
152
+ process.exit(result.resolved ? 0 : 1);
153
+ });
154
+
155
+ // List entities
156
+ program
157
+ .command('list')
158
+ .description('List all entities')
159
+ .option('-l, --limit <limit>', 'Maximum results', '100')
160
+ .action((options, command) => {
161
+ const gts = loadStore(command.parent);
162
+ const entities = gts['store'].getAll().slice(0, parseInt(options.limit, 10));
163
+ const result = {
164
+ count: entities.length,
165
+ items: entities.map((e) => e.id),
166
+ };
167
+ console.log(JSON.stringify(result, null, 2));
168
+ });
169
+
170
+ // Server command
171
+ program
172
+ .command('server')
173
+ .description('Start HTTP server')
174
+ .option('--host <host>', 'Host to bind to', '127.0.0.1')
175
+ .option('--port <port>', 'Port to listen on', '8000')
176
+ .action(async (options, command) => {
177
+ const parentOpts = command.parent.opts();
178
+
179
+ // Import server dynamically
180
+ const { GtsServer } = await import('../server/server');
181
+
182
+ const config = {
183
+ host: options.host,
184
+ port: parseInt(options.port, 10),
185
+ verbose: parentOpts.verbose ? 2 : 1,
186
+ path: parentOpts.path,
187
+ };
188
+
189
+ const server = new GtsServer(config);
190
+
191
+ // Load entities if path provided
192
+ if (config.path) {
193
+ const gts = loadStore(command.parent);
194
+ // Transfer loaded entities to server
195
+ const entities = gts['store'].getAll();
196
+ for (const entity of entities) {
197
+ server['store'].register(entity.content);
198
+ }
199
+ console.log(`Loaded ${entities.length} entities from ${config.path}`);
200
+ }
201
+
202
+ await server.start();
203
+ });
204
+
205
+ // OpenAPI command
206
+ program
207
+ .command('openapi')
208
+ .description('Generate OpenAPI specification')
209
+ .option('-o, --out <file>', 'Output file')
210
+ .action((options) => {
211
+ const spec = {
212
+ openapi: '3.0.0',
213
+ info: {
214
+ title: 'GTS API',
215
+ version: '0.1.0',
216
+ description: 'Global Type System API',
217
+ },
218
+ servers: [
219
+ {
220
+ url: 'http://127.0.0.1:8000',
221
+ description: 'Default GTS Server',
222
+ },
223
+ ],
224
+ paths: {},
225
+ components: {},
226
+ };
227
+
228
+ const output = JSON.stringify(spec, null, 2);
229
+
230
+ if (options.out) {
231
+ fs.writeFileSync(options.out, output);
232
+ console.log(`OpenAPI spec written to ${options.out}`);
233
+ } else {
234
+ console.log(output);
235
+ }
236
+ });
237
+
238
+ // Helper function to load entities from path
239
+ function loadStore(command: any): GTS {
240
+ const options = command.opts();
241
+ const gts = new GTS({ validateRefs: false });
242
+
243
+ if (!options.path) {
244
+ return gts;
245
+ }
246
+
247
+ const paths = options.path.split(',');
248
+
249
+ for (const dirPath of paths) {
250
+ if (!fs.existsSync(dirPath)) {
251
+ if (options.verbose) {
252
+ console.warn(`Path does not exist: ${dirPath}`);
253
+ }
254
+ continue;
255
+ }
256
+
257
+ const stats = fs.statSync(dirPath);
258
+ if (!stats.isDirectory()) {
259
+ if (options.verbose) {
260
+ console.warn(`Path is not a directory: ${dirPath}`);
261
+ }
262
+ continue;
263
+ }
264
+
265
+ loadEntitiesFromDir(gts, dirPath, options.verbose);
266
+ }
267
+
268
+ return gts;
269
+ }
270
+
271
+ function loadEntitiesFromDir(gts: GTS, dirPath: string, verbose: boolean): void {
272
+ const files = fs.readdirSync(dirPath);
273
+
274
+ for (const file of files) {
275
+ if (!file.endsWith('.json')) continue;
276
+
277
+ const filePath = path.join(dirPath, file);
278
+
279
+ try {
280
+ const content = fs.readFileSync(filePath, 'utf-8');
281
+ const data = JSON.parse(content);
282
+
283
+ // Handle arrays of entities
284
+ const entities = Array.isArray(data) ? data : [data];
285
+
286
+ for (const entity of entities) {
287
+ try {
288
+ const gtsEntity = createJsonEntity(entity);
289
+ if (gtsEntity.id) {
290
+ gts.register(entity);
291
+ if (verbose) {
292
+ console.log(`Loaded: ${gtsEntity.id}`);
293
+ }
294
+ }
295
+ } catch (err) {
296
+ if (verbose) {
297
+ console.warn(`Failed to load entity from ${file}: ${err}`);
298
+ }
299
+ }
300
+ }
301
+ } catch (err) {
302
+ if (verbose) {
303
+ console.warn(`Failed to read ${file}: ${err}`);
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ // Parse arguments
310
+ program.parse(process.argv);
311
+
312
+ // Show help if no command provided
313
+ if (!process.argv.slice(2).length) {
314
+ program.outputHelp();
315
+ }
@@ -0,0 +1,201 @@
1
+ import { CompatibilityResult } from './types';
2
+ import { GtsStore } from './store';
3
+ import { Gts } from './gts';
4
+
5
+ export class GtsCompatibility {
6
+ static checkCompatibility(
7
+ store: GtsStore,
8
+ oldId: string,
9
+ newId: string,
10
+ mode: 'backward' | 'forward' | 'full' = 'full'
11
+ ): CompatibilityResult {
12
+ const errors: string[] = [];
13
+ const warnings: string[] = [];
14
+
15
+ try {
16
+ const oldGtsId = Gts.parseGtsID(oldId);
17
+ const newGtsId = Gts.parseGtsID(newId);
18
+
19
+ const oldEntity = store.get(oldGtsId.id);
20
+ const newEntity = store.get(newGtsId.id);
21
+
22
+ if (!oldEntity) {
23
+ errors.push(`Old schema not found: ${oldId}`);
24
+ return { compatible: false, oldId, newId, mode, errors, warnings };
25
+ }
26
+
27
+ if (!newEntity) {
28
+ errors.push(`New schema not found: ${newId}`);
29
+ return { compatible: false, oldId, newId, mode, errors, warnings };
30
+ }
31
+
32
+ if (!oldEntity.isSchema) {
33
+ errors.push(`Old entity is not a schema: ${oldId}`);
34
+ return { compatible: false, oldId, newId, mode, errors, warnings };
35
+ }
36
+
37
+ if (!newEntity.isSchema) {
38
+ errors.push(`New entity is not a schema: ${newId}`);
39
+ return { compatible: false, oldId, newId, mode, errors, warnings };
40
+ }
41
+
42
+ const oldSchema = oldEntity.content;
43
+ const newSchema = newEntity.content;
44
+
45
+ if (mode === 'backward' || mode === 'full') {
46
+ this.checkBackwardCompatibility(oldSchema, newSchema, errors, warnings);
47
+ }
48
+
49
+ if (mode === 'forward' || mode === 'full') {
50
+ this.checkForwardCompatibility(oldSchema, newSchema, errors, warnings);
51
+ }
52
+
53
+ return {
54
+ compatible: errors.length === 0,
55
+ oldId,
56
+ newId,
57
+ mode,
58
+ errors,
59
+ warnings,
60
+ };
61
+ } catch (error) {
62
+ errors.push(error instanceof Error ? error.message : String(error));
63
+ return {
64
+ compatible: false,
65
+ oldId,
66
+ newId,
67
+ mode,
68
+ errors,
69
+ warnings,
70
+ };
71
+ }
72
+ }
73
+
74
+ private static checkBackwardCompatibility(
75
+ oldSchema: any,
76
+ newSchema: any,
77
+ errors: string[],
78
+ warnings: string[]
79
+ ): void {
80
+ const oldProps = oldSchema.properties || {};
81
+ const newProps = newSchema.properties || {};
82
+ const oldRequired = new Set(oldSchema.required || []);
83
+ const newRequired = new Set(newSchema.required || []);
84
+
85
+ for (const prop of oldRequired) {
86
+ if (!newRequired.has(prop)) {
87
+ warnings.push(`Property '${prop}' is no longer required in new schema`);
88
+ }
89
+ }
90
+
91
+ for (const [propName, propSchema] of Object.entries(oldProps)) {
92
+ if (!(propName in newProps)) {
93
+ if (oldRequired.has(propName)) {
94
+ errors.push(`Required property '${propName}' removed in new schema`);
95
+ } else {
96
+ warnings.push(`Optional property '${propName}' removed in new schema`);
97
+ }
98
+ } else {
99
+ this.checkPropertyCompatibility(propName, propSchema, newProps[propName], errors, warnings, 'backward');
100
+ }
101
+ }
102
+ }
103
+
104
+ private static checkForwardCompatibility(oldSchema: any, newSchema: any, errors: string[], warnings: string[]): void {
105
+ const oldProps = oldSchema.properties || {};
106
+ const newProps = newSchema.properties || {};
107
+ const oldRequired = new Set(oldSchema.required || []);
108
+ const newRequired = new Set(newSchema.required || []);
109
+
110
+ for (const prop of newRequired) {
111
+ if (!oldRequired.has(prop)) {
112
+ errors.push(`New required property '${prop}' added in new schema`);
113
+ }
114
+ }
115
+
116
+ for (const [propName, propSchema] of Object.entries(newProps)) {
117
+ if (!(propName in oldProps)) {
118
+ if (newRequired.has(propName)) {
119
+ errors.push(`New required property '${propName}' added`);
120
+ } else {
121
+ warnings.push(`New optional property '${propName}' added`);
122
+ }
123
+ } else {
124
+ this.checkPropertyCompatibility(propName, oldProps[propName], propSchema, errors, warnings, 'forward');
125
+ }
126
+ }
127
+ }
128
+
129
+ private static checkPropertyCompatibility(
130
+ propName: string,
131
+ oldProp: any,
132
+ newProp: any,
133
+ errors: string[],
134
+ warnings: string[],
135
+ direction: 'backward' | 'forward'
136
+ ): void {
137
+ const oldType = this.normalizeType(oldProp.type);
138
+ const newType = this.normalizeType(newProp.type);
139
+
140
+ if (oldType !== newType) {
141
+ if (this.areTypesCompatible(oldType, newType, direction)) {
142
+ warnings.push(`Property '${propName}' type changed from ${oldType} to ${newType}`);
143
+ } else {
144
+ errors.push(`Property '${propName}' type incompatibly changed from ${oldType} to ${newType}`);
145
+ }
146
+ }
147
+
148
+ if (oldProp.enum && newProp.enum) {
149
+ const oldEnum = new Set(oldProp.enum);
150
+ const newEnum = new Set(newProp.enum);
151
+
152
+ if (direction === 'backward') {
153
+ for (const value of oldEnum) {
154
+ if (!newEnum.has(value)) {
155
+ errors.push(`Enum value '${value}' removed from property '${propName}'`);
156
+ }
157
+ }
158
+ } else {
159
+ for (const value of newEnum) {
160
+ if (!oldEnum.has(value)) {
161
+ warnings.push(`Enum value '${value}' added to property '${propName}'`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ private static normalizeType(type: any): string {
169
+ if (Array.isArray(type)) {
170
+ return type.join('|');
171
+ }
172
+ return type || 'any';
173
+ }
174
+
175
+ private static areTypesCompatible(oldType: string, newType: string, direction: 'backward' | 'forward'): boolean {
176
+ if (oldType === newType) return true;
177
+
178
+ if (direction === 'backward') {
179
+ if (newType === 'any') return true;
180
+ if (oldType === 'integer' && newType === 'number') return true;
181
+ } else {
182
+ if (oldType === 'any') return true;
183
+ if (newType === 'integer' && oldType === 'number') return true;
184
+ }
185
+
186
+ const oldTypes = new Set(oldType.split('|'));
187
+ const newTypes = new Set(newType.split('|'));
188
+
189
+ if (direction === 'backward') {
190
+ for (const t of oldTypes) {
191
+ if (!newTypes.has(t)) return false;
192
+ }
193
+ } else {
194
+ for (const t of newTypes) {
195
+ if (!oldTypes.has(t)) return false;
196
+ }
197
+ }
198
+
199
+ return true;
200
+ }
201
+ }