@matter/model 0.12.0-alpha.0-20241228-9f74a0273 → 0.12.0-alpha.0-20241231-14ac774ba

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 (73) hide show
  1. package/dist/cjs/aspects/Constraint.d.ts +24 -15
  2. package/dist/cjs/aspects/Constraint.d.ts.map +1 -1
  3. package/dist/cjs/aspects/Constraint.js +268 -198
  4. package/dist/cjs/aspects/Constraint.js.map +2 -2
  5. package/dist/cjs/common/FieldValue.d.ts +10 -4
  6. package/dist/cjs/common/FieldValue.d.ts.map +1 -1
  7. package/dist/cjs/common/FieldValue.js +1 -1
  8. package/dist/cjs/common/FieldValue.js.map +1 -1
  9. package/dist/cjs/common/Metatype.d.ts +19 -1
  10. package/dist/cjs/common/Metatype.d.ts.map +1 -1
  11. package/dist/cjs/common/Metatype.js +171 -170
  12. package/dist/cjs/common/Metatype.js.map +1 -1
  13. package/dist/cjs/common/Specification.d.ts +1 -1
  14. package/dist/cjs/common/Specification.d.ts.map +1 -1
  15. package/dist/cjs/logic/ModelDiff.d.ts +40 -0
  16. package/dist/cjs/logic/ModelDiff.d.ts.map +1 -0
  17. package/dist/cjs/logic/ModelDiff.js +119 -0
  18. package/dist/cjs/logic/ModelDiff.js.map +6 -0
  19. package/dist/cjs/logic/definition-validation/ValueValidator.js +1 -1
  20. package/dist/cjs/logic/definition-validation/ValueValidator.js.map +1 -1
  21. package/dist/cjs/logic/index.d.ts +1 -0
  22. package/dist/cjs/logic/index.d.ts.map +1 -1
  23. package/dist/cjs/logic/index.js +1 -0
  24. package/dist/cjs/logic/index.js.map +1 -1
  25. package/dist/cjs/parser/Lexer.d.ts +3 -3
  26. package/dist/cjs/parser/Lexer.d.ts.map +1 -1
  27. package/dist/cjs/parser/Lexer.js +35 -31
  28. package/dist/cjs/parser/Lexer.js.map +1 -1
  29. package/dist/cjs/parser/Token.d.ts +5 -2
  30. package/dist/cjs/parser/Token.d.ts.map +1 -1
  31. package/dist/cjs/parser/TokenStream.js +2 -2
  32. package/dist/esm/aspects/Constraint.d.ts +24 -15
  33. package/dist/esm/aspects/Constraint.d.ts.map +1 -1
  34. package/dist/esm/aspects/Constraint.js +269 -199
  35. package/dist/esm/aspects/Constraint.js.map +2 -2
  36. package/dist/esm/common/FieldValue.d.ts +10 -4
  37. package/dist/esm/common/FieldValue.d.ts.map +1 -1
  38. package/dist/esm/common/FieldValue.js +1 -1
  39. package/dist/esm/common/FieldValue.js.map +1 -1
  40. package/dist/esm/common/Metatype.d.ts +19 -1
  41. package/dist/esm/common/Metatype.d.ts.map +1 -1
  42. package/dist/esm/common/Metatype.js +171 -170
  43. package/dist/esm/common/Metatype.js.map +1 -1
  44. package/dist/esm/common/Specification.d.ts +1 -1
  45. package/dist/esm/common/Specification.d.ts.map +1 -1
  46. package/dist/esm/logic/ModelDiff.d.ts +40 -0
  47. package/dist/esm/logic/ModelDiff.d.ts.map +1 -0
  48. package/dist/esm/logic/ModelDiff.js +99 -0
  49. package/dist/esm/logic/ModelDiff.js.map +6 -0
  50. package/dist/esm/logic/definition-validation/ValueValidator.js +1 -1
  51. package/dist/esm/logic/definition-validation/ValueValidator.js.map +1 -1
  52. package/dist/esm/logic/index.d.ts +1 -0
  53. package/dist/esm/logic/index.d.ts.map +1 -1
  54. package/dist/esm/logic/index.js +1 -0
  55. package/dist/esm/logic/index.js.map +1 -1
  56. package/dist/esm/parser/Lexer.d.ts +3 -3
  57. package/dist/esm/parser/Lexer.d.ts.map +1 -1
  58. package/dist/esm/parser/Lexer.js +35 -31
  59. package/dist/esm/parser/Lexer.js.map +1 -1
  60. package/dist/esm/parser/Token.d.ts +5 -2
  61. package/dist/esm/parser/Token.d.ts.map +1 -1
  62. package/dist/esm/parser/TokenStream.js +2 -2
  63. package/package.json +4 -4
  64. package/src/aspects/Constraint.ts +340 -215
  65. package/src/common/FieldValue.ts +10 -5
  66. package/src/common/Metatype.ts +200 -181
  67. package/src/common/Specification.ts +1 -1
  68. package/src/logic/ModelDiff.ts +150 -0
  69. package/src/logic/definition-validation/ValueValidator.ts +1 -1
  70. package/src/logic/index.ts +1 -0
  71. package/src/parser/Lexer.ts +38 -40
  72. package/src/parser/Token.ts +11 -1
  73. package/src/parser/TokenStream.ts +2 -2
