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

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 +10 -2
  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 +37 -2
  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-diff.ts ADDED
@@ -0,0 +1,168 @@
1
+ export type SchemaChangeFact = {
2
+ changeKind: string;
3
+ description: string;
4
+ /** Structured metadata for the impact assessor to reason about. */
5
+ context: Record<string, any>;
6
+ from?: string;
7
+ to?: string;
8
+ };
9
+
10
+ const SCHEMA_DISPLAY_NAMES: Record<string, string> = {
11
+ FunctionLikeSchema: 'Function',
12
+ ClassSchema: 'Class',
13
+ InterfaceSchema: 'Interface',
14
+ TypeSchema: 'Type Alias',
15
+ EnumSchema: 'Enum',
16
+ VariableLikeSchema: 'Variable',
17
+ ModuleSchema: 'Namespace',
18
+ ReactSchema: 'React Component',
19
+ TypeRefSchema: 'Type Reference',
20
+ TypeUnionSchema: 'Union Type',
21
+ TypeIntersectionSchema: 'Intersection Type',
22
+ TypeLiteralSchema: 'Type Literal',
23
+ TypeArraySchema: 'Array Type',
24
+ TupleTypeSchema: 'Tuple Type',
25
+ ParameterSchema: 'Parameter',
26
+ ExportSchema: 'Export',
27
+ DecoratorSchema: 'Decorator',
28
+ ConstructorSchema: 'Constructor',
29
+ GetAccessorSchema: 'Getter',
30
+ SetAccessorSchema: 'Setter',
31
+ IndexSignatureSchema: 'Index Signature',
32
+ KeywordTypeSchema: 'Keyword Type',
33
+ LiteralTypeSchema: 'Literal Type',
34
+ InferenceTypeSchema: 'Inferred Type',
35
+ };
36
+
37
+ export function schemaDisplayName(rawSchemaType: string, singular = true): string {
38
+ const name = SCHEMA_DISPLAY_NAMES[rawSchemaType];
39
+ if (name) return singular ? name : pluralize(name);
40
+ const cleaned = rawSchemaType
41
+ .replace(/Schema$/, '')
42
+ .replace(/([A-Z])/g, ' $1')
43
+ .trim();
44
+ return singular ? cleaned : pluralize(cleaned);
45
+ }
46
+
47
+ function pluralize(word: string): string {
48
+ if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z')) return `${word}es`;
49
+ if (word.endsWith('y') && !/[aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
50
+ return `${word}s`;
51
+ }
52
+
53
+ type TypeRenderer = (node: Record<string, any>) => string;
54
+
55
+ const TYPE_RENDERERS: Record<string, TypeRenderer> = {
56
+ TypeUnionSchema: (n) => (n.types || []).map((t: any) => typeStr(t)).join(' | ') || 'unknown',
57
+ TypeIntersectionSchema: (n) => (n.types || []).map((t: any) => typeStr(t)).join(' & ') || 'unknown',
58
+ TypeArraySchema: (n) => `${typeStr(n.type)}[]`,
59
+ TupleTypeSchema: (n) => `[${(n.members || []).map((t: any) => typeStr(t)).join(', ')}]`,
60
+ InferenceTypeSchema: (n) => n.type || n.name || 'inferred',
61
+ KeywordTypeSchema: (n) => n.name || 'keyword',
62
+ LiteralTypeSchema: (n) => (n.value !== undefined ? String(n.value) : n.name || 'literal'),
63
+ TypeRefSchema: (n) => {
64
+ const base = n.name || 'Ref';
65
+ return n.typeArgs?.length ? `${base}<${n.typeArgs.map((a: any) => typeStr(a)).join(', ')}>` : base;
66
+ },
67
+ TypeLiteralSchema: (n) => {
68
+ const members = (n.members || []).map((m: any) => m.signature || m.name || '').filter(Boolean);
69
+ return members.length <= 3 ? `{ ${members.join('; ')} }` : `{ ${members.slice(0, 3).join('; ')}; ... }`;
70
+ },
71
+ ParenthesizedTypeSchema: (n) => `(${typeStr(n.type)})`,
72
+ };
73
+
74
+ export function typeStr(node: Record<string, any> | undefined): string {
75
+ if (!node) return 'unknown';
76
+ const renderer = TYPE_RENDERERS[node.__schema];
77
+ if (renderer) return renderer(node);
78
+ return node.signature || node.name || typeStrFallback(node);
79
+ }
80
+
81
+ function typeStrFallback(node: Record<string, any>): string {
82
+ if (node.type && typeof node.type === 'string') return node.type;
83
+ if (node.type && typeof node.type === 'object') return typeStr(node.type);
84
+ return schemaDisplayName(node.__schema || 'unknown', true);
85
+ }
86
+
87
+ export function typesAreSemanticallyEqual(
88
+ base: Record<string, any> | undefined,
89
+ compare: Record<string, any> | undefined
90
+ ): boolean {
91
+ if (!base && !compare) return true;
92
+ if (!base || !compare) return false;
93
+
94
+ if (base.__schema === 'TypeRefSchema' && compare.__schema === 'TypeRefSchema') {
95
+ if (base.name !== compare.name) return false;
96
+ const baseArgs = base.typeArgs || [];
97
+ const compareArgs = compare.typeArgs || [];
98
+ if (baseArgs.length !== compareArgs.length) return false;
99
+ return baseArgs.every((a: any, i: number) => typesAreSemanticallyEqual(a, compareArgs[i]));
100
+ }
101
+
102
+ return deepEqualNoLocation(base, compare);
103
+ }
104
+
105
+ export function deepEqualNoLocation(a: any, b: any): boolean {
106
+ if (a === b) return true;
107
+ if (a == null || b == null) return false;
108
+ if (typeof a !== typeof b) return false;
109
+
110
+ if (Array.isArray(a)) {
111
+ if (!Array.isArray(b) || a.length !== b.length) return false;
112
+ return a.every((item, i) => deepEqualNoLocation(item, b[i]));
113
+ }
114
+
115
+ if (typeof a === 'object') {
116
+ const keysA = Object.keys(a).filter((k) => k !== 'location');
117
+ const keysB = Object.keys(b).filter((k) => k !== 'location');
118
+ if (keysA.length !== keysB.length) return false;
119
+ return keysA.every((key) => deepEqualNoLocation(a[key], b[key]));
120
+ }
121
+
122
+ return false;
123
+ }
124
+
125
+ export function diffDoc(
126
+ baseDoc: Record<string, any> | undefined,
127
+ compareDoc: Record<string, any> | undefined
128
+ ): SchemaChangeFact[] {
129
+ if (deepEqualNoLocation(baseDoc, compareDoc)) return [];
130
+
131
+ if (!baseDoc && compareDoc) {
132
+ return [
133
+ {
134
+ changeKind: 'documentation-added',
135
+ description: 'documentation added',
136
+ context: {},
137
+ to: compareDoc.comment || '(doc added)',
138
+ },
139
+ ];
140
+ }
141
+ if (baseDoc && !compareDoc) {
142
+ return [
143
+ {
144
+ changeKind: 'documentation-removed',
145
+ description: 'documentation removed',
146
+ context: {},
147
+ from: baseDoc.comment || '(doc removed)',
148
+ },
149
+ ];
150
+ }
151
+
152
+ const changes: string[] = [];
153
+ if (baseDoc?.comment !== compareDoc?.comment) changes.push('description');
154
+ const baseTags = (baseDoc?.tags || []).map((t: any) => t.tagName || t.name).sort();
155
+ const compareTags = (compareDoc?.tags || []).map((t: any) => t.tagName || t.name).sort();
156
+ if (!deepEqualNoLocation(baseTags, compareTags)) changes.push('tags');
157
+ if (changes.length === 0) changes.push('content');
158
+
159
+ return [
160
+ {
161
+ changeKind: 'documentation-changed',
162
+ description: `documentation ${changes.join(' and ')} changed`,
163
+ context: {},
164
+ from: baseDoc?.comment,
165
+ to: compareDoc?.comment,
166
+ },
167
+ ];
168
+ }
package/schema-node.ts CHANGED
@@ -1,20 +1,23 @@
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;
9
+ name?: string;
10
+ displaySchemaName: string;
7
11
  location: SchemaLocation;
8
12
  doc?: DocSchema;
9
13
  signature?: string;
10
- name?: string;
11
14
  toObject(): Record<string, any>;
12
15
  toString(options?: { color?: boolean }): string;
13
16
  toFullSignature(options?: { showDocs?: boolean }): string;
14
17
  getNodes(): SchemaNode[];
15
18
  findNode(predicate: (node: SchemaNode) => boolean, visitedNodes?: Set<SchemaNode>): SchemaNode | undefined;
16
19
  getAllNodesRecursively(visitedNodes?: Set<SchemaNode>): SchemaNode[];
17
- displaySchemaName: string;
20
+ diff(other: SchemaNode): SchemaChangeFact[];
18
21
  }
19
22
 
20
23
  /**
@@ -84,6 +87,38 @@ export abstract class SchemaNode implements ISchemaNode {
84
87
 
85
88
  return undefined;
86
89
  }
90
+
91
+ /**
92
+ * Compute neutral change facts between this node and another node of the same type.
93
+ * Subclasses should override with type-specific comparison logic.
94
+ * Returns facts without impact judgment — impact is assessed separately.
95
+ */
96
+ diff(other: SchemaNode): SchemaChangeFact[] {
97
+ const facts: SchemaChangeFact[] = [];
98
+ const baseObj = this.toObject();
99
+ const compareObj = other.toObject();
100
+
101
+ // Doc changes
102
+ facts.push(...diffDoc(baseObj.doc, compareObj.doc));
103
+
104
+ // If only doc changed, we're done
105
+ const baseNoDoc = { ...baseObj, doc: undefined, location: undefined };
106
+ const compareNoDoc = { ...compareObj, doc: undefined, location: undefined };
107
+ if (deepEqualNoLocation(baseNoDoc, compareNoDoc)) return facts;
108
+
109
+ // Signature changed
110
+ if (baseObj.signature !== compareObj.signature) {
111
+ facts.push({
112
+ changeKind: 'signature-changed',
113
+ description: 'signature changed',
114
+ context: { fromSignature: baseObj.signature, toSignature: compareObj.signature },
115
+ from: baseObj.signature,
116
+ to: compareObj.signature,
117
+ });
118
+ }
119
+
120
+ return facts;
121
+ }
87
122
  }
88
123
 
89
124
  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;