@teambit/semantics.entities.semantic-schema 0.0.100 → 0.0.101

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 (41) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +42 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/schema-diff-members.d.ts +3 -0
  5. package/dist/schema-diff-members.js +171 -0
  6. package/dist/schema-diff-members.js.map +1 -0
  7. package/dist/schema-diff.d.ts +13 -0
  8. package/dist/schema-diff.js +138 -0
  9. package/dist/schema-diff.js.map +1 -0
  10. package/dist/schema-node.d.ts +7 -0
  11. package/dist/schema-node.js +49 -0
  12. package/dist/schema-node.js.map +1 -1
  13. package/dist/schemas/class.d.ts +2 -0
  14. package/dist/schemas/class.js +11 -0
  15. package/dist/schemas/class.js.map +1 -1
  16. package/dist/schemas/enum.d.ts +2 -0
  17. package/dist/schemas/enum.js +55 -0
  18. package/dist/schemas/enum.js.map +1 -1
  19. package/dist/schemas/function-like.d.ts +9 -0
  20. package/dist/schemas/function-like.js +300 -0
  21. package/dist/schemas/function-like.js.map +1 -1
  22. package/dist/schemas/interface.d.ts +2 -0
  23. package/dist/schemas/interface.js +11 -0
  24. package/dist/schemas/interface.js.map +1 -1
  25. package/dist/schemas/type.d.ts +2 -0
  26. package/dist/schemas/type.js +25 -0
  27. package/dist/schemas/type.js.map +1 -1
  28. package/dist/schemas/variable-like.d.ts +2 -0
  29. package/dist/schemas/variable-like.js +60 -0
  30. package/dist/schemas/variable-like.js.map +1 -1
  31. package/index.ts +2 -0
  32. package/package.json +2 -2
  33. package/schema-diff-members.ts +164 -0
  34. package/schema-diff.ts +168 -0
  35. package/schema-node.ts +34 -0
  36. package/schemas/class.ts +7 -0
  37. package/schemas/enum.ts +46 -0
  38. package/schemas/function-like.ts +277 -0
  39. package/schemas/interface.ts +7 -0
  40. package/schemas/type.ts +18 -0
  41. package/schemas/variable-like.ts +54 -0
package/schema-node.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { pickBy } from 'lodash';
2
2
  import pluralize from 'pluralize';
3
3
  import type { DocSchema } from './schemas';
4
+ import type { SchemaChangeFact } from './schema-diff';
5
+ import { deepEqualNoLocation, diffDoc } from './schema-diff';
4
6
 
5
7
  export interface ISchemaNode {
6
8
  __schema: string;
@@ -84,6 +86,38 @@ export abstract class SchemaNode implements ISchemaNode {
84
86
 
85
87
  return undefined;
86
88
  }
89
+
90
+ /**
91
+ * Compute neutral change facts between this node and another node of the same type.
92
+ * Subclasses should override with type-specific comparison logic.
93
+ * Returns facts without impact judgment — impact is assessed separately.
94
+ */
95
+ diff(other: SchemaNode): SchemaChangeFact[] {
96
+ const facts: SchemaChangeFact[] = [];
97
+ const baseObj = this.toObject();
98
+ const compareObj = other.toObject();
99
+
100
+ // Doc changes
101
+ facts.push(...diffDoc(baseObj.doc, compareObj.doc));
102
+
103
+ // If only doc changed, we're done
104
+ const baseNoDoc = { ...baseObj, doc: undefined, location: undefined };
105
+ const compareNoDoc = { ...compareObj, doc: undefined, location: undefined };
106
+ if (deepEqualNoLocation(baseNoDoc, compareNoDoc)) return facts;
107
+
108
+ // Signature changed
109
+ if (baseObj.signature !== compareObj.signature) {
110
+ facts.push({
111
+ changeKind: 'signature-changed',
112
+ description: 'signature changed',
113
+ context: { fromSignature: baseObj.signature, toSignature: compareObj.signature },
114
+ from: baseObj.signature,
115
+ to: compareObj.signature,
116
+ });
117
+ }
118
+
119
+ return facts;
120
+ }
87
121
  }
88
122
 
89
123
  export type SchemaLocation = {
package/schemas/class.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { diffMembers } from '../schema-diff-members';
4
6
  import { DocSchema } from './docs';
5
7
  import { ExpressionWithTypeArgumentsSchema } from './expression-with-arguments';
6
8
  import { SchemaRegistry } from '../schema-registry';
@@ -120,6 +122,11 @@ export class ClassSchema extends SchemaNode {
120
122
  );
121
123
  }