@@ -47,6 +47,11 @@ export namespace FieldValue {
47
47
  export const none = "none";
48
48
  export type none = typeof none;
49
49
 
50
+ /**
51
+ * A field value that allows type extension.
52
+ */
53
+ export type Open = FieldValue | { type: string };
54
+
50
55
  /**
51
56
  * If a field value isn't a primitive type, it's an object with a type field indicating one of these types.
52
57
  */
@@ -55,7 +60,7 @@ export namespace FieldValue {
55
60
  /**
56
61
  * Test for one of the special placeholder types.
57
62
  */
58
- export function is(value: FieldValue | undefined, type: Type) {
63
+ export function is(value: Open | undefined, type: Type) {
59
64
  return value && (value as any).type === type;
60
65
  }
61
66
 
@@ -150,7 +155,7 @@ export namespace FieldValue {
150
155
  return `${(value as Celsius).value}°C`;
151
156
  }
152
157
  if (is(value, percent)) {
153
- return `${(value as Percent).value}%';`;
158
+ return `${(value as Percent).value}%`;
154
159
  }
155
160
  if (is(value, properties)) {
156
161
  return stringSerialize((value as Properties).properties) ?? "?";
@@ -161,7 +166,7 @@ export namespace FieldValue {
161
166
  /**
162
167
  * Given a type name as a hint, do our best to convert a field value to a number.
163
168
  */
164
- export function numericValue(value: FieldValue | undefined, typeName?: string) {
169
+ export function numericValue(value: Open | undefined, typeName?: string) {
165
170
  if (typeof value === "boolean") {
166
171
  return value ? 1 : 0;
167
172
  }
@@ -240,7 +245,7 @@ export namespace FieldValue {
240
245
  /**
241
246
  * Get the referenced name if the FieldValue is a reference.
242
247
  */
243
- export function referenced(value: FieldValue | undefined) {
248
+ export function referenced(value: Open | undefined) {
244
249
  if (is(value, reference)) {
245
250
  return (value as Reference).name;
246
251
  }
@@ -254,7 +259,7 @@ export namespace FieldValue {
254
259
  *
255
260
  * @returns the cast value or FieldValue.Invalid if cast is not possible
256
261
  */
257
- export function cast(type: Metatype, value: any): FieldValue | FieldValue.Invalid | undefined {
262
+ export function cast<const T extends Metatype>(type: T, value: any): FieldValue | FieldValue.Invalid | undefined {
258
263
  if (value === undefined || value === null || type === "any") {
259
264
  return value;
260
265
  }
@@ -74,6 +74,27 @@ export namespace Metatype {
74
74
  }
75
75
  }
76
76
 
77
+ /**
78
+ * Map metatype value to JS type.
79
+ */
80
+ export type Native<T> = T extends "boolean"
81
+ ? boolean
82
+ : T extends "integer" | "float"
83
+ ? number
84
+ : T extends "string"
85
+ ? string
86
+ : T extends "bitmap" | "object"
87
+ ? Record<string, unknown>
88
+ : T extends "array"
89
+ ? unknown[]
90
+ : T extends "bytes"
91
+ ? Uint8Array
92
+ : T extends "date"
93
+ ? Date
94
+ : T extends "any"
95
+ ? unknown
96
+ : never;
97
+
77
98
  /**
78
99
  * Functions that perform conversion of arbitrary values to a metatype.
79
100
  *
@@ -82,243 +103,241 @@ export namespace Metatype {
82
103
  *
83
104
  * @throws {@link UnsupportedCastError} if the cast is deemed impossible
84
105
  */
85
- export const cast: Record<Metatype, (value: any) => any> = {
86
- any(value: any) {
106
+ export function cast<const T extends `${Metatype}`>(type: T, value: unknown) {
107
+ const caster = cast[type];
108
+ return caster(value) as Native<T>;
109
+ }
110
+
111
+ cast.any = (value: unknown) => value;
112
+
113
+ cast.boolean = (value: unknown): boolean | null | undefined => {
114
+ if (typeof value === "boolean" || value === null || value === undefined) {
87
115
  return value;
88
- },
116
+ }
89
117
 
90
- boolean(value: any): boolean | null | undefined {
91
- if (typeof value === "boolean" || value === null || value === undefined) {
92
- return value;
118
+ if (typeof value === "string") {
119
+ const normalized = value.toLowerCase().trim();
120
+ switch (normalized) {
121
+ case "":
122
+ case "0":
123
+ case "off":
124
+ case "no":
125
+ case "false":
126
+ return false;
127
+
128
+ case "1":
129
+ case "on":
130
+ case "yes":
131
+ case "true":
132
+ return true;
93
133
  }
134
+ }
135
+
136
+ if (typeof value === "number" || typeof value === "bigint") {
137
+ return !!value;
138
+ }
94
139
 
95
- if (typeof value === "string") {
96
- const normalized = value.toLowerCase().trim();
97
- switch (normalized) {
98
- case "":
99
- case "0":
100
- case "off":
101
- case "no":
102
- case "false":
103
- return false;
104
-
105
- case "1":
106
- case "on":
107
- case "yes":
108
- case "true":
109
- return true;
140
+ if (ArrayBuffer.isView(value)) {
141
+ for (const byte of new Uint8Array(value.buffer)) {
142
+ if (byte) {
143
+ return true;
110
144
  }
111
145
  }
146
+ return false;
147
+ }
112
148
 
113
- if (typeof value === "number" || typeof value === "bigint") {
114
- return !!value;
115
- }
149
+ throw new UnsupportedCastError(`Cannot convert "${value}" to boolean`);
150
+ };
116
151
 
117
- if (ArrayBuffer.isView(value)) {
118
- for (const byte of new Uint8Array(value.buffer)) {
119
- if (byte) {
120
- return true;
121
- }
122
- }
123
- return false;
124
- }
152
+ cast.bitmap = (value: any): number | bigint | Record<string, number> | null | undefined => {
153
+ if (value === null || value === undefined) {
154
+ return value;
155
+ }
125
156
 
126
- throw new UnsupportedCastError(`Cannot convert "${value}" to boolean`);
127
- },
157
+ if (typeof value === "string") {
158
+ value = cast.integer(value);
159
+ }
128
160
 
129
- bitmap(value: any): number | bigint | Record<string, number> | null | undefined {
130
- if (value === null || value === undefined) {
161
+ if (typeof value === "number") {
162
+ if (Number.isFinite(value)) {
131
163
  return value;
132
164
  }
165
+ } else if (typeof value === "bigint") {
166
+ return value;
167
+ } else if (isObject(value)) {
168
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cast.integer(v)])) as Record<
169
+ string,
170
+ number
171
+ >;
172
+ }
133
173
 
134
- if (typeof value === "string") {
135
- value = cast.integer(value);
136
- }
174
+ throw new UnsupportedCastError(`Cannot convert "${value}" to bitmap`);
175
+ };
137
176
 
138
- if (typeof value === "number") {
139
- if (Number.isFinite(value)) {
140
- return value;
141
- }
142
- } else if (typeof value === "bigint") {
177
+ cast.enum = (value: any): number | string | null | undefined => {
178
+ if (typeof value === "string") {
179
+ if (value.trim().match(/^(?:[0-9]+|0x[0-9a-f]+|0b[01]+)$/)) {
180
+ value = Number.parseInt(value);
181
+ } else {
143
182
  return value;
144
- } else if (isObject(value)) {
145
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cast.integer(v)])) as Record<
146
- string,
147
- number
148
- >;
149
183
  }
184
+ }
150
185
 
151
- throw new UnsupportedCastError(`Cannot convert "${value}" to bitmap`);
152
- },
186
+ if (typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value)) {
187
+ return value;
188
+ }
153
189
 
154
- enum(value: any): number | string | null | undefined {
155
- if (typeof value === "string") {
156
- if (value.trim().match(/^(?:[0-9]+|0x[0-9a-f]+|0b[01]+)$/)) {
157
- value = Number.parseInt(value);
158
- } else {
159
- return value;
160
- }
161
- }
190
+ throw new UnsupportedCastError(`Cannot convert "${value}" to an enum value`);
191
+ };
162
192
 
163
- if (typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value)) {
164
- return value;
165
- }
193
+ cast.integer = (value: any): number | bigint | null | undefined => {
194
+ if (value === null || value === undefined) {
195
+ return value;
196
+ }
166
197
 
167
- throw new UnsupportedCastError(`Cannot convert "${value}" to an enum value`);
168
- },
198
+ switch (typeof value) {
199
+ case "number":
200
+ return Math.floor(value);
169
201
 
170
- integer(value: any): number | bigint | null | undefined {
171
- if (value === null || value === undefined) {
202
+ case "bigint":
172
203
  return value;
173
- }
174
-
175
- switch (typeof value) {
176
- case "number":
177
- return Math.floor(value);
178
-
179
- case "bigint":
180
- return value;
181
204
 
182
- case "boolean":
183
- return value ? 1 : 0;
184
- }
205
+ case "boolean":
206
+ return value ? 1 : 0;
207
+ }
185
208
 
186
- if (value instanceof Date) {
187
- return value.getTime();
188
- }
209
+ if (value instanceof Date) {
210
+ return value.getTime();
211
+ }
189
212
 
190
- if (typeof value === "string") {
191
- try {
192
- const big = BigInt(value);
193
- const little = Number.parseInt(value);
194
- if (big === BigInt(little)) {
195
- return little;
196
- }
197
- return big;
198
- } catch (e) {
199
- if (!(e instanceof SyntaxError)) {
200
- throw e;
201
- }
213
+ if (typeof value === "string") {
214
+ try {
215
+ const big = BigInt(value);
216
+ const little = Number.parseInt(value);
217
+ if (big === BigInt(little)) {
218
+ return little;
219
+ }
220
+ return big;
221
+ } catch (e) {
222
+ if (!(e instanceof SyntaxError)) {
223
+ throw e;
202
224
  }
203
225
  }
226
+ }
204
227
 
205
- throw new UnsupportedCastError(`Cannot convert "${value}" to an integer`);
206
- },
228
+ throw new UnsupportedCastError(`Cannot convert "${value}" to an integer`);
229
+ };
207
230
 
208
- float(value: any): number | null | undefined {
209
- if (typeof value === "number" || value === null || value === undefined) {
210
- return value;
211
- }
231
+ cast.float = (value: any): number | null | undefined => {
232
+ if (typeof value === "number" || value === null || value === undefined) {
233
+ return value;
234
+ }
212
235
 
213
- if (value instanceof Date) {
214
- return value.getTime();
215
- }
236
+ if (value instanceof Date) {
237
+ return value.getTime();
238
+ }
216
239
 
217
- const number = Number(value);
218
- if (!Number.isNaN(number) && Number.isFinite(value)) {
219
- return number;
220
- }
240
+ const number = Number(value);
241
+ if (!Number.isNaN(number) && Number.isFinite(value)) {
242
+ return number;
243
+ }
221
244
 
222
- throw new UnsupportedCastError(`Cannot convert "${value}" to a float`);
223
- },
245
+ throw new UnsupportedCastError(`Cannot convert "${value}" to a float`);
246
+ };
224
247
 
225
- bytes(value: any): Uint8Array | null | undefined {
226
- if (value === undefined || value === null || value instanceof Uint8Array) {
227
- return value;
228
- }
248
+ cast.bytes = (value: any): Uint8Array | null | undefined => {
249
+ if (value === undefined || value === null || value instanceof Uint8Array) {
250
+ return value;
251
+ }
229
252
 
230
- if (typeof value === "string") {
231
- return Bytes.fromHex(value);
232
- }
253
+ if (typeof value === "string") {
254
+ return Bytes.fromHex(value);
255
+ }
233
256
 
234
- if (typeof value === "boolean") {
235
- return new Uint8Array([value ? 1 : 0]);
236
- }
257
+ if (typeof value === "boolean") {
258
+ return new Uint8Array([value ? 1 : 0]);
259
+ }
237
260
 
238
- if (typeof value === "number" || typeof value === "bigint") {
239
- return Bytes.fromHex(value.toString(16));
240
- }
261
+ if (typeof value === "number" || typeof value === "bigint") {
262
+ return Bytes.fromHex(value.toString(16));
263
+ }
241
264
 
242
- throw new UnsupportedCastError(`Cannot convert "${value}" to bytes`);
243
- },
265
+ throw new UnsupportedCastError(`Cannot convert "${value}" to bytes`);
266
+ };
244
267
 
245
- array(value: any): Array<unknown> | null | undefined {
246
- if (value === undefined || value === null || Array.isArray(value)) {
247
- return value;
248
- }
268
+ cast.array = (value: any): Array<unknown> | null | undefined => {
269
+ if (value === undefined || value === null || Array.isArray(value)) {
270
+ return value;
271
+ }
249
272
 
250
- if (typeof value === "string") {
251
- try {
252
- const parsed = JSON.parse(value);
253
- if (Array.isArray(parsed)) {
254
- return parsed;
255
- }
256
- } catch (e) {
257
- if (!(e instanceof SyntaxError)) {
258
- throw e;
259
- }
273
+ if (typeof value === "string") {
274
+ try {
275
+ const parsed = JSON.parse(value);
276
+ if (Array.isArray(parsed)) {
277
+ return parsed;
278
+ }
279
+ } catch (e) {
280
+ if (!(e instanceof SyntaxError)) {
281
+ throw e;
260
282
  }
261
283
  }
284
+ }
262
285
 
263
- throw new UnsupportedCastError(`Cannot convert "${value}" to array`);
264
- },
286
+ throw new UnsupportedCastError(`Cannot convert "${value}" to array`);
287
+ };
265
288
 
266
- object(value: any): Record<string, unknown> | null | undefined {
267
- if (
268
- value === undefined ||
269
- (typeof value === "object" && !Array.isArray(value) && !(value instanceof Date))
270
- ) {
271
- return value;
272
- }
289
+ cast.object = (value: any): Record<string, unknown> | null | undefined => {
290
+ if (value === undefined || (typeof value === "object" && !Array.isArray(value) && !(value instanceof Date))) {
291
+ return value;
292
+ }
273
293
 
274
- if (typeof value === "string") {
275
- try {
276
- const parsed = JSON.parse(value);
277
- return parsed;
278
- } catch (e) {
279
- if (!(e instanceof SyntaxError)) {
280
- throw e;
281
- }
294
+ if (typeof value === "string") {
295
+ try {
296
+ const parsed = JSON.parse(value);
297
+ return parsed;
298
+ } catch (e) {
299
+ if (!(e instanceof SyntaxError)) {
300
+ throw e;
282
301
  }
283
302
  }
303
+ }
284
304
 
285
- throw new UnsupportedCastError(`Cannot convert "${value}" to object`);
286
- },
305
+ throw new UnsupportedCastError(`Cannot convert "${value}" to object`);
306
+ };
287
307
 
288
- string(value: any): string | null | undefined {
289
- if (value === undefined || value === null) {
290
- return value;
291
- }
308
+ cast.string = (value: any): string | null | undefined => {
309
+ if (value === undefined || value === null) {
310
+ return value;
311
+ }
292
312
 
293
- if (typeof value === "string") {
294
- return value;
295
- }
313
+ if (typeof value === "string") {
314
+ return value;
315
+ }
296
316
 
297
- if (value instanceof Date) {
298
- return value.toISOString();
299
- }
317
+ if (value instanceof Date) {
318
+ return value.toISOString();
319
+ }
300
320
 
301
- if (typeof value === "object" || Array.isArray(value)) {
302
- return JSON.stringify(value);
303
- }
321
+ if (typeof value === "object" || Array.isArray(value)) {
322
+ return JSON.stringify(value);
323
+ }
304
324
 
305
- return value.toString();
306
- },
325
+ return value.toString();
326
+ };
307
327
 
308
- date(value: any): Date | null | undefined {
309
- if (value === undefined || value === null || value instanceof Date) {
310
- return value;
311
- }
328
+ cast.date = (value: any): Date | null | undefined => {
329
+ if (value === undefined || value === null || value instanceof Date) {
330
+ return value;
331
+ }
312
332
 
313
- if (typeof value === "number" || typeof value === "string") {
314
- const date = new Date(value);
315
- if (!Number.isNaN(date.getTime())) {
316
- return date;
317
- }
333
+ if (typeof value === "number" || typeof value === "string") {
334
+ const date = new Date(value);
335
+ if (!Number.isNaN(date.getTime())) {
336
+ return date;
318
337
  }
338
+ }
319
339
 
320
- throw new UnexpectedDataError();
321
- },
340
+ throw new UnexpectedDataError();
322
341
  };
323
342
 
324
343
  /**
@@ -44,7 +44,7 @@ export namespace Specification {
44
44
  /**
45
45
  * Matter specification version.
46
46
  */
47
- export type Revision = `${number}.${number}`;
47
+ export type Revision = `${number}.${number}` | `${number}.${number}.${number}.${number}`;
48
48
 
49
49
  /**
50
50
  * The default specification revision for Matter.js.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { ElementTag } from "#common/ElementTag.js";
8
+ import { Specification } from "#index.js";
9
+ import { Model } from "#models/Model.js";
10
+ import { Diagnostic } from "@matter/general";
11
+ import { ModelVariantTraversal, VariantDetail } from "./ModelVariantTraversal.js";
12
+
13
+ /**
14
+ * A high level summary of changes between two models.
15
+ */
16
+ export type ModelDiff = ModelDiff.Add | ModelDiff.Delete | ModelDiff.List | ModelDiff.Summary;
17
+
18
+ /**
19
+ * Diff two models.
20
+ */
21
+ export function ModelDiff(from: Model, to: Model, depth = 2) {
22
+ const traversal = new DiffTraversal(depth);
23
+ return traversal.traverse({ from, to });
24
+ }
25
+
26
+ class DiffTraversal extends ModelVariantTraversal<ModelDiff | undefined> {
27
+ #detailDepth: number;
28
+ #currentDepth = 0;
29
+
30
+ constructor(depth: number) {
31
+ super(Specification.REVISION, ["from", "to"]);
32
+ this.#detailDepth = depth;
33
+ }
34
+
35
+ protected override visit(variants: VariantDetail, recurse: () => (ModelDiff | undefined)[]): ModelDiff | undefined {
36
+ if (variants.map.to === undefined) {
37
+ if (variants.map.from === undefined) {
38
+ return;
39
+ }
40
+ return {
41
+ kind: "delete",
42
+ tag: variants.tag,
43
+ name: variants.name,
44
+ };
45
+ }
46
+
47
+ if (variants.map.from === undefined) {
48
+ return {
49
+ kind: "add",
50
+ tag: variants.tag,
51
+ name: variants.name,
52
+ };
53
+ }
54
+
55
+ if (this.#currentDepth >= this.#detailDepth) {
56
+ return;
57
+ }
58
+
59
+ this.#currentDepth++;
60
+ const children = recurse().filter(child => child !== undefined);
61
+ this.#currentDepth--;
62
+
63
+ if (!children.length) {
64
+ return;
65
+ }
66
+
67
+ if (this.#currentDepth < this.#detailDepth - 1) {
68
+ return {
69
+ kind: "list",
70
+ tag: variants.tag,
71
+ name: variants.name,
72
+ children,
73
+ };
74
+ }
75
+
76
+ const changes = {} as ModelDiff.Summary["changes"];
77
+ for (const child of children) {
78
+ changes[child.tag] = (changes[child.tag] ?? 0) + 1;
79
+ }
80
+
81
+ return {
82
+ kind: "summary",
83
+ tag: variants.tag,
84
+ name: variants.name,
85
+ changes,
86
+ };
87
+ }
88
+ }
89
+
90
+ export namespace ModelDiff {
91
+ /**
92
+ * Convert a diff to a diagnostic for serialization.
93
+ */
94
+ export function diagnosticOf(diff: ModelDiff | undefined): unknown {
95
+ const id = `${diff?.tag}#${diff?.name}`;
96
+ switch (diff?.kind) {
97
+ case "add":
98
+ return Diagnostic.added(id);
99
+
100
+ case "delete":
101
+ return Diagnostic.deleted(id);
102
+
103
+ case "list":
104
+ if (diff.children.length) {
105
+ return [id, Diagnostic.list(diff.children.map(diagnosticOf))];
106
+ }
107
+ break;
108
+
109
+ case "summary":
110
+ const changes = Object.entries(diff.changes).map(([tag, count]) => {
111
+ if (count < 0) {
112
+ return Diagnostic.deleted(`${-count} ${tag}`);
113
+ }
114
+
115
+ if (count > 0) {
116
+ return Diagnostic.added(`${count} ${tag}`);
117
+ }
118
+
119
+ return Diagnostic.weak(`${0} ${tag}`);
120
+ });
121
+
122
+ return [`${id}`, ...changes];
123
+ }
124
+
125
+ return Diagnostic.weak("(unchanged)");
126
+ }
127
+
128
+ export interface Identity {
129
+ name: string;
130
+ tag: ElementTag;
131
+ }
132
+
133
+ export interface Add extends Identity {
134
+ kind: "add";
135
+ }
136
+
137
+ export interface Delete extends Identity {
138
+ kind: "delete";
139
+ }
140
+
141
+ export interface List extends Identity {
142
+ kind: "list";
143
+ children: ModelDiff[];
144
+ }
145
+
146
+ export interface Summary extends Identity {
147
+ kind: "summary";
148
+ changes: Record<ElementTag, number>;
149
+ }
150
+ }
@@ -49,7 +49,7 @@ export class ValueValidator<T extends ValueModel> extends ModelValidator<T> {
49
49
  private validateAspect(name: string) {
50
50
  const aspect = (this.model as any)[name] as Aspect;
51
51
  if (aspect?.errors) {
52
- aspect.errors.forEach((e: DefinitionError) => this.model.error(e.code, e.message));
52
+ aspect.errors.forEach((e: DefinitionError) => this.model.error(e.code, `${e.source}: ${e.message}`));
53
53
  }
54
54
  }
55
55