@typra/emitter 0.2.5 → 0.2.7

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.
@@ -0,0 +1,667 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const EMPTY_SUMMARY = {
4
+ exports: { added: 0, removed: 0, changed: 0 },
5
+ protocols: { added: 0, removed: 0, changed: 0 },
6
+ files: { added: 0, deleted: 0, ownershipChanged: 0 },
7
+ packageNamesChanged: 0,
8
+ modulesChanged: 0,
9
+ toolchain: { changed: 0, unsupported: 0 },
10
+ protectedPathTouches: 0,
11
+ hydrationZoneTouches: 0,
12
+ staleCleanupCandidates: 0,
13
+ schema: {
14
+ addedTypes: 0,
15
+ removedTypes: 0,
16
+ addedOptionalProperties: 0,
17
+ addedRequiredProperties: 0,
18
+ removedProperties: 0,
19
+ requirednessChanged: 0,
20
+ propertyTypesChanged: 0,
21
+ wireNamesChanged: 0,
22
+ discriminatorsChanged: 0,
23
+ enumValuesChanged: 0,
24
+ },
25
+ };
26
+ export function verifyTypraMetadata(options) {
27
+ return compareTypraMetadata(loadTypraMetadata(options.baselineRoot), loadTypraMetadata(options.currentRoot), options.configPath ? loadVerifyConfig(options.configPath) : undefined);
28
+ }
29
+ export function loadTypraMetadata(root) {
30
+ return {
31
+ exportSurface: readJson(metadataFile(root, "export-surfaces.json")),
32
+ manifest: readJson(metadataFile(root, "manifest.json")),
33
+ model: readOptionalJson(modelFile(root)),
34
+ hydration: readOptionalJson(metadataFile(root, "hydration-seams.json")),
35
+ };
36
+ }
37
+ export function loadVerifyConfig(configPath) {
38
+ const config = readJson(configPath);
39
+ if (config.protectedPaths !== undefined && !Array.isArray(config.protectedPaths)) {
40
+ throw new Error(`Invalid Typra verifier config: protectedPaths must be an array.`);
41
+ }
42
+ if (config.protectedPaths?.some((entry) => typeof entry !== "string")) {
43
+ throw new Error(`Invalid Typra verifier config: protectedPaths entries must be strings.`);
44
+ }
45
+ if (config.hydrationZones !== undefined && !Array.isArray(config.hydrationZones)) {
46
+ throw new Error(`Invalid Typra verifier config: hydrationZones must be an array.`);
47
+ }
48
+ if (config.hydrationZones?.some((entry) => typeof entry !== "string")) {
49
+ throw new Error(`Invalid Typra verifier config: hydrationZones entries must be strings.`);
50
+ }
51
+ return config;
52
+ }
53
+ export function compareTypraMetadata(baseline, current, config = {}) {
54
+ const summary = cloneSummary();
55
+ const failures = [];
56
+ compareSnapshotIdentity(baseline.exportSurface, current.exportSurface, summary, failures);
57
+ compareToolchain(baseline.exportSurface, current.exportSurface, summary, failures);
58
+ compareExports(baseline.exportSurface, current.exportSurface, summary, failures);
59
+ compareProtocols(baseline.exportSurface, current.exportSurface, summary, failures);
60
+ compareManifest(baseline.manifest, current.manifest, summary, failures);
61
+ compareHydrationBoundaryMetadata(baseline.hydration, current.hydration, failures);
62
+ compareProtectedPaths(current.manifest, baseline.hydration, current.hydration, config, summary, failures);
63
+ compareHydrationZones(current.manifest, current.hydration, config, summary);
64
+ const schemaEvolution = compareSchemaEvolution(baseline.model, current.model, summary, failures);
65
+ const conformanceMap = buildConformanceMap(current.exportSurface);
66
+ const staleCleanupDryRun = buildStaleCleanupDryRun(baseline.manifest, current.manifest, config, baseline.hydration, current.hydration);
67
+ summary.staleCleanupCandidates = staleCleanupDryRun.length;
68
+ const hydrationBoundaries = buildHydrationBoundaryReport(current.hydration, config);
69
+ failures.sort(compareFailures);
70
+ const breakingChange = classifyBreakingChange(summary, schemaEvolution, failures);
71
+ return {
72
+ ok: failures.every((failure) => !failure.blocking),
73
+ breakingChange,
74
+ summary,
75
+ failures,
76
+ schemaEvolution,
77
+ conformanceMap,
78
+ staleCleanupDryRun,
79
+ hydrationBoundaries,
80
+ };
81
+ }
82
+ export function formatVerifySummary(result) {
83
+ const lines = [
84
+ `Typra verify: ${result.ok ? "passed" : "failed"}`,
85
+ `exports: +${result.summary.exports.added} / -${result.summary.exports.removed} / changed ${result.summary.exports.changed}`,
86
+ `protocols: +${result.summary.protocols.added} / -${result.summary.protocols.removed} / changed ${result.summary.protocols.changed}`,
87
+ `files: +${result.summary.files.added} / deleted ${result.summary.files.deleted} / ownership changed ${result.summary.files.ownershipChanged}`,
88
+ `package names changed: ${result.summary.packageNamesChanged}`,
89
+ `modules changed: ${result.summary.modulesChanged}`,
90
+ `toolchain changed: ${result.summary.toolchain.changed} / unsupported ${result.summary.toolchain.unsupported}`,
91
+ `protected path touches: ${result.summary.protectedPathTouches}`,
92
+ `hydration zone touches: ${result.summary.hydrationZoneTouches}`,
93
+ `stale cleanup dry-run candidates: ${result.summary.staleCleanupCandidates}`,
94
+ `schema: types +${result.summary.schema.addedTypes} / -${result.summary.schema.removedTypes}, required fields +${result.summary.schema.addedRequiredProperties}, optional fields +${result.summary.schema.addedOptionalProperties}, requiredness changed ${result.summary.schema.requirednessChanged}, property types changed ${result.summary.schema.propertyTypesChanged}, wire names changed ${result.summary.schema.wireNamesChanged}, discriminators changed ${result.summary.schema.discriminatorsChanged}, enum values changed ${result.summary.schema.enumValuesChanged}`,
95
+ `breaking change classification: ${result.breakingChange}`,
96
+ ];
97
+ const blocking = result.failures.filter((failure) => failure.blocking).sort(compareFailures);
98
+ if (blocking.length > 0) {
99
+ lines.push("blocking failures:");
100
+ for (const failure of blocking) {
101
+ lines.push(`- [${failure.code}] ${failure.message}`);
102
+ }
103
+ }
104
+ return `${lines.join("\n")}\n`;
105
+ }
106
+ function compareSnapshotIdentity(baseline, current, summary, failures) {
107
+ if (baseline.emitter !== current.emitter || baseline.version !== current.version) {
108
+ addFailure(failures, "snapshot.identity", `Snapshot identity changed from ${baseline.emitter}@${baseline.version} to ${current.emitter}@${current.version}.`);
109
+ }
110
+ if (stableStringify(baseline.root) !== stableStringify(current.root)) {
111
+ addFailure(failures, "snapshot.root", `Root metadata changed from ${stableStringify(baseline.root)} to ${stableStringify(current.root)}.`);
112
+ }
113
+ const baselineTargets = mapTargets(baseline.targets);
114
+ const currentTargets = mapTargets(current.targets);
115
+ for (const target of sortedUnion([...baselineTargets.keys()], [...currentTargets.keys()])) {
116
+ const left = baselineTargets.get(target);
117
+ const right = currentTargets.get(target);
118
+ if (!left || !right) {
119
+ summary.packageNamesChanged += 1;
120
+ addFailure(failures, "target.set", `Target set changed for ${target}.`);
121
+ continue;
122
+ }
123
+ if ((left.packageName ?? "") !== (right.packageName ?? "")) {
124
+ summary.packageNamesChanged += 1;
125
+ addFailure(failures, "target.package", `${target} package name changed from ${left.packageName ?? "<none>"} to ${right.packageName ?? "<none>"}.`);
126
+ }
127
+ if ((left.namespace ?? "") !== (right.namespace ?? "")) {
128
+ summary.packageNamesChanged += 1;
129
+ addFailure(failures, "target.namespace", `${target} namespace changed from ${left.namespace ?? "<none>"} to ${right.namespace ?? "<none>"}.`);
130
+ }
131
+ if (stableStringify(left.modules) !== stableStringify(right.modules)) {
132
+ summary.modulesChanged += 1;
133
+ addFailure(failures, "target.modules", `${target} module list changed.`);
134
+ }
135
+ }
136
+ }
137
+ function compareToolchain(baseline, current, summary, failures) {
138
+ const baselinePackages = new Map(baseline.toolchain.packages.map((entry) => [entry.name, entry]));
139
+ const currentPackages = new Map(current.toolchain.packages.map((entry) => [entry.name, entry]));
140
+ for (const name of sortedUnion([...baselinePackages.keys()], [...currentPackages.keys()])) {
141
+ const left = baselinePackages.get(name);
142
+ const right = currentPackages.get(name);
143
+ if (!left || !right || stableStringify(left) !== stableStringify(right)) {
144
+ summary.toolchain.changed += 1;
145
+ addFailure(failures, "toolchain.changed", `${name} toolchain metadata changed.`);
146
+ }
147
+ if (right && !right.supported) {
148
+ summary.toolchain.unsupported += 1;
149
+ addFailure(failures, "toolchain.unsupported", `${name}@${right.version} is outside supported range ${right.supportedRange}.`);
150
+ }
151
+ }
152
+ }
153
+ function compareExports(baseline, current, summary, failures) {
154
+ const baselineExports = mapExports(baseline.targets);
155
+ const currentExports = mapExports(current.targets);
156
+ for (const key of sortedUnion([...baselineExports.keys()], [...currentExports.keys()])) {
157
+ const left = baselineExports.get(key);
158
+ const right = currentExports.get(key);
159
+ if (!left && right) {
160
+ summary.exports.added += 1;
161
+ }
162
+ else if (left && !right) {
163
+ summary.exports.removed += 1;
164
+ addFailure(failures, "exports.removed", `${key} was removed.`);
165
+ }
166
+ else if (left && right && exportChanged(left.entry, right.entry)) {
167
+ summary.exports.changed += 1;
168
+ addFailure(failures, "exports.changed", `${key} changed from ${exportSignature(left.entry)} to ${exportSignature(right.entry)}.`);
169
+ }
170
+ }
171
+ }
172
+ function compareProtocols(baseline, current, summary, failures) {
173
+ const baselineProtocols = mapProtocols(baseline.targets);
174
+ const currentProtocols = mapProtocols(current.targets);
175
+ for (const key of sortedUnion([...baselineProtocols.keys()], [...currentProtocols.keys()])) {
176
+ const left = baselineProtocols.get(key);
177
+ const right = currentProtocols.get(key);
178
+ if (!left && right) {
179
+ summary.protocols.added += 1;
180
+ }
181
+ else if (left && !right) {
182
+ summary.protocols.removed += 1;
183
+ addFailure(failures, "protocols.removed", `${key} was removed.`);
184
+ }
185
+ else if (left && right && protocolSignature(left.protocol) !== protocolSignature(right.protocol)) {
186
+ summary.protocols.changed += 1;
187
+ addFailure(failures, "protocols.changed", `${key} signature changed.`);
188
+ }
189
+ }
190
+ }
191
+ function compareManifest(baseline, current, summary, failures) {
192
+ if (baseline.emitter !== current.emitter || baseline.version !== current.version) {
193
+ addFailure(failures, "manifest.identity", `Manifest identity changed from ${baseline.emitter}@${baseline.version} to ${current.emitter}@${current.version}.`);
194
+ }
195
+ const baselineFiles = new Map(baseline.files.map((entry) => [normalizePath(entry.path), entry]));
196
+ const currentFiles = new Map(current.files.map((entry) => [normalizePath(entry.path), entry]));
197
+ for (const filePath of sortedUnion([...baselineFiles.keys()], [...currentFiles.keys()])) {
198
+ const left = baselineFiles.get(filePath);
199
+ const right = currentFiles.get(filePath);
200
+ if (!left && right) {
201
+ summary.files.added += 1;
202
+ }
203
+ else if (left && !right) {
204
+ summary.files.deleted += 1;
205
+ addFailure(failures, "files.deleted", `${filePath} was deleted from generated manifest.`);
206
+ }
207
+ else if (left && right && manifestOwnershipChanged(left, right)) {
208
+ summary.files.ownershipChanged += 1;
209
+ addFailure(failures, "files.ownership", `${filePath} generated ownership metadata changed.`);
210
+ }
211
+ }
212
+ }
213
+ function compareProtectedPaths(manifest, baselineHydration, hydration, config, summary, failures) {
214
+ const protectedPaths = getProtectedPathPatterns(config, baselineHydration, hydration);
215
+ if (protectedPaths.length === 0)
216
+ return;
217
+ for (const entry of manifest.files) {
218
+ const filePath = normalizePath(entry.path);
219
+ if (protectedPaths.some((pattern) => pattern.test(filePath))) {
220
+ summary.protectedPathTouches += 1;
221
+ addFailure(failures, "protected-path.touch", `${filePath} matches a protected path.`);
222
+ }
223
+ }
224
+ }
225
+ function compareHydrationBoundaryMetadata(baseline, current, failures) {
226
+ if (!baseline && !current)
227
+ return;
228
+ if (!baseline && current) {
229
+ return;
230
+ }
231
+ if (baseline && !current) {
232
+ addFailure(failures, "hydration-boundary.changed", "Hydration boundary metadata was removed.");
233
+ return;
234
+ }
235
+ if (!baseline || !current)
236
+ return;
237
+ if (baseline.emitter !== current.emitter || baseline.version !== current.version) {
238
+ addFailure(failures, "hydration-boundary.changed", "Hydration boundary metadata identity changed.");
239
+ }
240
+ if (stableStringify(baseline.protectedPaths) !== stableStringify(current.protectedPaths)) {
241
+ addFailure(failures, "hydration-boundary.protected-paths", "Hydration boundary protected paths changed.");
242
+ }
243
+ if (stableStringify(baseline.hydrationZones) !== stableStringify(current.hydrationZones)) {
244
+ addFailure(failures, "hydration-boundary.zones", "Hydration zones changed.");
245
+ }
246
+ if (stableStringify(baseline.seams) !== stableStringify(current.seams)) {
247
+ addFailure(failures, "hydration-boundary.seams", "Hydration seams changed.");
248
+ }
249
+ }
250
+ function compareHydrationZones(manifest, hydration, config, summary) {
251
+ const configuredZones = [...(config.hydrationZones ?? []), ...(hydration?.hydrationZones ?? [])].map((entry) => globToRegExp(normalizePath(entry)));
252
+ if (configuredZones.length === 0)
253
+ return;
254
+ for (const entry of manifest.files) {
255
+ const filePath = normalizePath(entry.path);
256
+ if (configuredZones.some((pattern) => pattern.test(filePath))) {
257
+ summary.hydrationZoneTouches += 1;
258
+ }
259
+ }
260
+ }
261
+ function compareSchemaEvolution(baseline, current, summary, failures) {
262
+ if (!baseline && !current)
263
+ return [];
264
+ if (!baseline || !current) {
265
+ addFailure(failures, "schema.missing-model", `Schema evolution could not run because ${baseline ? "current" : "baseline"} json-ast/model.json is missing.`);
266
+ return [
267
+ {
268
+ kind: baseline ? "type-removed" : "type-added",
269
+ path: "json-ast/model.json",
270
+ severity: "major",
271
+ message: `${baseline ? "Current" : "Baseline"} json-ast/model.json is missing.`,
272
+ },
273
+ ];
274
+ }
275
+ const changes = [];
276
+ const baselineTypes = flattenSchemaTypes(baseline);
277
+ const currentTypes = flattenSchemaTypes(current);
278
+ for (const typeName of sortedUnion([...baselineTypes.keys()], [...currentTypes.keys()])) {
279
+ const left = baselineTypes.get(typeName);
280
+ const right = currentTypes.get(typeName);
281
+ if (!left && right) {
282
+ summary.schema.addedTypes += 1;
283
+ changes.push({
284
+ kind: "type-added",
285
+ path: typeName,
286
+ severity: "minor",
287
+ message: `${typeName} was added.`,
288
+ });
289
+ continue;
290
+ }
291
+ if (left && !right) {
292
+ summary.schema.removedTypes += 1;
293
+ changes.push({
294
+ kind: "type-removed",
295
+ path: typeName,
296
+ severity: "major",
297
+ message: `${typeName} was removed.`,
298
+ });
299
+ addFailure(failures, "schema.type-removed", `${typeName} was removed.`);
300
+ continue;
301
+ }
302
+ if (!left || !right)
303
+ continue;
304
+ if ((left.discriminator ?? "") !== (right.discriminator ?? "")) {
305
+ summary.schema.discriminatorsChanged += 1;
306
+ changes.push({
307
+ kind: "type-discriminator-changed",
308
+ path: typeName,
309
+ severity: "major",
310
+ message: `${typeName} discriminator changed from ${left.discriminator ?? "<none>"} to ${right.discriminator ?? "<none>"}.`,
311
+ });
312
+ addFailure(failures, "schema.discriminator", `${typeName} discriminator changed.`);
313
+ }
314
+ compareSchemaProperties(typeName, left, right, summary, failures, changes);
315
+ }
316
+ return changes.sort((left, right) => `${left.kind}:${left.path}`.localeCompare(`${right.kind}:${right.path}`));
317
+ }
318
+ function compareSchemaProperties(typeName, baseline, current, summary, failures, changes) {
319
+ const baselineProperties = mapSchemaProperties(baseline);
320
+ const currentProperties = mapSchemaProperties(current);
321
+ for (const propertyName of sortedUnion([...baselineProperties.keys()], [...currentProperties.keys()])) {
322
+ const left = baselineProperties.get(propertyName);
323
+ const right = currentProperties.get(propertyName);
324
+ const pathName = `${typeName}.${propertyName}`;
325
+ if (!left && right) {
326
+ if (right.isOptional) {
327
+ summary.schema.addedOptionalProperties += 1;
328
+ changes.push({
329
+ kind: "property-added-optional",
330
+ path: pathName,
331
+ severity: "minor",
332
+ message: `${pathName} optional property was added.`,
333
+ });
334
+ }
335
+ else {
336
+ summary.schema.addedRequiredProperties += 1;
337
+ changes.push({
338
+ kind: "property-added-required",
339
+ path: pathName,
340
+ severity: "major",
341
+ message: `${pathName} required property was added.`,
342
+ });
343
+ addFailure(failures, "schema.required-added", `${pathName} required property was added.`);
344
+ }
345
+ continue;
346
+ }
347
+ if (left && !right) {
348
+ summary.schema.removedProperties += 1;
349
+ changes.push({
350
+ kind: "property-removed",
351
+ path: pathName,
352
+ severity: "major",
353
+ message: `${pathName} property was removed.`,
354
+ });
355
+ addFailure(failures, "schema.property-removed", `${pathName} property was removed.`);
356
+ continue;
357
+ }
358
+ if (!left || !right)
359
+ continue;
360
+ if ((left.isOptional ?? false) !== (right.isOptional ?? false)) {
361
+ summary.schema.requirednessChanged += 1;
362
+ changes.push({
363
+ kind: "property-requiredness-changed",
364
+ path: pathName,
365
+ severity: "major",
366
+ message: `${pathName} requiredness changed.`,
367
+ });
368
+ addFailure(failures, "schema.requiredness", `${pathName} requiredness changed.`);
369
+ }
370
+ if (stableStringify(propertyTypeSignature(left)) !== stableStringify(propertyTypeSignature(right))) {
371
+ summary.schema.propertyTypesChanged += 1;
372
+ changes.push({
373
+ kind: "property-type-changed",
374
+ path: pathName,
375
+ severity: "major",
376
+ message: `${pathName} type shape changed.`,
377
+ });
378
+ addFailure(failures, "schema.property-type", `${pathName} type shape changed.`);
379
+ }
380
+ if (stableStringify(normalizeKnownAs(left.knownAs)) !== stableStringify(normalizeKnownAs(right.knownAs))) {
381
+ summary.schema.wireNamesChanged += 1;
382
+ changes.push({
383
+ kind: "property-wire-name-changed",
384
+ path: pathName,
385
+ severity: "major",
386
+ message: `${pathName} wire-name mappings changed.`,
387
+ });
388
+ addFailure(failures, "schema.wire-name", `${pathName} wire-name mappings changed.`);
389
+ }
390
+ if (stableStringify(enumSignature(left)) !== stableStringify(enumSignature(right))) {
391
+ summary.schema.enumValuesChanged += 1;
392
+ changes.push({
393
+ kind: "property-enum-values-changed",
394
+ path: pathName,
395
+ severity: "major",
396
+ message: `${pathName} enum values changed.`,
397
+ });
398
+ addFailure(failures, "schema.enum", `${pathName} enum values changed.`);
399
+ }
400
+ }
401
+ }
402
+ function buildConformanceMap(snapshot) {
403
+ const contracts = new Map();
404
+ for (const target of snapshot.targets) {
405
+ for (const entry of target.exports) {
406
+ const key = `${entry.protocol ? "protocol" : "type"}:${entry.name}`;
407
+ const mapEntry = contracts.get(key) ?? {
408
+ contract: entry.name,
409
+ protocol: entry.protocol,
410
+ targets: [],
411
+ };
412
+ mapEntry.targets.push({
413
+ target: target.target,
414
+ symbol: entry.name,
415
+ source: entry.source,
416
+ packageName: target.packageName,
417
+ namespace: target.namespace,
418
+ outputRoot: target.outputRoot,
419
+ modules: target.modules,
420
+ exported: target.rootExports.includes(entry.name),
421
+ });
422
+ contracts.set(key, mapEntry);
423
+ }
424
+ }
425
+ return Array.from(contracts.values())
426
+ .map((entry) => ({
427
+ ...entry,
428
+ targets: entry.targets.sort((left, right) => left.target.localeCompare(right.target)),
429
+ }))
430
+ .sort((left, right) => `${left.protocol}:${left.contract}`.localeCompare(`${right.protocol}:${right.contract}`));
431
+ }
432
+ function buildStaleCleanupDryRun(baseline, current, config, baselineHydration, hydration) {
433
+ const currentFiles = new Set(current.files.map((entry) => normalizePath(entry.path)));
434
+ const protectedPaths = getProtectedPathPatterns(config, baselineHydration, hydration);
435
+ const hydrationZones = [...(config.hydrationZones ?? []), ...(baselineHydration?.hydrationZones ?? []), ...(hydration?.hydrationZones ?? [])]
436
+ .map((entry) => globToRegExp(normalizePath(entry)));
437
+ return baseline.files
438
+ .filter((entry) => !currentFiles.has(normalizePath(entry.path)))
439
+ .map((entry) => {
440
+ const filePath = normalizePath(entry.path);
441
+ const reasons = [
442
+ "present in prior generated manifest",
443
+ entry.marker ? "prior entry was marked generated" : "prior entry was not marked generated",
444
+ `scoped to output root ${normalizePath(entry.outputRoot)}`,
445
+ ];
446
+ const protectedMatch = protectedPaths.some((pattern) => pattern.test(filePath));
447
+ const hydrationZoneMatch = hydrationZones.some((pattern) => pattern.test(filePath));
448
+ reasons.push(protectedMatch ? "blocked by protected path" : "not protected");
449
+ if (hydrationZoneMatch)
450
+ reasons.push("inside hydration zone");
451
+ return {
452
+ path: filePath,
453
+ reasons,
454
+ safe: entry.marker && !protectedMatch && !hydrationZoneMatch,
455
+ };
456
+ })
457
+ .sort((left, right) => left.path.localeCompare(right.path));
458
+ }
459
+ function buildHydrationBoundaryReport(hydration, config) {
460
+ return {
461
+ protectedPaths: uniqueSorted([...(config.protectedPaths ?? []), ...(hydration?.protectedPaths ?? [])]),
462
+ hydrationZones: uniqueSorted([...(config.hydrationZones ?? []), ...(hydration?.hydrationZones ?? [])]),
463
+ seams: [...(hydration?.seams ?? [])].sort((left, right) => `${left.target}:${left.group}:${left.contract}:${left.symbol}`.localeCompare(`${right.target}:${right.group}:${right.contract}:${right.symbol}`)),
464
+ };
465
+ }
466
+ function getProtectedPathPatterns(config, baselineHydration, hydration) {
467
+ return [...(config.protectedPaths ?? []), ...(baselineHydration?.protectedPaths ?? []), ...(hydration?.protectedPaths ?? [])]
468
+ .map((entry) => globToRegExp(normalizePath(entry)));
469
+ }
470
+ function classifyBreakingChange(summary, schemaEvolution, failures) {
471
+ if (failures.some((failure) => failure.blocking) || schemaEvolution.some((change) => change.severity === "major")) {
472
+ return "major";
473
+ }
474
+ if (summary.exports.added > 0 ||
475
+ summary.protocols.added > 0 ||
476
+ summary.files.added > 0 ||
477
+ summary.schema.addedOptionalProperties > 0 ||
478
+ summary.schema.addedTypes > 0) {
479
+ return "minor";
480
+ }
481
+ return "patch";
482
+ }
483
+ function metadataFile(root, fileName) {
484
+ const direct = path.join(root, fileName);
485
+ if (existsSync(direct))
486
+ return direct;
487
+ return path.join(root, ".typra-generated", fileName);
488
+ }
489
+ function modelFile(root) {
490
+ const direct = path.join(root, "json-ast", "model.json");
491
+ if (existsSync(direct))
492
+ return direct;
493
+ const sibling = path.join(root, "..", "json-ast", "model.json");
494
+ if (existsSync(sibling))
495
+ return sibling;
496
+ return direct;
497
+ }
498
+ function readJson(filePath) {
499
+ if (!existsSync(filePath)) {
500
+ throw new Error(`Missing Typra verifier input: ${filePath}`);
501
+ }
502
+ return JSON.parse(readFileSync(filePath, "utf8"));
503
+ }
504
+ function readOptionalJson(filePath) {
505
+ if (!existsSync(filePath)) {
506
+ return undefined;
507
+ }
508
+ return JSON.parse(readFileSync(filePath, "utf8"));
509
+ }
510
+ function mapTargets(targets) {
511
+ return new Map(targets.map((target) => [target.target, target]));
512
+ }
513
+ function mapExports(targets) {
514
+ const entries = new Map();
515
+ for (const target of targets) {
516
+ for (const entry of target.exports) {
517
+ entries.set(`${target.target}:${entry.group}:${entry.name}`, { target: target.target, entry });
518
+ }
519
+ }
520
+ return entries;
521
+ }
522
+ function mapProtocols(targets) {
523
+ const entries = new Map();
524
+ for (const target of targets) {
525
+ for (const protocol of target.protocols) {
526
+ entries.set(`${target.target}:${protocol.group}:${protocol.name}`, { target: target.target, protocol });
527
+ }
528
+ }
529
+ return entries;
530
+ }
531
+ function exportChanged(left, right) {
532
+ return left.kind !== right.kind || left.source !== right.source || left.protocol !== right.protocol;
533
+ }
534
+ function exportSignature(entry) {
535
+ return stableStringify({
536
+ kind: entry.kind,
537
+ protocol: entry.protocol,
538
+ source: entry.source,
539
+ });
540
+ }
541
+ function protocolSignature(protocol) {
542
+ return stableStringify({
543
+ methods: protocol.methods,
544
+ source: protocol.source,
545
+ symbol: protocol.symbol,
546
+ });
547
+ }
548
+ function manifestOwnershipChanged(left, right) {
549
+ return left.marker !== right.marker || normalizePath(left.outputRoot) !== normalizePath(right.outputRoot);
550
+ }
551
+ function addFailure(failures, code, message) {
552
+ failures.push({ code, message, blocking: true });
553
+ }
554
+ function cloneSummary() {
555
+ return JSON.parse(JSON.stringify(EMPTY_SUMMARY));
556
+ }
557
+ function sortedUnion(left, right) {
558
+ return Array.from(new Set([...left, ...right])).sort((a, b) => a.localeCompare(b));
559
+ }
560
+ function compareFailures(left, right) {
561
+ const byCode = left.code.localeCompare(right.code);
562
+ if (byCode !== 0)
563
+ return byCode;
564
+ return left.message.localeCompare(right.message);
565
+ }
566
+ function normalizePath(filePath) {
567
+ return filePath.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "");
568
+ }
569
+ function flattenSchemaTypes(root) {
570
+ const types = new Map();
571
+ const visit = (node) => {
572
+ if (!node)
573
+ return;
574
+ const key = schemaTypeKey(node);
575
+ if (key && !types.has(key)) {
576
+ types.set(key, node);
577
+ }
578
+ for (const child of node.childTypes ?? [])
579
+ visit(child);
580
+ for (const property of node.properties ?? [])
581
+ visit(property.type);
582
+ };
583
+ visit(root);
584
+ return types;
585
+ }
586
+ function schemaTypeKey(node) {
587
+ const namespace = node.typeName?.namespace ?? "";
588
+ const name = node.typeName?.name ?? "";
589
+ return namespace ? `${namespace}.${name}` : name;
590
+ }
591
+ function mapSchemaProperties(node) {
592
+ return new Map((node.properties ?? []).filter((property) => !!property.name).map((property) => [property.name, property]));
593
+ }
594
+ function normalizeKnownAs(knownAs) {
595
+ return (knownAs ?? [])
596
+ .map((entry) => ({
597
+ provider: entry.provider ?? "",
598
+ name: entry.name ?? "",
599
+ }))
600
+ .sort((left, right) => `${left.provider}:${left.name}`.localeCompare(`${right.provider}:${right.name}`));
601
+ }
602
+ function propertyTypeSignature(property) {
603
+ return {
604
+ typeName: {
605
+ namespace: property.typeName?.namespace ?? "",
606
+ name: property.typeName?.name ?? "",
607
+ },
608
+ isScalar: property.isScalar ?? false,
609
+ isCollection: property.isCollection ?? false,
610
+ isAny: property.isAny ?? false,
611
+ isDict: property.isDict ?? false,
612
+ };
613
+ }
614
+ function enumSignature(property) {
615
+ return {
616
+ allowedValues: [...(property.allowedValues ?? [])].sort((left, right) => left.localeCompare(right)),
617
+ enumName: property.enumName ?? "",
618
+ isOpenEnum: property.isOpenEnum ?? false,
619
+ };
620
+ }
621
+ function uniqueSorted(values) {
622
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
623
+ }
624
+ function globToRegExp(pattern) {
625
+ let source = "^";
626
+ for (let index = 0; index < pattern.length; index += 1) {
627
+ const char = pattern[index];
628
+ const next = pattern[index + 1];
629
+ const afterNext = pattern[index + 2];
630
+ if (char === "*" && next === "*" && afterNext === "/") {
631
+ source += "(?:.*/)?";
632
+ index += 2;
633
+ }
634
+ else if (char === "*" && next === "*") {
635
+ source += ".*";
636
+ index += 1;
637
+ }
638
+ else if (char === "*") {
639
+ source += "[^/]*";
640
+ }
641
+ else if (char === "?") {
642
+ source += "[^/]";
643
+ }
644
+ else {
645
+ source += escapeRegExp(char);
646
+ }
647
+ }
648
+ return new RegExp(`${source}$`);
649
+ }
650
+ function escapeRegExp(value) {
651
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
652
+ }
653
+ function stableStringify(value) {
654
+ return JSON.stringify(sortValue(value));
655
+ }
656
+ function sortValue(value) {
657
+ if (Array.isArray(value)) {
658
+ return value.map(sortValue);
659
+ }
660
+ if (value && typeof value === "object") {
661
+ return Object.fromEntries(Object.entries(value)
662
+ .sort(([left], [right]) => left.localeCompare(right))
663
+ .map(([key, entry]) => [key, sortValue(entry)]));
664
+ }
665
+ return value;
666
+ }
667
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};