122
124
 
125
+ diff(other: SchemaNode): SchemaChangeFact[] {
126
+ if (!(other instanceof ClassSchema)) return super.diff(other);
127
+ return diffMembers(this.toObject(), other.toObject(), this, other);
128
+ }
129
+
123
130
  static fromSchema(node: ClassSchema, memberTransformer: (_node: SchemaNode) => SchemaNode = (_node) => _node) {
124
131
  return new ClassSchema(
125
132
  node.name,
package/schemas/enum.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { deepEqualNoLocation, diffDoc } from '../schema-diff';
4
6
  import { DocSchema } from './docs';
5
7
  import { SchemaRegistry } from '..';
6
8
 
@@ -63,6 +65,50 @@ export class EnumSchema extends SchemaNode {
63
65
  };
64
66
  }
65
67
 
68
+ diff(other: SchemaNode): SchemaChangeFact[] {
69
+ if (!(other instanceof EnumSchema)) return super.diff(other);
70
+ const facts: SchemaChangeFact[] = [];
71
+ const baseMembers: Record<string, any>[] = this.toObject().members || [];
72
+ const compareMembers: Record<string, any>[] = other.toObject().members || [];
73
+ const baseMap = new Map(baseMembers.map((m) => [m.name || '', m]));
74
+ const compareMap = new Map(compareMembers.map((m) => [m.name || '', m]));
75
+
76
+ for (const [name, member] of compareMap) {
77
+ if (!baseMap.has(name)) {
78
+ facts.push({
79
+ changeKind: 'enum-member-added',
80
+ description: `enum member '${name}' added`,
81
+ context: { memberName: name },
82
+ to: member.signature || name,
83
+ });
84
+ }
85
+ }
86
+ for (const [name, member] of baseMap) {
87
+ if (!compareMap.has(name)) {
88
+ facts.push({
89
+ changeKind: 'enum-member-removed',
90
+ description: `enum member '${name}' removed`,
91
+ context: { memberName: name },
92
+ from: member.signature || name,
93
+ });
94
+ }
95
+ }
96
+ for (const [name, bm] of baseMap) {
97
+ const cm = compareMap.get(name);
98
+ if (cm && !deepEqualNoLocation(bm, cm)) {
99
+ facts.push({
100
+ changeKind: 'enum-member-value-changed',
101
+ description: `enum member '${name}' value changed`,
102
+ context: { memberName: name },
103
+ from: bm.signature || name,
104
+ to: cm.signature || name,
105
+ });
106
+ }
107
+ }
108
+ facts.push(...diffDoc(this.doc?.toObject(), other.doc?.toObject()));
109
+ return facts;
110
+ }
111
+
66
112
  static fromObject(obj: Record<string, any>): EnumSchema {
67
113
  const location = obj.location;
68
114
  const name = obj.name;
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { typesAreSemanticallyEqual, typeStr, deepEqualNoLocation, diffDoc } from '../schema-diff';
4
6
  import { ParameterSchema } from './parameter';
5
7
  import { DocSchema } from './docs';
6
8
  import { TagName } from './docs/tag';
@@ -148,6 +150,281 @@ export class FunctionLikeSchema extends SchemaNode {
148
150
  );
149
151
  }
150
152
 
153
+ diff(other: SchemaNode): SchemaChangeFact[] {
154
+ if (!(other instanceof FunctionLikeSchema)) return super.diff(other);
155
+ const facts: SchemaChangeFact[] = [];
156
+ const baseObj = this.toObject();
157
+ const compareObj = other.toObject();
158
+
159
+ FunctionLikeSchema.diffParams(baseObj.params || [], compareObj.params || [], facts);
160
+
161
+ if (!typesAreSemanticallyEqual(baseObj.returnType, compareObj.returnType)) {
162
+ const fromType = typeStr(baseObj.returnType);
163
+ const toType = typeStr(compareObj.returnType);
164
+ facts.push({
165
+ changeKind: 'return-type-changed',
166
+ description: `return type changed: ${fromType} → ${toType}`,
167
+ context: { fromType, toType, position: 'return-type' },
168
+ from: fromType,
169
+ to: toType,
170
+ });
171
+ }
172
+
173
+ if (!deepEqualNoLocation(baseObj.typeParams, compareObj.typeParams)) {
174
+ facts.push({
175
+ changeKind: 'type-parameters-changed',
176
+ description: `type parameters changed: <${(baseObj.typeParams || []).join(', ') || 'none'}> → <${(compareObj.typeParams || []).join(', ') || 'none'}>`,
177
+ context: { from: (baseObj.typeParams || []).join(', '), to: (compareObj.typeParams || []).join(', ') },
178
+ from: (baseObj.typeParams || []).join(', '),
179
+ to: (compareObj.typeParams || []).join(', '),
180
+ });
181
+ }
182
+
183
+ FunctionLikeSchema.diffModifiers(this.modifiers || [], other.modifiers || [], facts);
184
+ facts.push(...diffDoc(baseObj.doc, compareObj.doc));
185
+ return facts;
186
+ }
187
+
188
+ private static diffModifiers(baseMods: string[], compareMods: string[], facts: SchemaChangeFact[]): void {
189
+ const base = baseMods.filter((m) => m !== 'export');
190
+ const compare = compareMods.filter((m) => m !== 'export');
191
+ if (deepEqualNoLocation(base, compare)) return;
192
+
193
+ const added = compare.filter((m) => !base.includes(m));
194
+ const removed = base.filter((m) => !compare.includes(m));
195
+ const parts: string[] = [];
196
+ if (added.length) parts.push(`added: ${added.join(', ')}`);
197
+ if (removed.length) parts.push(`removed: ${removed.join(', ')}`);
198
+ const accessNarrowed = removed.includes('public') || added.includes('private') || added.includes('protected');
199
+
200
+ facts.push({
201
+ changeKind: accessNarrowed ? 'access-narrowed' : 'modifiers-changed',
202
+ description: `modifiers changed (${parts.join('; ')})`,
203
+ context: { added, removed, accessNarrowed },
204
+ from: base.join(', ') || 'none',
205
+ to: compare.join(', ') || 'none',
206
+ });
207
+ }
208
+
209
+ private static diffParams(
210
+ baseParams: Record<string, any>[],
211
+ compareParams: Record<string, any>[],
212
+ facts: SchemaChangeFact[]
213
+ ): void {
214
+ for (const p of compareParams.slice(baseParams.length)) {
215
+ const isOpt = p.isOptional || p.defaultValue !== undefined;
216
+ facts.push({
217
+ changeKind: 'parameter-added',
218
+ description: `parameter '${p.name}: ${typeStr(p.type)}' added${isOpt ? ' (optional)' : ' (required)'}`,
219
+ context: {
220
+ paramName: p.name,
221
+ isOptional: !!p.isOptional,
222
+ hasDefault: p.defaultValue !== undefined,
223
+ paramType: typeStr(p.type),
224
+ },
225
+ to: `${p.name}${p.isOptional ? '?' : ''}: ${typeStr(p.type)}`,
226
+ });
227
+ }
228
+
229
+ for (const p of baseParams.slice(compareParams.length)) {
230
+ facts.push({
231
+ changeKind: 'parameter-removed',
232
+ description: `parameter '${p.name}: ${typeStr(p.type)}' removed`,
233
+ context: { paramName: p.name, isOptional: !!p.isOptional, paramType: typeStr(p.type) },
234
+ from: `${p.name}${p.isOptional ? '?' : ''}: ${typeStr(p.type)}`,
235
+ });
236
+ }
237
+
238
+ const minLen = Math.min(baseParams.length, compareParams.length);
239
+ for (let i = 0; i < minLen; i++) {
240
+ FunctionLikeSchema.diffOverlappingParam(baseParams[i], compareParams[i], i, facts);
241
+ }
242
+ }
243
+
244
+ private static diffOverlappingParam(
245
+ bp: Record<string, any>,
246
+ cp: Record<string, any>,
247
+ index: number,
248
+ facts: SchemaChangeFact[]
249
+ ): void {
250
+ if (bp.objectBindingNodes || cp.objectBindingNodes) {
251
+ FunctionLikeSchema.diffDestructuredParam(bp, cp, facts);
252
+ if (!typesAreSemanticallyEqual(bp.type, cp.type)) {
253
+ const paramName = cp.name || bp.name;
254
+ facts.push({
255
+ changeKind: 'parameter-type-changed',
256
+ description: `parameter at position ${index} type changed: ${typeStr(bp.type)} → ${typeStr(cp.type)}`,
257
+ context: { paramName, fromType: typeStr(bp.type), toType: typeStr(cp.type), position: 'parameter' },
258
+ from: typeStr(bp.type),
259
+ to: typeStr(cp.type),
260
+ });
261
+ }
262
+ return;
263
+ }
264
+
265
+ const nameEqual = bp.name === cp.name;
266
+ const typeEqual = typesAreSemanticallyEqual(bp.type, cp.type);
267
+ if (nameEqual && typeEqual && bp.isOptional === cp.isOptional && bp.defaultValue === cp.defaultValue) return;
268
+
269
+ const paramName = cp.name || bp.name;
270
+ if (!nameEqual) {
271
+ facts.push({
272
+ changeKind: 'parameter-renamed',
273
+ description: `parameter at position ${index} renamed: '${bp.name}' → '${cp.name}'`,
274
+ context: { fromName: bp.name, toName: cp.name, position: index },
275
+ from: bp.name,
276
+ to: cp.name,
277
+ });
278
+ }
279
+ if (!typeEqual) {
280
+ facts.push({
281
+ changeKind: 'parameter-type-changed',
282
+ description: `parameter '${paramName}' type changed: ${typeStr(bp.type)} → ${typeStr(cp.type)}`,
283
+ context: { paramName, fromType: typeStr(bp.type), toType: typeStr(cp.type), position: 'parameter' },
284
+ from: typeStr(bp.type),
285
+ to: typeStr(cp.type),
286
+ });
287
+ }
288
+ FunctionLikeSchema.diffParamOptionality(bp, cp, paramName, facts);
289
+ FunctionLikeSchema.diffParamDefault(bp, cp, paramName, facts);
290
+ }
291
+
292
+ private static diffParamOptionality(
293
+ bp: Record<string, any>,
294
+ cp: Record<string, any>,
295
+ paramName: string,
296
+ facts: SchemaChangeFact[]
297
+ ): void {
298
+ if (bp.isOptional && !cp.isOptional) {
299
+ facts.push({
300
+ changeKind: 'became-required',
301
+ description: `parameter '${paramName}' became required (was optional)`,
302
+ context: { paramName, position: 'parameter' },
303
+ from: 'optional',
304
+ to: 'required',
305
+ });
306
+ } else if (!bp.isOptional && cp.isOptional) {
307
+ facts.push({
308
+ changeKind: 'became-optional',
309
+ description: `parameter '${paramName}' became optional (was required)`,
310
+ context: { paramName, position: 'parameter' },
311
+ from: 'required',
312
+ to: 'optional',
313
+ });
314
+ }
315
+ }
316
+
317
+ private static diffParamDefault(
318
+ bp: Record<string, any>,
319
+ cp: Record<string, any>,
320
+ paramName: string,
321
+ facts: SchemaChangeFact[]
322
+ ): void {
323
+ if (bp.defaultValue === cp.defaultValue) return;
324
+ if (bp.defaultValue !== undefined && cp.defaultValue === undefined) {
325
+ facts.push({
326
+ changeKind: 'parameter-default-removed',
327
+ description: `parameter '${paramName}' default value removed (was: ${bp.defaultValue})`,
328
+ context: { paramName, isOptional: !!bp.isOptional, previousDefault: String(bp.defaultValue) },
329
+ from: String(bp.defaultValue),
330
+ });
331
+ } else if (bp.defaultValue === undefined && cp.defaultValue !== undefined) {
332
+ facts.push({
333
+ changeKind: 'parameter-default-added',
334
+ description: `parameter '${paramName}' default value added: ${cp.defaultValue}`,
335
+ context: { paramName, newDefault: String(cp.defaultValue) },
336
+ to: String(cp.defaultValue),
337
+ });
338
+ } else {
339
+ facts.push({
340
+ changeKind: 'parameter-default-changed',
341
+ description: `parameter '${paramName}' default value changed: ${bp.defaultValue} → ${cp.defaultValue}`,
342
+ context: { paramName, previousDefault: String(bp.defaultValue), newDefault: String(cp.defaultValue) },
343
+ from: String(bp.defaultValue),
344
+ to: String(cp.defaultValue),
345
+ });
346
+ }
347
+ }
348
+
349
+ private static diffDestructuredParam(
350
+ base: Record<string, any>,
351
+ compare: Record<string, any>,
352
+ facts: SchemaChangeFact[]
353
+ ): void {
354
+ const baseBindings: Record<string, any>[] = base.objectBindingNodes || [];
355
+ const compareBindings: Record<string, any>[] = compare.objectBindingNodes || [];
356
+ const baseMap = new Map(baseBindings.map((b) => [b.name || '', b]));
357
+ const compareMap = new Map(compareBindings.map((b) => [b.name || '', b]));
358
+
359
+ for (const [name] of compareMap) {
360
+ if (!baseMap.has(name)) {
361
+ facts.push({
362
+ changeKind: 'destructured-property-added',
363
+ description: `destructured property '${name}' added`,
364
+ context: { propertyName: name },
365
+ to: name,
366
+ });
367
+ }
368
+ }
369
+ for (const [name] of baseMap) {
370
+ if (!compareMap.has(name)) {
371
+ facts.push({
372
+ changeKind: 'destructured-property-removed',
373
+ description: `destructured property '${name}' removed`,
374
+ context: { propertyName: name },
375
+ from: name,
376
+ });
377
+ }
378
+ }
379
+ for (const [name, bb] of baseMap) {
380
+ const cb = compareMap.get(name);
381
+ if (!cb) continue;
382
+ FunctionLikeSchema.diffBindingDefault(name, bb, cb, !!base.isOptional, facts);
383
+ if (bb.type && cb.type && !typesAreSemanticallyEqual(bb.type, cb.type)) {
384
+ facts.push({
385
+ changeKind: 'destructured-property-type-changed',
386
+ description: `destructured property '${name}' type changed: ${typeStr(bb.type)} → ${typeStr(cb.type)}`,
387
+ context: { propertyName: name, fromType: typeStr(bb.type), toType: typeStr(cb.type), position: 'parameter' },
388
+ from: typeStr(bb.type),
389
+ to: typeStr(cb.type),
390
+ });
391
+ }
392
+ }
393
+ }
394
+
395
+ private static diffBindingDefault(
396
+ name: string,
397
+ bb: Record<string, any>,
398
+ cb: Record<string, any>,
399
+ parentIsOptional: boolean,
400
+ facts: SchemaChangeFact[]
401
+ ): void {
402
+ if (bb.defaultValue === cb.defaultValue) return;
403
+ if (bb.defaultValue !== undefined && cb.defaultValue === undefined) {
404
+ facts.push({
405
+ changeKind: 'destructured-property-default-removed',
406
+ description: `destructured property '${name}' default value removed (was: ${bb.defaultValue})`,
407
+ context: { propertyName: name, isOptional: parentIsOptional, previousDefault: String(bb.defaultValue) },
408
+ from: String(bb.defaultValue),
409
+ });
410
+ } else if (bb.defaultValue === undefined && cb.defaultValue !== undefined) {
411
+ facts.push({
412
+ changeKind: 'destructured-property-default-added',
413
+ description: `destructured property '${name}' default value added: ${cb.defaultValue}`,
414
+ context: { propertyName: name, newDefault: String(cb.defaultValue) },
415
+ to: String(cb.defaultValue),
416
+ });
417
+ } else {
418
+ facts.push({
419
+ changeKind: 'destructured-property-default-changed',
420
+ description: `destructured property '${name}' default value changed: ${bb.defaultValue} → ${cb.defaultValue}`,
421
+ context: { propertyName: name, previousDefault: String(bb.defaultValue), newDefault: String(cb.defaultValue) },
422
+ from: String(bb.defaultValue),
423
+ to: String(cb.defaultValue),
424
+ });
425
+ }
426
+ }
427
+
151
428
  private modifiersToString() {
152
429
  const modifiersToPrint = this.modifiers.filter((modifier) => modifier !== 'export');
153
430
  return modifiersToPrint.length ? `${modifiersToPrint.join(' ')} ` : '';
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { diffMembers } from '../schema-diff-members';
4
6
  import { DocSchema } from './docs';
5
7
  import { ExpressionWithTypeArgumentsSchema } from './expression-with-arguments';
6
8
  import { SchemaRegistry } from '../schema-registry';
@@ -82,6 +84,11 @@ export class InterfaceSchema extends SchemaNode {
82
84
  };
83
85
  }
84
86
 
87
+ diff(other: SchemaNode): SchemaChangeFact[] {
88
+ if (!(other instanceof InterfaceSchema)) return super.diff(other);
89
+ return diffMembers(this.toObject(), other.toObject(), this, other);
90
+ }
91
+
85
92
  static fromObject(obj: Record<string, any>): InterfaceSchema {
86
93
  const location = obj.location;
87
94
  const name = obj.name;
package/schemas/type.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { typesAreSemanticallyEqual, diffDoc } from '../schema-diff';
4
6
  import { DocSchema } from './docs';
5
7
  import { SchemaRegistry } from '../schema-registry';
6
8
 
@@ -51,6 +53,22 @@ export class TypeSchema extends SchemaNode {
51
53
  };
52
54
  }
53
55
 
56
+ diff(other: SchemaNode): SchemaChangeFact[] {
57
+ if (!(other instanceof TypeSchema)) return super.diff(other);
58
+ const facts: SchemaChangeFact[] = [];
59
+ if (!typesAreSemanticallyEqual(this.type.toObject(), other.type.toObject())) {
60
+ facts.push({
61
+ changeKind: 'type-definition-changed',
62
+ description: 'type definition changed',
63
+ context: { fromSignature: this.signature, toSignature: other.signature },
64
+ from: this.signature,
65
+ to: other.signature,
66
+ });
67
+ }
68
+ facts.push(...diffDoc(this.doc?.toObject(), other.doc?.toObject()));
69
+ return facts;
70
+ }
71
+
54
72
  static fromObject(obj: Record<string, any>): TypeSchema {
55
73
  const location = obj.location;
56
74
  const name = obj.name;
@@ -1,6 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import type { SchemaLocation } from '../schema-node';
3
3
  import { SchemaNode } from '../schema-node';
4
+ import type { SchemaChangeFact } from '../schema-diff';
5
+ import { typesAreSemanticallyEqual, typeStr, diffDoc } from '../schema-diff';
4
6
  import { DocSchema } from './docs';
5
7
  import { SchemaRegistry } from '../schema-registry';
6
8
 
@@ -64,6 +66,58 @@ export class VariableLikeSchema extends SchemaNode {
64
66
  };
65
67
  }
66
68
 
69
+ diff(other: SchemaNode): SchemaChangeFact[] {
70
+ if (!(other instanceof VariableLikeSchema)) return super.diff(other);
71
+ const facts: SchemaChangeFact[] = [];
72
+ if (!typesAreSemanticallyEqual(this.type.toObject(), other.type.toObject())) {
73
+ facts.push({
74
+ changeKind: 'type-annotation-changed',
75
+ description: `type changed: ${typeStr(this.type.toObject())} → ${typeStr(other.type.toObject())}`,
76
+ context: {
77
+ fromType: typeStr(this.type.toObject()),
78
+ toType: typeStr(other.type.toObject()),
79
+ position: 'variable',
80
+ },
81
+ from: typeStr(this.type.toObject()),
82
+ to: typeStr(other.type.toObject()),
83
+ });
84
+ }
85
+ if (this.isOptional && !other.isOptional) {
86
+ facts.push({
87
+ changeKind: 'became-required',
88
+ description: 'became required (was optional)',
89
+ context: { position: 'variable' },
90
+ from: 'optional',
91
+ to: 'required',
92
+ });
93
+ } else if (!this.isOptional && other.isOptional) {
94
+ facts.push({
95
+ changeKind: 'became-optional',
96
+ description: 'became optional (was required)',
97
+ context: { position: 'variable' },
98
+ from: 'required',
99
+ to: 'optional',
100
+ });
101
+ }
102
+ if (this.defaultValue !== other.defaultValue) {
103
+ const changeKind =
104
+ this.defaultValue === undefined
105
+ ? 'parameter-default-added'
106
+ : other.defaultValue === undefined
107
+ ? 'parameter-default-removed'
108
+ : 'parameter-default-changed';
109
+ facts.push({
110
+ changeKind,
111
+ description: `default value changed: ${this.defaultValue ?? 'none'} → ${other.defaultValue ?? 'none'}`,
112
+ context: { previousDefault: this.defaultValue, newDefault: other.defaultValue },
113
+ from: this.defaultValue,
114
+ to: other.defaultValue,
115
+ });
116
+ }
117
+ facts.push(...diffDoc(this.doc?.toObject(), other.doc?.toObject()));
118
+ return facts;
119
+ }
120
+
67
121
  static fromObject(obj: Record<string, any>): VariableLikeSchema {
68
122
  const location = obj.location;
69
123
  const name = obj.name;