breakage 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +164 -46
  2. package/dist/breakage-DIY49fzP.js +1349 -0
  3. package/dist/breakage-DIY49fzP.js.map +1 -0
  4. package/dist/breakage.d.ts +179 -11
  5. package/dist/breakage.d.ts.map +1 -1
  6. package/dist/breakage.js +3 -1
  7. package/dist/breakage.x.js +169 -67
  8. package/dist/breakage.x.js.map +1 -1
  9. package/dist/changesets.d.ts +8 -0
  10. package/dist/changesets.d.ts.map +1 -0
  11. package/dist/changesets.js +27 -0
  12. package/dist/changesets.js.map +1 -0
  13. package/dist/release-BsdIDT4g.js +69 -0
  14. package/dist/release-BsdIDT4g.js.map +1 -0
  15. package/dist/release-D47BqRuB.d.ts +40 -0
  16. package/dist/release-D47BqRuB.d.ts.map +1 -0
  17. package/npm.md +236 -0
  18. package/package.json +51 -11
  19. package/src/breakage.ts +136 -70
  20. package/src/breakage.x.ts +160 -112
  21. package/src/changesets.ts +51 -0
  22. package/src/config.ts +128 -0
  23. package/src/npm/check.ts +449 -0
  24. package/src/npm/cli.ts +130 -0
  25. package/src/npm/config.ts +118 -0
  26. package/src/npm/environment.ts +661 -0
  27. package/src/npm/manifest.ts +134 -0
  28. package/src/npm/release.ts +173 -0
  29. package/src/npm/types.ts +64 -0
  30. package/src/release.ts +153 -0
  31. package/src/task.ts +54 -0
  32. package/dist/breakage-Dc4IldeW.js +0 -435
  33. package/dist/breakage-Dc4IldeW.js.map +0 -1
  34. package/dist/breakage.$configPath.schema.json +0 -33
  35. package/dist/breakage.help.schema.json +0 -6
  36. package/dist/breakage.main.schema.json +0 -33
  37. package/dist/breakage.prelude.$configPath.schema.json +0 -31
  38. package/dist/breakage.prelude.schema.json +0 -31
  39. package/dist/breakage.schema.schema.json +0 -10
@@ -0,0 +1,1349 @@
1
+ import { a as validateRelease, n as releaseSchema, o as versionDelta, r as resolveReleaseDelta, t as rejectCertificationOptions } from "./release-BsdIDT4g.js";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { minimatch } from "minimatch";
5
+ import { simpleGit } from "simple-git";
6
+ import logger from "takua";
7
+ import { spawn } from "node:child_process";
8
+ import * as fs from "node:fs/promises";
9
+ import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
10
+ import semver from "semver";
11
+ import { type } from "arktype";
12
+ import { randomUUID } from "node:crypto";
13
+ import { setTimeout } from "node:timers/promises";
14
+ import { existsSync } from "node:fs";
15
+ import { cli, options } from "comline";
16
+ // src/task.ts
17
+ /** Capture output per environment so parallel runs cannot interleave their reports. */
18
+ async function command(commandLine, cwd) {
19
+ if (typeof commandLine !== `string` || !commandLine.trim()) throw new Error(`Commands must be nonempty shell strings.`);
20
+ return await new Promise((resolve, reject) => {
21
+ const child = spawn(commandLine, {
22
+ cwd,
23
+ shell: true,
24
+ env: {
25
+ ...process.env,
26
+ NODE_PATH: ``,
27
+ PATH: [
28
+ path.join(cwd, `node_modules/.bin`),
29
+ path.join(cwd, `../node_modules/.bin`),
30
+ process.env.PATH
31
+ ].join(path.delimiter)
32
+ },
33
+ stdio: [
34
+ `ignore`,
35
+ `pipe`,
36
+ `pipe`
37
+ ]
38
+ });
39
+ let output = ``;
40
+ const collect = (chunk) => {
41
+ output = (output + chunk.toString()).slice(-1e6);
42
+ };
43
+ child.stdout.on(`data`, collect);
44
+ child.stderr.on(`data`, collect);
45
+ child.on(`error`, reject);
46
+ child.on(`close`, (code, signal) => resolve({
47
+ code: signal ? 2 : code ?? 2,
48
+ output
49
+ }));
50
+ });
51
+ }
52
+ async function prepare(commandLine, cwd) {
53
+ const result = await command(commandLine, cwd);
54
+ if (result.code !== 0) throw new Error(`Command failed (${result.code}) in ${cwd}: ${commandLine}\n${result.output}`);
55
+ }
56
+ function errorMessage(error) {
57
+ return error instanceof Error ? `${error.message}${error.cause ? `\n${errorMessage(error.cause)}` : ``}` : String(error);
58
+ }
59
+ // src/npm/manifest.ts
60
+ function record(value) {
61
+ return typeof value === `object` && value !== null && !Array.isArray(value);
62
+ }
63
+ function packageName(value) {
64
+ return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
65
+ }
66
+ function parseManifest(value, label) {
67
+ if (!record(value) || typeof value.name !== `string` || !packageName(value.name) || typeof value.version !== `string` || !semver.valid(value.version)) throw new Error(`Invalid package identity in ${label}.`);
68
+ if (value.peerDependenciesMeta !== void 0 && (!record(value.peerDependenciesMeta) || Object.values(value.peerDependenciesMeta).some((entry) => !record(entry) || entry.optional !== void 0 && typeof entry.optional !== `boolean`))) throw new Error(`Invalid peerDependenciesMeta in ${label}.`);
69
+ for (const field of [
70
+ `dependencies`,
71
+ `optionalDependencies`,
72
+ `devDependencies`,
73
+ `peerDependencies`
74
+ ]) {
75
+ const entries = value[field];
76
+ if (entries !== void 0 && (!record(entries) || Object.entries(entries).some(([name, spec]) => !packageName(name) || typeof spec !== `string`))) throw new Error(`Invalid ${field} in ${label}.`);
77
+ }
78
+ if (value.breakage !== void 0 && (!record(value.breakage) || value.breakage.candidates !== void 0 && (!Array.isArray(value.breakage.candidates) || value.breakage.candidates.some((name) => typeof name !== `string` || !packageName(name))))) throw new Error(`Invalid candidate peers in ${label}.`);
79
+ return value;
80
+ }
81
+ async function readManifest(filename) {
82
+ return parseManifest(JSON.parse(await readFile(filename, `utf8`)), filename);
83
+ }
84
+ /** Current support is authored explicitly; only historical shorthand is resolved. */
85
+ async function normalizeWorkspacePeers(manifest, versionAtBaseline) {
86
+ if (!manifest.peerDependencies) return manifest;
87
+ const entries = await Promise.all(Object.entries(manifest.peerDependencies).map(async ([peer, range]) => {
88
+ if (!range.startsWith(`workspace:`)) return [peer, range];
89
+ const requested = range.slice(`workspace:`.length);
90
+ if ([
91
+ ``,
92
+ `*`,
93
+ `^`,
94
+ `~`
95
+ ].includes(requested)) {
96
+ if (!versionAtBaseline) throw new Error(`Moving workspace peer range ${peer}: ${range} in ${manifest.name}. Declare explicit support eras, for example ^1.0.0 || ^2.0.0, so local version changes cannot change the support promise.`);
97
+ const version = await versionAtBaseline(peer);
98
+ if (!semver.valid(version)) throw new Error(`Invalid historical workspace version for ${peer}: ${version}.`);
99
+ return [peer, `${requested === `^` || requested === `~` ? requested : ``}${version}`];
100
+ }
101
+ try {
102
+ eras(requested);
103
+ } catch {
104
+ throw new Error(`Unsupported workspace peer range for ${peer}: ${range}.`);
105
+ }
106
+ return [peer, requested];
107
+ }));
108
+ return {
109
+ ...manifest,
110
+ peerDependencies: Object.fromEntries(entries)
111
+ };
112
+ }
113
+ function eras(range) {
114
+ const parts = range.split(`||`).map((part) => part.trim());
115
+ if (parts.some((part) => !part || semver.validRange(part) === null)) throw new Error(`Invalid peer range: ${range}.`);
116
+ return parts;
117
+ }
118
+ /** Previews only represent an explicitly enrolled exact preview. */
119
+ function matches(version, range) {
120
+ return eras(range).some((era) => semver.prerelease(version) ? semver.valid(era.replace(/^=\s*/, ``)) === semver.valid(version) : semver.satisfies(version, era));
121
+ }
122
+ // src/directory-lock.ts
123
+ async function withDirectoryLock(lock, action, waitForCleanup = false) {
124
+ const deadline = Date.now() + 6e4;
125
+ for (;;) try {
126
+ await mkdir(lock);
127
+ break;
128
+ } catch (thrown) {
129
+ if (thrown.code !== `EEXIST`) throw thrown;
130
+ if (!waitForCleanup && Date.now() >= deadline) throw new Error(`Timed out waiting for ${lock}. Check for an interrupted operation before removing this lock.`);
131
+ await setTimeout(20);
132
+ }
133
+ try {
134
+ return await action();
135
+ } finally {
136
+ await rmdir(lock);
137
+ }
138
+ }
139
+ // src/release-tag.ts
140
+ function compareInteger(a, b) {
141
+ return a.length - b.length || (a > b ? 1 : a < b ? -1 : 0);
142
+ }
143
+ function compareRelease(a, b) {
144
+ for (let index = 0; index < 3; index++) {
145
+ const difference = compareInteger(a.core[index], b.core[index]);
146
+ if (difference) return difference;
147
+ }
148
+ if (!a.prerelease.length || !b.prerelease.length) return Number(!a.prerelease.length) - Number(!b.prerelease.length);
149
+ for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index++) {
150
+ const left = a.prerelease[index];
151
+ const right = b.prerelease[index];
152
+ const leftNumeric = /^\d+$/.test(left);
153
+ const rightNumeric = /^\d+$/.test(right);
154
+ const difference = leftNumeric && rightNumeric ? compareInteger(left, right) : leftNumeric !== rightNumeric ? Number(rightNumeric) - Number(leftNumeric) : left > right ? 1 : left < right ? -1 : 0;
155
+ if (difference) return difference;
156
+ }
157
+ return a.prerelease.length - b.prerelease.length;
158
+ }
159
+ function latestReleaseTag(remoteTags, tagPattern) {
160
+ const pattern = tagPattern ? new RegExp(tagPattern) : void 0;
161
+ let latest;
162
+ for (const line of remoteTags.split(`\n`)) {
163
+ if (pattern && !pattern.test(line)) continue;
164
+ const ref = line.split(`\t`)[1]?.replace(/\^\{\}$/, ``);
165
+ if (!ref?.startsWith(`refs/tags/`)) continue;
166
+ const name = ref.slice(`refs/tags/`.length);
167
+ const version = name.slice(name.lastIndexOf(`@`) + 1);
168
+ const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
169
+ if (!match) continue;
170
+ const prerelease = match[4]?.split(`.`) ?? [];
171
+ if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) continue;
172
+ const release = {
173
+ ref,
174
+ core: match.slice(1, 4),
175
+ prerelease
176
+ };
177
+ if (!latest || compareRelease(release, latest) > 0) latest = release;
178
+ }
179
+ return latest?.ref;
180
+ }
181
+ // src/baseline.ts
182
+ const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
183
+ function isBaseline(value) {
184
+ if (typeof value !== `object` || value === null) return false;
185
+ const record = value;
186
+ return record.version === 1 && (record.tagPattern === null || typeof record.tagPattern === `string`) && typeof record.packageDirectory === `string` && typeof record.ref === `string` && record.ref.startsWith(`refs/tags/`) && typeof record.commit === `string` && objectId.test(record.commit);
187
+ }
188
+ /** Resolve and fetch a release without running tests or changing the worktree. */
189
+ async function breakagePrelude({ out, tagPattern, baseDirname = process.cwd() }) {
190
+ const filename = path.resolve(baseDirname, out);
191
+ const temporaryPath = `${filename}.${randomUUID()}.tmp`;
192
+ let canInvalidate = false;
193
+ try {
194
+ const git = simpleGit(baseDirname);
195
+ if (!(await git.raw([
196
+ `check-ignore`,
197
+ `--`,
198
+ filename
199
+ ])).trim()) throw new Error(`Baseline output must be gitignored: ${filename}.`);
200
+ canInvalidate = true;
201
+ const remoteTags = await git.listRemote([`--tags`, `origin`]);
202
+ const ref = latestReleaseTag(remoteTags, tagPattern);
203
+ if (!ref) throw new Error(`No tags found matching the pattern "${tagPattern}".`);
204
+ const refs = new Map(remoteTags.trim().split(`\n`).map((line) => {
205
+ const [sha, name] = line.split(`\t`);
206
+ return [name, sha];
207
+ }));
208
+ const commit = refs.get(`${ref}^{}`) ?? refs.get(ref);
209
+ if (!commit || !objectId.test(commit)) throw new Error(`Could not resolve the release commit for ${ref}.`);
210
+ const commonGitDirectory = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
211
+ await withDirectoryLock(path.join(commonGitDirectory, `breakage-fetch.lock`), () => git.fetch([
212
+ `origin`,
213
+ commit,
214
+ `--no-tags`,
215
+ `--no-write-fetch-head`
216
+ ]));
217
+ if ((await git.raw([
218
+ `cat-file`,
219
+ `-t`,
220
+ commit
221
+ ])).trim() !== `commit`) throw new Error(`Release ${ref} does not resolve to a commit.`);
222
+ const baseline = {
223
+ version: 1,
224
+ tagPattern: tagPattern ?? null,
225
+ packageDirectory: await git.revparse([`--show-prefix`]),
226
+ ref,
227
+ commit
228
+ };
229
+ await mkdir(path.dirname(filename), { recursive: true });
230
+ await writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\n`, { flag: `wx` });
231
+ await rename(temporaryPath, filename);
232
+ return baseline;
233
+ } catch (cause) {
234
+ if (canInvalidate) await rm(filename, { force: true });
235
+ throw new Error(`Failed to prepare baseline ${filename}.`, { cause });
236
+ } finally {
237
+ await rm(temporaryPath, { force: true });
238
+ }
239
+ }
240
+ async function readBaseline(git, filename, { tagPattern }) {
241
+ try {
242
+ const baseline = JSON.parse(await readFile(filename, `utf8`));
243
+ if (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`);
244
+ if (baseline.tagPattern !== (tagPattern ?? null) || baseline.packageDirectory !== await git.revparse([`--show-prefix`])) throw new Error(`Baseline belongs to a different package or tag pattern.`);
245
+ if ((await git.raw([
246
+ `check-ref-format`,
247
+ `--normalize`,
248
+ baseline.ref
249
+ ])).trim() !== baseline.ref) throw new Error(`Invalid release ref.`);
250
+ if ((await git.raw([
251
+ `cat-file`,
252
+ `-t`,
253
+ baseline.commit
254
+ ])).trim() !== `commit`) throw new Error(`Pinned object is not a locally available commit.`);
255
+ return baseline;
256
+ } catch (cause) {
257
+ throw new Error(`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`, { cause });
258
+ }
259
+ }
260
+ // src/test-file-state.ts
261
+ function isActiveCheck(value) {
262
+ if (typeof value !== `object` || value === null) return false;
263
+ const record = value;
264
+ return typeof record.pid === `number` && Number.isSafeInteger(record.pid) && record.pid > 0 && typeof record.head === `string` && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(record.head) && (record.recoveryRequired === void 0 || typeof record.recoveryRequired === `boolean`) && Array.isArray(record.paths) && record.paths.length > 0 && record.paths.every((file) => typeof file === `string` && !path.isAbsolute(file) && !path.win32.isAbsolute(file) && file.split(`/`).every((part) => part !== `` && part !== `.` && part !== `..`));
265
+ }
266
+ async function readRecoveryRecord(recordPath) {
267
+ try {
268
+ const record = JSON.parse(await readFile(recordPath, `utf8`));
269
+ if (!isActiveCheck(record)) throw new TypeError(`Invalid recovery record fields`);
270
+ return record;
271
+ } catch (cause) {
272
+ throw new Error(`A breakage run needs recovery. The record ${recordPath} is unreadable or invalid. Verify no check is running and recover its test files before removing this record.`, { cause });
273
+ }
274
+ }
275
+ async function writeRecoveryRecord(recordPath, record) {
276
+ const temporaryPath = `${recordPath}.${randomUUID()}.tmp`;
277
+ try {
278
+ await writeFile(temporaryPath, JSON.stringify(record), { flag: `wx` });
279
+ await rename(temporaryPath, recordPath);
280
+ } finally {
281
+ await rm(temporaryPath, { force: true });
282
+ }
283
+ }
284
+ function assertContained(root, filename) {
285
+ const relative = path.relative(root, filename);
286
+ if (relative === `..` || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`Cannot replace tests outside the worktree: ${filename}.`);
287
+ }
288
+ async function validateParents(root, filenames) {
289
+ const checked = /* @__PURE__ */ new Set();
290
+ for (const filename of filenames) {
291
+ assertContained(root, filename);
292
+ for (let parent = path.dirname(filename);; parent = path.dirname(parent)) {
293
+ if (checked.has(parent)) break;
294
+ const stats = await lstat(parent).catch((thrown) => {
295
+ if (thrown.code !== `ENOENT`) throw thrown;
296
+ });
297
+ if (stats && !stats.isDirectory()) throw new Error(`Cannot replace tests through ${parent}.`);
298
+ checked.add(parent);
299
+ if (parent === root) break;
300
+ }
301
+ }
302
+ }
303
+ async function withTestFileState(git, baseDirname, action) {
304
+ const root = await realpath(await git.revparse([`--show-toplevel`]));
305
+ baseDirname = await realpath(baseDirname);
306
+ assertContained(root, baseDirname);
307
+ const stateDirectory = path.join(await git.revparse([`--absolute-git-dir`]), `breakage`);
308
+ await mkdir(stateDirectory, { recursive: true });
309
+ const lock = path.join(stateDirectory, `lock`);
310
+ return withDirectoryLock(lock, async () => {
311
+ const activeChecks = [];
312
+ for (const entry of await readdir(stateDirectory)) {
313
+ if (!entry.endsWith(`.json`)) continue;
314
+ const recordPath = path.join(stateDirectory, entry);
315
+ const record = await readRecoveryRecord(recordPath);
316
+ let interrupted = record.recoveryRequired;
317
+ try {
318
+ process.kill(record.pid, 0);
319
+ } catch (thrown) {
320
+ if (thrown.code !== `EPERM`) interrupted = true;
321
+ }
322
+ if (interrupted) throw new Error(`A breakage run needs recovery. Restore the test paths recorded in ${recordPath} before removing that record.`);
323
+ activeChecks.push(record);
324
+ }
325
+ const activePaths = activeChecks.flatMap((record) => record.paths);
326
+ return action({
327
+ isClean: async () => !await simpleGit(root).raw([
328
+ `--no-optional-locks`,
329
+ `status`,
330
+ `--porcelain`,
331
+ `--untracked-files=all`,
332
+ `--`,
333
+ `.`,
334
+ ...activePaths.map((file) => `:(top,literal,exclude)${file}`)
335
+ ]),
336
+ replaceTests: async (release, files) => {
337
+ const paths = files.map((file) => path.relative(root, path.resolve(baseDirname, file)).split(path.sep).join(`/`));
338
+ for (const file of paths) if (activePaths.some((active) => file === active || file.startsWith(`${active}/`) || active.startsWith(`${file}/`))) throw new Error(`Another breakage run is using ${file}. Concurrent checks must use disjoint test paths.`);
339
+ const head = await git.revparse([`HEAD`]);
340
+ const currentFiles = new Set((await git.raw([
341
+ `ls-tree`,
342
+ `-r`,
343
+ `--name-only`,
344
+ `-z`,
345
+ head
346
+ ])).split(`\0`).filter(Boolean));
347
+ const existing = files.filter((file) => currentFiles.has(file));
348
+ const absent = files.filter((file) => !currentFiles.has(file));
349
+ const modes = /* @__PURE__ */ new Map();
350
+ const filenames = files.map((file) => path.resolve(baseDirname, file));
351
+ await validateParents(root, filenames);
352
+ for (const file of files) {
353
+ const filename = path.resolve(baseDirname, file);
354
+ const stats = await lstat(filename).catch((thrown) => {
355
+ if (thrown.code !== `ENOENT`) throw thrown;
356
+ });
357
+ if (!currentFiles.has(file) && stats) throw new Error(`Cannot overwrite the existing untracked path ${filename}.`);
358
+ if (stats?.isFile()) modes.set(filename, stats.mode);
359
+ }
360
+ const recordPath = path.join(stateDirectory, `${randomUUID()}.json`);
361
+ const record = {
362
+ pid: process.pid,
363
+ paths,
364
+ head
365
+ };
366
+ await writeRecoveryRecord(recordPath, record);
367
+ const restore = async () => {
368
+ await writeRecoveryRecord(recordPath, {
369
+ ...record,
370
+ recoveryRequired: true
371
+ });
372
+ await validateParents(root, filenames);
373
+ if (existing.length) await git.raw([
374
+ `--literal-pathspecs`,
375
+ `restore`,
376
+ `--source=${head}`,
377
+ `--worktree`,
378
+ `--`,
379
+ ...existing
380
+ ]);
381
+ for (const file of absent) await rm(path.resolve(baseDirname, file), { force: true });
382
+ for (const [filename, mode] of modes) await chmod(filename, mode);
383
+ await rm(recordPath);
384
+ };
385
+ try {
386
+ await git.raw([
387
+ `--literal-pathspecs`,
388
+ `restore`,
389
+ `--source=${release}`,
390
+ `--worktree`,
391
+ `--`,
392
+ ...files
393
+ ]);
394
+ } catch (thrown) {
395
+ await restore();
396
+ throw thrown;
397
+ }
398
+ return () => withDirectoryLock(lock, restore, true);
399
+ }
400
+ });
401
+ });
402
+ }
403
+ // src/npm/config.ts
404
+ const npmSuiteSchema = type({
405
+ "+": `reject`,
406
+ files: `string[]`,
407
+ command: `string`
408
+ });
409
+ const npmConfigSchema = type({
410
+ "+": `reject`,
411
+ "release?": releaseSchema,
412
+ "packageDirectory?": `string`,
413
+ artifact: `string`,
414
+ representatives: `string | string[]`,
415
+ lockfile: `string`,
416
+ prepareCommand: `string`,
417
+ "installCommand?": `string`,
418
+ suites: { "[string]": npmSuiteSchema },
419
+ "localPeers?": { "[string]": {
420
+ "+": `reject`,
421
+ directory: `string`,
422
+ artifact: `string`
423
+ } },
424
+ baseline: type({
425
+ "+": `reject`,
426
+ firstRelease: `true`
427
+ }).or({
428
+ "+": `reject`,
429
+ "tagPattern?": `string`,
430
+ "baselineFile?": `string`
431
+ })
432
+ });
433
+ function nonempty(value) {
434
+ return typeof value === `string` && value.trim().length > 0;
435
+ }
436
+ function validateConfig(value) {
437
+ if (!record(value)) throw new Error(`Configuration must have an npm object.`);
438
+ rejectCertificationOptions(value);
439
+ npmConfigSchema.assert(value);
440
+ for (const key of [`artifact`, `lockfile`]) if (!nonempty(value[key])) throw new Error(`npm.${key} must be a nonempty string.`);
441
+ if (path.isAbsolute(value.lockfile) || value.lockfile.split(/[\\/]/).includes(`..`)) throw new Error(`npm.lockfile must stay inside each representative.`);
442
+ if (!nonempty(value.prepareCommand)) throw new Error(`npm.prepareCommand must be a nonempty shell command string.`);
443
+ if (value.installCommand !== void 0 && !nonempty(value.installCommand)) throw new Error(`Invalid npm.installCommand.`);
444
+ for (const key of [`packageDirectory`]) if (value[key] !== void 0 && !nonempty(value[key])) throw new Error(`Invalid npm.${key}.`);
445
+ const globs = typeof value.representatives === `string` ? [value.representatives] : value.representatives;
446
+ if (!Array.isArray(globs) || !globs.length || globs.some((glob) => !nonempty(glob))) throw new Error(`npm.representatives must contain directory globs.`);
447
+ if (!record(value.suites) || !Object.keys(value.suites).length) throw new Error(`Configure at least one npm peer suite.`);
448
+ for (const [peer, suite] of Object.entries(value.suites)) if (!packageName(peer) || !record(suite) || !nonempty(suite.command) || !Array.isArray(suite.files) || !suite.files.length || suite.files.some((file) => !nonempty(file))) throw new Error(`Invalid npm suite for ${peer}.`);
449
+ if (!record(value.baseline) || (`firstRelease` in value.baseline ? value.baseline.firstRelease !== true || Object.keys(value.baseline).length !== 1 : Object.entries(value.baseline).some(([key, item]) => ![`tagPattern`, `baselineFile`].includes(key) || !nonempty(item)))) throw new Error(`Configure npm.baseline with tagPattern/baselineFile or { firstRelease: true }.`);
450
+ if (value.localPeers !== void 0) {
451
+ if (!record(value.localPeers)) throw new Error(`Invalid npm.localPeers.`);
452
+ for (const [peer, local] of Object.entries(value.localPeers)) if (!packageName(peer) || !record(local) || !nonempty(local.directory) || !nonempty(local.artifact)) throw new Error(`Invalid local peer ${peer}.`);
453
+ }
454
+ }
455
+ function validatePeerRanges(peers) {
456
+ for (const range of Object.values(peers)) eras(range);
457
+ }
458
+ // src/config.ts
459
+ const configFields = {
460
+ tagPattern: type(`string`),
461
+ baselineFile: type(`string`),
462
+ testPattern: type(`string`),
463
+ testCommand: type(`string`),
464
+ test: type(`string`),
465
+ baseDirname: type(`string`),
466
+ verbose: type(`boolean`),
467
+ out: type(`string`)
468
+ };
469
+ const configSchema = type({
470
+ "+": `reject`,
471
+ "tagPattern?": configFields.tagPattern.or(`undefined`),
472
+ "baselineFile?": configFields.baselineFile.or(`undefined`),
473
+ "testPattern?": configFields.testPattern,
474
+ "testCommand?": configFields.testCommand,
475
+ "test?": configFields.test,
476
+ "baseDirname?": configFields.baseDirname,
477
+ "verbose?": configFields.verbose.or(`undefined`),
478
+ "out?": configFields.out,
479
+ "release?": releaseSchema,
480
+ "npm?": npmConfigSchema
481
+ });
482
+ /** The complete config, with no fields exposed to argv by this loader. */
483
+ const configOptions = options(`Breakage configuration`, configSchema, {
484
+ tagPattern: { configOnly: true },
485
+ baselineFile: { configOnly: true },
486
+ testPattern: { configOnly: true },
487
+ testCommand: { configOnly: true },
488
+ test: { configOnly: true },
489
+ baseDirname: { configOnly: true },
490
+ verbose: { configOnly: true },
491
+ out: { configOnly: true },
492
+ release: { configOnly: true },
493
+ npm: { configOnly: true }
494
+ });
495
+ /** Share the root release model with npm checks unless they supply their own. */
496
+ function defineConfig(config) {
497
+ if (config && typeof config === `object`) {
498
+ rejectCertificationOptions(config);
499
+ if (config.npm) rejectCertificationOptions(config.npm);
500
+ if (config.release !== void 0) validateRelease(config.release);
501
+ if (config.npm?.release !== void 0) validateRelease(config.npm.release);
502
+ }
503
+ configSchema.assert(config);
504
+ return {
505
+ ...config,
506
+ ...config.npm ? { npm: {
507
+ ...config.npm,
508
+ ...config.release && config.npm.release === void 0 ? { release: config.release } : {}
509
+ } } : {}
510
+ };
511
+ }
512
+ function defaultConfig(directory) {
513
+ const filename = path.join(directory, `breakage.config.ts`);
514
+ if (!existsSync(filename) && existsSync(path.join(directory, `breakage.config.json`))) throw new Error(`JSON configuration was removed. Migrate breakage.config.json to breakage.config.ts.`);
515
+ return filename;
516
+ }
517
+ /** Comline ignores absent discovered files; explicit paths must exist. */
518
+ function configPath(filename, required = true) {
519
+ const absolute = path.resolve(filename);
520
+ if (path.extname(absolute) !== `.ts`) throw new Error(`Configuration must be a .ts module with a default export; JSON configuration was removed.`);
521
+ if (required && !existsSync(absolute)) throw new Error(`Configuration file does not exist: ${absolute}`);
522
+ return absolute;
523
+ }
524
+ /** Apply application semantics after Comline validates and merges configuration. */
525
+ function resolveConfig(config, filename) {
526
+ return {
527
+ ...defineConfig(config),
528
+ baseDirname: path.resolve(filename ? path.dirname(filename) : process.cwd(), config.baseDirname ?? `.`)
529
+ };
530
+ }
531
+ /** Use Comline's module loading and full-schema validation for programmatic and npm consumers. */
532
+ async function loadConfig(filename) {
533
+ const absolute = configPath(filename);
534
+ return resolveConfig((await cli({
535
+ cliName: `breakage`,
536
+ routeOptions: { "": configOptions },
537
+ discoverConfigPath: () => absolute
538
+ }).async([process.execPath, `breakage`])).inputs.opts, absolute);
539
+ }
540
+ // src/npm/release.ts
541
+ async function releaseVersion(config, context) {
542
+ return config.release ? (await resolveReleaseDelta(config.release, context))?.version : void 0;
543
+ }
544
+ /**
545
+ * Compare the union, including adjacent alternatives. semver.subset compares each
546
+ * conjunction to a single conjunction and cannot prove coverage by their union.
547
+ * Membership can change only at comparator boundaries or prerelease tuple gates.
548
+ */
549
+ function includesRange(current, previous) {
550
+ const ranges = [new semver.Range(previous), new semver.Range(current)];
551
+ const samples = /* @__PURE__ */ new Set([`0.0.0`, `0.0.0-0`]);
552
+ for (const range of ranges) for (const conjunction of range.set) for (const comparator of conjunction) {
553
+ const version = comparator.semver;
554
+ if (typeof version === `symbol`) continue;
555
+ const base = `${version.major}.${version.minor}.${version.patch}`;
556
+ samples.add(version.version);
557
+ samples.add(base);
558
+ samples.add(`${base}-0`);
559
+ if (version.prerelease.length) samples.add(`${version.version}.0`);
560
+ const next = semver.inc(base, `patch`);
561
+ if (next) {
562
+ samples.add(next);
563
+ samples.add(`${next}-0`);
564
+ }
565
+ }
566
+ return [...samples].every((version) => !semver.satisfies(version, previous) || semver.satisfies(version, current));
567
+ }
568
+ function supportChanges(previous, current) {
569
+ const before = previous.peerDependencies ?? {};
570
+ const after = current.peerDependencies ?? {};
571
+ validatePeerRanges(before);
572
+ validatePeerRanges(after);
573
+ return Object.entries(after).flatMap(([peer, range]) => {
574
+ const old = before[peer];
575
+ const wasOptional = previous.peerDependenciesMeta?.[peer]?.optional === true;
576
+ const isOptional = current.peerDependenciesMeta?.[peer]?.optional === true;
577
+ return old === void 0 && isOptional || old !== void 0 && includesRange(range, old) && !(wasOptional && !isOptional) ? [] : [{
578
+ peer,
579
+ reason: old === void 0 || wasOptional && !isOptional ? `required-peer` : `narrowed-range`,
580
+ previous: old ?? null,
581
+ current: range
582
+ }];
583
+ });
584
+ }
585
+ async function previousManifest(config, directory) {
586
+ if (`firstRelease` in config.baseline) return void 0;
587
+ const git = simpleGit(directory);
588
+ let commit;
589
+ if (config.baseline.baselineFile) commit = (await readBaseline(git, path.resolve(directory, config.baseline.baselineFile), config.baseline)).commit;
590
+ else {
591
+ const tags = await git.listRemote([`--tags`, `origin`]);
592
+ const ref = latestReleaseTag(tags, config.baseline.tagPattern);
593
+ if (!ref) throw new Error(`No npm release baseline found; configure a tag pattern or explicitly declare firstRelease.`);
594
+ const refs = new Map(tags.trim().split(`\n`).map((line) => {
595
+ const [sha, name] = line.split(`\t`);
596
+ return [name, sha];
597
+ }));
598
+ const resolved = refs.get(`${ref}^{}`) ?? refs.get(ref);
599
+ if (!resolved || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(resolved)) throw new Error(`Invalid release commit for ${ref}.`);
600
+ commit = resolved;
601
+ const common = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
602
+ await withDirectoryLock(path.join(common, `breakage-fetch.lock`), () => git.fetch([
603
+ `origin`,
604
+ commit,
605
+ `--no-tags`,
606
+ `--no-write-fetch-head`
607
+ ]));
608
+ }
609
+ const prefix = await git.revparse([`--show-prefix`]);
610
+ return await normalizeWorkspacePeers(parseManifest(JSON.parse(await git.show([`${commit}:${prefix}package.json`])), `release ${commit}`), async (peer) => {
611
+ const local = config.localPeers?.[peer];
612
+ if (!local) throw new Error(`Historical workspace peer ${peer} needs a localPeers directory to resolve release ${commit}.`);
613
+ const root = await git.revparse([`--show-toplevel`]);
614
+ const relative = path.relative(root, path.resolve(directory, local.directory));
615
+ if (relative === `..` || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`Historical workspace peer ${peer} escapes the release repository.`);
616
+ const filename = path.join(relative, `package.json`).split(path.sep).join(`/`);
617
+ const historical = parseManifest(JSON.parse(await git.show([`${commit}:${filename}`])), `release ${commit}:${filename}`);
618
+ if (historical.name !== peer) throw new Error(`Historical workspace peer identity mismatch for ${peer}.`);
619
+ return historical.version;
620
+ });
621
+ }
622
+ // src/npm/environment.ts
623
+ function within(parent, child) {
624
+ const relative = path.relative(parent, child);
625
+ return relative === `` || !relative.startsWith(`..${path.sep}`) && relative !== `..` && !path.isAbsolute(relative);
626
+ }
627
+ async function exists(filename) {
628
+ try {
629
+ await lstat(filename);
630
+ return true;
631
+ } catch (error) {
632
+ if (error.code === `ENOENT`) return false;
633
+ throw error;
634
+ }
635
+ }
636
+ async function discover(config, base) {
637
+ const found = /* @__PURE__ */ new Set();
638
+ for await (const name of fs.glob(config.representatives, {
639
+ cwd: base,
640
+ exclude: [
641
+ `**/node_modules/**`,
642
+ `**/test/**`,
643
+ `**/.git/**`
644
+ ]
645
+ })) {
646
+ const directory = path.resolve(base, name);
647
+ if (!(await lstat(directory)).isDirectory()) throw new Error(`Representative is not a directory: ${name}.`);
648
+ found.add(directory);
649
+ }
650
+ if (!found.size) throw new Error(`No representative directories matched the configured globs.`);
651
+ return [...found].sort();
652
+ }
653
+ /** A freshly created ignored stage is also the exclusive ownership lock. */
654
+ async function withStage(directory, action) {
655
+ const stage = path.join(directory, `test`);
656
+ const git = simpleGit(directory);
657
+ const root = await git.revparse([`--show-toplevel`]);
658
+ if (!within(await realpath(root), await realpath(directory))) throw new Error(`Representative escapes the repository: ${directory}.`);
659
+ if (!(await git.raw([
660
+ `check-ignore`,
661
+ `--`,
662
+ `${stage}/`
663
+ ])).trim() || (await git.raw([
664
+ `ls-files`,
665
+ `--`,
666
+ stage
667
+ ])).trim()) throw new Error(`Representative test/ must be gitignored and contain no tracked files: ${stage}.`);
668
+ try {
669
+ await mkdir(stage);
670
+ } catch (cause) {
671
+ throw new Error(`Cannot own ${stage}; another run or preexisting files may be present. Remove it only after checking ownership.`, { cause });
672
+ }
673
+ try {
674
+ return await action(stage);
675
+ } finally {
676
+ await rm(stage, {
677
+ recursive: true,
678
+ force: true
679
+ });
680
+ }
681
+ }
682
+ async function installed(directory, peer) {
683
+ const location = path.join(directory, `node_modules`, peer);
684
+ const physical = await realpath(location);
685
+ if (!physical.split(path.sep).includes(`node_modules`)) throw new Error(`Peer ${peer} resolves to workspace source: ${physical}.`);
686
+ const manifest = await readManifest(path.join(physical, `package.json`));
687
+ if (manifest.name !== peer) throw new Error(`Expected ${peer}, found ${manifest.name} in ${location}.`);
688
+ return {
689
+ version: manifest.version,
690
+ path: physical,
691
+ source: `installed`
692
+ };
693
+ }
694
+ function representativeDeclarations(manifest) {
695
+ for (const field of [
696
+ `dependencies`,
697
+ `devDependencies`,
698
+ `optionalDependencies`,
699
+ `peerDependencies`
700
+ ]) for (const [name, specifier] of Object.entries(manifest[field] ?? {})) {
701
+ const version = semver.parse(specifier);
702
+ const exact = version && `${version.version}${version.build.length ? `+${version.build.join(`.`)}` : ``}`;
703
+ if (!exact || specifier !== exact) throw new Error(`Representative ${manifest.name} must pin ${field}.${name} to an exact version, such as 1.0.0; received ${JSON.stringify(specifier)}. Declare support ranges in the package under test, and use breakage.candidates for coordinated local releases.`);
704
+ }
705
+ for (const [peer, development] of Object.entries(manifest.devDependencies ?? {})) for (const field of [`dependencies`, `optionalDependencies`]) {
706
+ const runtime = manifest[field]?.[peer];
707
+ if (runtime !== void 0 && runtime !== development) throw new Error(`Conflicting representative declarations for ${peer}: devDependencies has ${development}, but ${field} has ${runtime}. Use one consistent requirement.`);
708
+ }
709
+ return {
710
+ ...manifest.devDependencies,
711
+ ...manifest.dependencies,
712
+ ...manifest.optionalDependencies
713
+ };
714
+ }
715
+ /** Select exact pins without acquiring or preparing their stages. */
716
+ async function maySelectEnvironment(config, base, directory, packageManifest, selection) {
717
+ const manifest = await readManifest(path.join(directory, `package.json`));
718
+ const declarations = representativeDeclarations(manifest);
719
+ const { peer, range } = selection;
720
+ if (peer === packageManifest.name) return matches(packageManifest.version, range);
721
+ if (manifest.breakage?.candidates?.includes(peer)) {
722
+ const local = config.localPeers?.[peer];
723
+ if (!local) throw new Error(`Candidate ${peer} has no localPeers configuration.`);
724
+ const packageDirectory = path.resolve(base, local.directory);
725
+ const current = await readManifest(path.join(packageDirectory, `package.json`));
726
+ if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
727
+ const release = await releaseVersion(config, {
728
+ packageName: peer,
729
+ packageDirectory,
730
+ currentVersion: current.version
731
+ });
732
+ if (release === void 0) throw new Error(`Candidate ${peer} has no release version.`);
733
+ return matches(release, range);
734
+ }
735
+ const declared = declarations[peer];
736
+ return declared !== void 0 && matches(declared, range);
737
+ }
738
+ async function prepareEnvironment(config, base, directory, stage, packageManifest) {
739
+ const peerNames = Object.keys(packageManifest.peerDependencies ?? {});
740
+ const filename = path.join(directory, `package.json`);
741
+ const lockfile = path.join(directory, config.lockfile);
742
+ for (const input of [filename, lockfile]) if (!(await lstat(input)).isFile() || !within(directory, await realpath(input))) throw new Error(`Installation inputs must be regular files inside the representative: ${input}.`);
743
+ const inputs = await Promise.all([readFile(filename), readFile(lockfile)]);
744
+ const manifest = await readManifest(filename);
745
+ const declarations = representativeDeclarations(manifest);
746
+ const candidates = {};
747
+ for (const peer of manifest.breakage?.candidates ?? []) {
748
+ if (!peerNames.includes(peer)) throw new Error(`Candidate ${peer} is not a declared peer of the package under test.`);
749
+ const local = config.localPeers?.[peer];
750
+ if (!local) throw new Error(`Candidate ${peer} has no localPeers configuration.`);
751
+ const localDirectory = path.resolve(base, local.directory);
752
+ const localManifest = await normalizeWorkspacePeers(await readManifest(path.join(localDirectory, `package.json`)));
753
+ if (localManifest.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
754
+ validatePeerRanges(localManifest.peerDependencies ?? {});
755
+ const evidence = await releaseVersion(config, {
756
+ packageName: peer,
757
+ packageDirectory: localDirectory,
758
+ currentVersion: localManifest.version
759
+ });
760
+ if (evidence === void 0) throw new Error(`Candidate ${peer} has no release version.`);
761
+ candidates[peer] = {
762
+ directory: localDirectory,
763
+ artifact: path.resolve(base, local.artifact),
764
+ currentVersion: localManifest.version,
765
+ version: evidence,
766
+ manifest: localManifest
767
+ };
768
+ }
769
+ const relevant = [.../* @__PURE__ */ new Set([...peerNames, ...Object.values(candidates).flatMap((candidate) => Object.keys(candidate.manifest.peerDependencies ?? {}))])].filter((peer) => peer === packageManifest.name || declarations[peer] !== void 0 || candidates[peer] !== void 0);
770
+ const before = {};
771
+ for (const peer of relevant) if (peer !== packageManifest.name && !candidates[peer] && await exists(path.join(directory, `node_modules`, peer))) before[peer] = (await installed(directory, peer)).version;
772
+ const preparation = await prepare(config.prepareCommand, directory).then(() => ({ ok: true }), (error) => ({
773
+ ok: false,
774
+ error
775
+ }));
776
+ const after = await Promise.all([filename, lockfile].map((input) => readFile(input).catch(() => Buffer.alloc(0))));
777
+ if (inputs.some((input, index) => !input.equals(after[index]))) {
778
+ await Promise.all([filename, lockfile].map(async (input, index) => {
779
+ await rm(input, { force: true });
780
+ await writeFile(input, inputs[index], { flag: `wx` });
781
+ }));
782
+ throw new Error(`Frozen preparation changed the manifest or lockfile in ${directory}; restored the original inputs.`, { cause: preparation.ok ? void 0 : preparation.error });
783
+ }
784
+ if (!preparation.ok) throw preparation.error;
785
+ const peers = {};
786
+ for (const peer of relevant) {
787
+ const candidate = candidates[peer];
788
+ const staged = candidate !== void 0 || peer === packageManifest.name;
789
+ if (!staged && Object.hasOwn(manifest.optionalDependencies ?? {}, peer) && !await exists(path.join(directory, `node_modules`, peer))) continue;
790
+ const evidence = staged ? {
791
+ version: candidate?.version ?? packageManifest.version,
792
+ path: path.join(stage, `node_modules`, peer),
793
+ source: `candidate`
794
+ } : await installed(directory, peer);
795
+ const specifier = declarations[peer];
796
+ if (!(staged && specifier === void 0) && evidence.version !== specifier) throw new Error(`Representative ${peer}@${evidence.version} does not match its manifest pin ${specifier ?? `(missing)`}.`);
797
+ if (before[peer] && before[peer] !== evidence.version) throw new Error(`Stale installed ${peer}: frozen preparation changed ${before[peer]} to ${evidence.version}.`);
798
+ for (const [localPeer, local] of Object.entries(config.localPeers ?? {})) {
799
+ if (staged) continue;
800
+ const localDirectory = path.resolve(base, local.directory);
801
+ if (!peerNames.includes(localPeer) && !relevant.includes(localPeer) && !await exists(localDirectory)) continue;
802
+ if (within(await realpath(localDirectory), evidence.path)) throw new Error(`Released peer ${peer} resolves into a local workspace package.`);
803
+ }
804
+ peers[peer] = evidence;
805
+ }
806
+ return {
807
+ directory,
808
+ stage,
809
+ manifest,
810
+ peers,
811
+ candidates
812
+ };
813
+ }
814
+ /** Validate the complete coordinated formulation before an audit or execution. */
815
+ function checkCandidatePeers(environment, focused) {
816
+ const conflicts = [];
817
+ for (const [name, candidate] of Object.entries(environment.candidates)) for (const [peer, range] of Object.entries(candidate.manifest.peerDependencies ?? {})) {
818
+ const evidence = environment.peers[peer];
819
+ if (!evidence) {
820
+ if (candidate.manifest.peerDependenciesMeta?.[peer]?.optional) continue;
821
+ throw new Error(`Complete representative is missing required peer ${peer}: ${range} of coordinated candidate ${name}@${candidate.version}.`);
822
+ }
823
+ if (!semver.satisfies(evidence.version, range)) conflicts.push(`Coordinated candidate ${name}@${candidate.version} requires ${peer}: ${range}, but the representative provides ${peer}@${evidence.version}.`);
824
+ }
825
+ if (conflicts.length && !focused) throw new Error(conflicts.join(`\n`));
826
+ return conflicts.map((conflict) => `${conflict} Running focused exploration outside declared support.`);
827
+ }
828
+ async function copySuite(base, stage, suite) {
829
+ for (const pattern of suite.files) {
830
+ if (path.isAbsolute(pattern) || pattern.split(/[\\/]/).includes(`..`)) throw new Error(`Suite pattern must stay inside the package: ${pattern}.`);
831
+ let count = 0;
832
+ for await (const filename of fs.glob(pattern, {
833
+ cwd: base,
834
+ exclude: [
835
+ `**/node_modules/**`,
836
+ `**/.git/**`,
837
+ path.relative(base, stage),
838
+ `${path.relative(base, stage)}/**`
839
+ ]
840
+ })) {
841
+ const source = path.resolve(base, filename);
842
+ const stat = await lstat(source);
843
+ if (stat.isDirectory()) continue;
844
+ if (!stat.isFile() || !within(await realpath(base), await realpath(source))) throw new Error(`Suite inputs must be regular package files: ${filename}.`);
845
+ let segment = base;
846
+ for (const part of filename.split(path.sep)) {
847
+ segment = path.join(segment, part);
848
+ if ((await lstat(segment)).isSymbolicLink()) throw new Error(`Suite inputs cannot follow symlinks: ${filename}.`);
849
+ }
850
+ if ([
851
+ `package.json`,
852
+ `package-lock.json`,
853
+ `npm-shrinkwrap.json`,
854
+ `.npmrc`
855
+ ].includes(filename) || filename.split(path.sep).includes(`node_modules`)) throw new Error(`Suite input collides with the consumer installation: ${filename}.`);
856
+ const destination = path.resolve(stage, filename);
857
+ if (!within(stage, destination)) throw new Error(`Suite path escapes its stage: ${filename}.`);
858
+ await mkdir(path.dirname(destination), { recursive: true });
859
+ await copyFile(source, destination);
860
+ count++;
861
+ }
862
+ if (!count) throw new Error(`Suite pattern matched no files: ${pattern}.`);
863
+ }
864
+ }
865
+ async function findPackage(from, name, installation) {
866
+ const origin = await realpath(from);
867
+ const parts = origin.split(path.sep);
868
+ const modules = parts.lastIndexOf(`node_modules`);
869
+ const boundary = within(installation, origin) ? installation : modules < 0 ? origin : parts.slice(0, modules + 1).join(path.sep);
870
+ const lookup = createRequire(path.join(origin, `__breakage_probe__.ts`)).resolve.paths(name) ?? [];
871
+ for (const directory of lookup) {
872
+ const location = path.join(directory, name);
873
+ if (await exists(location)) {
874
+ if (!within(installation, directory) && !within(boundary, directory)) throw new Error(`Dependency ${name} resolves outside the representative installation: ${location}.`);
875
+ return await realpath(location);
876
+ }
877
+ }
878
+ }
879
+ async function resolvePackage(from, name, installation) {
880
+ const location = await findPackage(from, name, installation);
881
+ if (location !== void 0) return location;
882
+ throw new Error(`Cannot resolve ${name} from ${from}.`);
883
+ }
884
+ async function stageCandidate(config, base, manifest, environment, suites) {
885
+ const { stage, candidates } = environment;
886
+ const artifacts = {
887
+ [manifest.name]: path.resolve(base, config.artifact),
888
+ ...Object.fromEntries(Object.entries(candidates).map(([name, entry]) => [name, entry.artifact]))
889
+ };
890
+ if (candidates[manifest.name]) throw new Error(`The package under test cannot also be its own candidate peer.`);
891
+ for (const artifact of Object.values(artifacts)) if (!(await lstat(artifact)).isFile()) throw new Error(`Candidate artifact must be a packed file: ${artifact}.`);
892
+ await writeFile(path.join(stage, `package.json`), JSON.stringify({
893
+ name: `breakage-consumer`,
894
+ version: `0.0.0`,
895
+ private: true,
896
+ type: environment.manifest.type ?? `module`,
897
+ dependencies: Object.fromEntries(Object.entries(artifacts).map(([name, artifact]) => [name, `file:${artifact}`]))
898
+ }));
899
+ await prepare(config.installCommand ?? `npm install --prefix . --workspaces=false --ignore-scripts --legacy-peer-deps --package-lock=false --no-audit --no-fund`, stage);
900
+ for (const [name] of Object.entries(artifacts)) {
901
+ const directory = path.join(stage, `node_modules`, name);
902
+ if (!within(stage, await realpath(directory))) throw new Error(`Candidate ${name} must be installed inside the test stage.`);
903
+ const filename = path.join(directory, `package.json`);
904
+ const packed = await readManifest(filename);
905
+ const candidate = candidates[name];
906
+ if (packed.name !== name || (candidate ? ![candidate.currentVersion, candidate.version].includes(packed.version) : packed.version !== manifest.version)) throw new Error(`Candidate archive identity/version mismatch for ${name}@${packed.version}.`);
907
+ const expectedManifest = candidate?.manifest ?? manifest;
908
+ if (Object.keys({
909
+ ...packed.peerDependencies,
910
+ ...expectedManifest.peerDependencies
911
+ }).some((peer) => packed.peerDependencies?.[peer] !== expectedManifest.peerDependencies?.[peer] || Boolean(packed.peerDependenciesMeta?.[peer]?.optional) !== Boolean(expectedManifest.peerDependenciesMeta?.[peer]?.optional))) throw new Error(`Candidate archive has stale peer declarations; rebuild and pack ${name}.`);
912
+ if (candidate) {
913
+ const evidence = environment.peers[name];
914
+ if (evidence) evidence.archiveVersion = packed.version;
915
+ if (packed.version !== candidate.version) {
916
+ const raw = JSON.parse(await readFile(filename, `utf8`));
917
+ const temporary = path.join(path.dirname(filename), `.breakage-${randomUUID()}.json`);
918
+ await writeFile(temporary, JSON.stringify({
919
+ ...raw,
920
+ version: candidate.version
921
+ }), { flag: `wx` });
922
+ try {
923
+ await rename(temporary, filename);
924
+ } finally {
925
+ await rm(temporary, { force: true });
926
+ }
927
+ }
928
+ }
929
+ }
930
+ for (const suite of suites) await copySuite(base, stage, suite);
931
+ await verifyResolution(manifest, environment);
932
+ }
933
+ async function verifyResolution(manifest, environment) {
934
+ const { stage, peers } = environment;
935
+ const installation = await realpath(environment.directory);
936
+ const packages = [manifest.name, ...Object.keys(environment.candidates)];
937
+ for (const [peer, evidence] of Object.entries(peers)) {
938
+ const expected = await realpath(evidence.path);
939
+ if (await resolvePackage(stage, peer, installation) !== expected) throw new Error(`Tests resolve a shadow copy of ${peer}.`);
940
+ for (const name of packages) {
941
+ if (name === peer) continue;
942
+ if (await resolvePackage(path.join(stage, `node_modules`, name), peer, installation) !== expected) throw new Error(`Candidate ${name} resolves a different instance of ${peer} than its tests.`);
943
+ }
944
+ const actual = await readManifest(path.join(expected, `package.json`));
945
+ if (actual.name !== peer || actual.version !== evidence.version) throw new Error(`Peer identity changed during execution: ${peer}.`);
946
+ }
947
+ const visited = /* @__PURE__ */ new Set();
948
+ const pending = packages.map((name) => path.join(stage, `node_modules`, name));
949
+ while (pending.length) {
950
+ const directory = await realpath(pending.pop());
951
+ if (visited.has(directory)) continue;
952
+ if (!directory.split(path.sep).includes(`node_modules`)) throw new Error(`Runtime dependency resolves to workspace source: ${directory}.`);
953
+ visited.add(directory);
954
+ const installedManifest = await readManifest(path.join(directory, `package.json`));
955
+ for (const peer of Object.keys(installedManifest.peerDependencies ?? {})) {
956
+ const evidence = peers[peer];
957
+ if (evidence && await resolvePackage(directory, peer, installation) !== await realpath(evidence.path)) throw new Error(`Runtime dependency ${installedManifest.name} resolves a different instance of ${peer}.`);
958
+ }
959
+ const optional = installedManifest.optionalDependencies ?? {};
960
+ for (const dependency of Object.keys({
961
+ ...installedManifest.dependencies,
962
+ ...optional
963
+ })) if (Object.hasOwn(optional, dependency)) {
964
+ const location = await findPackage(directory, dependency, installation);
965
+ if (location !== void 0) pending.push(location);
966
+ } else pending.push(await resolvePackage(directory, dependency, installation));
967
+ }
968
+ }
969
+ // src/npm/check.ts
970
+ async function checkSupport(config, base, manifest) {
971
+ try {
972
+ const previous = await previousManifest(config, base);
973
+ if (!previous) return {
974
+ status: `first-release`,
975
+ changes: []
976
+ };
977
+ if (previous.name !== manifest.name) throw new Error(`Release baseline belongs to ${previous.name}, not ${manifest.name}.`);
978
+ const changes = supportChanges(previous, manifest);
979
+ if (!changes.length) return {
980
+ status: `unchanged`,
981
+ previousVersion: previous.version,
982
+ changes
983
+ };
984
+ const evidence = config.release ? await resolveReleaseDelta(config.release, {
985
+ packageName: manifest.name,
986
+ packageDirectory: base,
987
+ currentVersion: manifest.version,
988
+ baseline: { version: previous.version }
989
+ }) : void 0;
990
+ if (evidence === void 0) return {
991
+ status: `end-of-support`,
992
+ previousVersion: previous.version,
993
+ changes
994
+ };
995
+ const release = versionDelta(evidence.previousVersion, evidence.version, config.release?.policy);
996
+ return {
997
+ status: release.breaking ? `breaking-release` : `end-of-support`,
998
+ previousVersion: previous.version,
999
+ changes,
1000
+ release
1001
+ };
1002
+ } catch (error) {
1003
+ return {
1004
+ status: `setup-error`,
1005
+ changes: [],
1006
+ error: errorMessage(error)
1007
+ };
1008
+ }
1009
+ }
1010
+ async function checkLocalReleases(config, base, manifest, representatives) {
1011
+ return (await Promise.all(Object.entries(config.localPeers ?? {}).map(async ([peer, local]) => {
1012
+ const range = manifest.peerDependencies?.[peer];
1013
+ if (range === void 0) return [];
1014
+ try {
1015
+ const directory = path.resolve(base, local.directory);
1016
+ const current = await readManifest(path.join(directory, `package.json`));
1017
+ if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
1018
+ const release = await releaseVersion(config, {
1019
+ packageName: peer,
1020
+ packageDirectory: directory,
1021
+ currentVersion: current.version
1022
+ });
1023
+ if (release === void 0) return [];
1024
+ if (!matches(release, range)) return [`Declared support for ${peer}: ${range} does not include its planned local release ${release}. Update the peer range to include the intended version.`];
1025
+ if (!representatives.some((result) => result.peers[peer]?.source === `candidate` && result.peers[peer]?.version === release && result.suites.some((suite) => suite.peer === peer))) return [`No coordinated representative covers planned local peer ${peer}@${release}. Enroll ${peer} with breakage.candidates and configure its relationship suite.`];
1026
+ return [];
1027
+ } catch (error) {
1028
+ return [`Cannot inspect planned local release for ${peer}: ${errorMessage(error)}`];
1029
+ }
1030
+ }))).flat();
1031
+ }
1032
+ async function runRepresentative(config, base, manifest, directory, coverageOnly, selection) {
1033
+ const result = {
1034
+ path: path.relative(base, directory).split(path.sep).join(`/`),
1035
+ status: `skipped`,
1036
+ peers: {},
1037
+ suites: [],
1038
+ warnings: []
1039
+ };
1040
+ if (selection && `representative` in selection && path.resolve(base, selection.representative) !== directory) return result;
1041
+ try {
1042
+ if (selection && `peer` in selection && !await maySelectEnvironment(config, base, directory, manifest, selection)) return result;
1043
+ return await withStage(directory, async (stage) => {
1044
+ const environment = await prepareEnvironment(config, base, directory, stage, manifest);
1045
+ result.peers = environment.peers;
1046
+ for (const peer of Object.keys(manifest.peerDependencies ?? {})) if (!result.peers[peer] && !manifest.peerDependenciesMeta?.[peer]?.optional) throw new Error(`Complete representative is missing required peer ${peer}.`);
1047
+ for (const [peer, evidence] of Object.entries(result.peers)) {
1048
+ const range = manifest.peerDependencies?.[peer];
1049
+ if (range !== void 0 && !matches(evidence.version, range)) result.warnings.push(`${peer}@${evidence.version} is outside declared support.`);
1050
+ }
1051
+ if (selection && `peer` in selection && (!result.peers[selection.peer] || !matches(result.peers[selection.peer].version, selection.range))) return result;
1052
+ const peers = Object.keys(config.suites).filter((peer) => {
1053
+ const evidence = result.peers[peer];
1054
+ if (!evidence) return false;
1055
+ if (selection && `peer` in selection) return peer === selection.peer;
1056
+ if (selection && `representative` in selection) return true;
1057
+ return matches(evidence.version, manifest.peerDependencies[peer]);
1058
+ });
1059
+ result.suites = peers.map((peer) => ({
1060
+ peer,
1061
+ status: `not-run`
1062
+ }));
1063
+ if (!peers.length) return result;
1064
+ result.warnings.push(...checkCandidatePeers(environment, !!selection));
1065
+ if (coverageOnly) {
1066
+ result.status = `audited`;
1067
+ return result;
1068
+ }
1069
+ await stageCandidate(config, base, manifest, environment, peers.map((peer) => config.suites[peer]));
1070
+ for (const suite of result.suites) try {
1071
+ await verifyResolution(manifest, environment);
1072
+ const execution = await command(config.suites[suite.peer].command, stage);
1073
+ await verifyResolution(manifest, environment);
1074
+ suite.status = execution.code === 0 ? `passed` : execution.code === 1 ? `compatibility-failure` : `setup-error`;
1075
+ if (execution.output) suite.output = execution.output;
1076
+ } catch (error) {
1077
+ suite.status = `setup-error`;
1078
+ suite.output = errorMessage(error);
1079
+ }
1080
+ result.status = result.suites.some((suite) => suite.status === `setup-error`) ? `setup-error` : result.suites.some((suite) => suite.status === `compatibility-failure`) ? `compatibility-failure` : `passed`;
1081
+ return result;
1082
+ });
1083
+ } catch (error) {
1084
+ result.status = `setup-error`;
1085
+ result.error = errorMessage(error);
1086
+ return result;
1087
+ }
1088
+ }
1089
+ async function run(config, baseDirectory, coverageOnly, selection) {
1090
+ const report = {
1091
+ package: {
1092
+ name: `(unresolved)`,
1093
+ version: `(unresolved)`
1094
+ },
1095
+ mode: selection ? `test` : coverageOnly ? `coverage-only` : `check`,
1096
+ exitCode: 0,
1097
+ representatives: [],
1098
+ coverage: [],
1099
+ support: {
1100
+ status: `not-checked`,
1101
+ changes: []
1102
+ },
1103
+ errors: []
1104
+ };
1105
+ try {
1106
+ validateConfig(config);
1107
+ const base = path.resolve(baseDirectory, config.packageDirectory ?? `.`);
1108
+ const manifest = await normalizeWorkspacePeers(await readManifest(path.join(base, `package.json`)));
1109
+ report.package = {
1110
+ name: manifest.name,
1111
+ version: manifest.version
1112
+ };
1113
+ validatePeerRanges(manifest.peerDependencies ?? {});
1114
+ for (const peer of Object.keys(config.suites)) if (!manifest.peerDependencies?.[peer]) throw new Error(`Suite ${peer} has no peerDependencies declaration.`);
1115
+ for (const peer of Object.keys(config.localPeers ?? {})) if (manifest.peerDependencies?.[peer] && !config.suites[peer]) throw new Error(`Declared local peer ${peer} needs a relationship suite.`);
1116
+ if (selection && `peer` in selection) {
1117
+ if (!config.suites[selection.peer]) throw new Error(`No relationship suite configured for ${selection.peer}.`);
1118
+ eras(selection.range);
1119
+ }
1120
+ const releases = /* @__PURE__ */ new Map();
1121
+ const resolveDelta = (context) => {
1122
+ const key = JSON.stringify([
1123
+ context.packageDirectory,
1124
+ context.currentVersion,
1125
+ context.baseline
1126
+ ]);
1127
+ let pending = releases.get(key);
1128
+ if (!pending) {
1129
+ pending = resolveReleaseDelta(config.release, context);
1130
+ releases.set(key, pending);
1131
+ }
1132
+ return pending;
1133
+ };
1134
+ const effective = {
1135
+ ...config,
1136
+ ...config.release ? { release: {
1137
+ ...config.release,
1138
+ resolveDelta
1139
+ } } : {}
1140
+ };
1141
+ const directories = await discover(config, base);
1142
+ const [support, representatives] = await Promise.all([selection ? Promise.resolve(report.support) : checkSupport(effective, base, manifest), Promise.all(directories.map((directory) => runRepresentative(effective, base, manifest, directory, coverageOnly, selection)))]);
1143
+ report.support = support;
1144
+ report.representatives = representatives;
1145
+ if (!selection) {
1146
+ report.errors.push(...await checkLocalReleases(effective, base, manifest, representatives));
1147
+ for (const peer of Object.keys(config.suites)) for (const era of eras(manifest.peerDependencies[peer])) {
1148
+ const matching = representatives.filter((representative) => representative.peers[peer] && matches(representative.peers[peer].version, era)).map((representative) => representative.path);
1149
+ report.coverage.push({
1150
+ peer,
1151
+ era,
1152
+ representatives: matching
1153
+ });
1154
+ if (!matching.length) report.errors.push(`No representative covers ${peer}: ${era}.`);
1155
+ }
1156
+ } else if (!representatives.some((representative) => representative.suites.length > 0)) report.errors.push(`No representative matched the requested test selection.`);
1157
+ if (support.status === `setup-error` || report.errors.length || representatives.some((result) => result.status === `setup-error`)) report.exitCode = 2;
1158
+ else if (support.status === `end-of-support` || representatives.some((result) => result.status === `compatibility-failure`)) report.exitCode = 1;
1159
+ } catch (error) {
1160
+ report.errors.push(errorMessage(error));
1161
+ report.exitCode = 2;
1162
+ }
1163
+ return report;
1164
+ }
1165
+ async function npmCheck(config, options = {}) {
1166
+ return await run(config, options.baseDirectory ?? process.cwd(), options.coverageOnly ?? false);
1167
+ }
1168
+ async function npmTest(config, selection, options = {}) {
1169
+ return await run(config, options.baseDirectory ?? process.cwd(), false, selection);
1170
+ }
1171
+ function formatNpmReport(report) {
1172
+ const lines = [`${report.package.name}@${report.package.version} — npm ${report.mode}`];
1173
+ for (const result of report.representatives) {
1174
+ lines.push(`${result.path}: ${result.status}`);
1175
+ for (const [peer, evidence] of Object.entries(result.peers)) lines.push(` ${peer}@${evidence.version}${evidence.source === `candidate` ? ` (conditional on publication)` : ` (installed)`}`);
1176
+ for (const suite of result.suites) {
1177
+ lines.push(` ${suite.peer}: ${suite.status}`);
1178
+ if (suite.status !== `passed` && suite.output) lines.push(suite.output);
1179
+ }
1180
+ for (const warning of result.warnings) lines.push(` Warning: ${warning}`);
1181
+ if (result.error) lines.push(` ${result.error}`);
1182
+ }
1183
+ lines.push(`Support: ${report.support.status}`);
1184
+ for (const change of report.support.changes) lines.push(` ${change.peer}: ${change.previous ?? `(unrestricted)`} → ${change.current ?? `(unrestricted)`} (${change.reason})`);
1185
+ if (report.support.release) lines.push(` Intended release: ${report.support.release.version} (${report.support.release.delta})`);
1186
+ if (report.support.error) lines.push(report.support.error);
1187
+ lines.push(...report.errors);
1188
+ if (report.mode === `coverage-only`) lines.push(`Coverage audit only; compatibility assertions were not executed.`);
1189
+ return `${lines.join(`\n`)}\n`;
1190
+ }
1191
+ // src/breakage.ts
1192
+ async function breakage(options) {
1193
+ rejectCertificationOptions(options);
1194
+ const { tagPattern, baselineFile, testPattern, testCommand, test, release, baseDirname = process.cwd(), verbose = false } = options;
1195
+ if (!testPattern || !test && !testCommand) throw new Error(`required arguments were not provided: testPattern and test or testCommand.`);
1196
+ if (test !== void 0 && testCommand !== void 0) throw new Error(`Choose test or testCommand, not both.`);
1197
+ if (typeof (test ?? testCommand) !== `string` || !(test ?? testCommand).trim()) throw new Error(`test must be a nonempty shell command string.`);
1198
+ if (release !== void 0) validateRelease(release);
1199
+ let mark;
1200
+ let logMarks;
1201
+ if (verbose) {
1202
+ const chronicle = logger.makeChronicle();
1203
+ mark = chronicle.mark;
1204
+ logMarks = chronicle.logMarks;
1205
+ }
1206
+ mark?.(`breakage`);
1207
+ const git = simpleGit(baseDirname);
1208
+ mark?.(`spawn git`);
1209
+ const isGitRepo = await git.checkIsRepo();
1210
+ mark?.(`is git repository`);
1211
+ if (!isGitRepo) return {
1212
+ summary: `The git repository must be clean to run this command.`,
1213
+ gitWasClean: false
1214
+ };
1215
+ if (!await withTestFileState(git, baseDirname, (state) => state.isClean())) return {
1216
+ summary: `The git repository must be clean to run this command.`,
1217
+ gitWasClean: false
1218
+ };
1219
+ let latestReleaseTag$1;
1220
+ let releaseCommit;
1221
+ if (baselineFile !== void 0) {
1222
+ const baseline = await readBaseline(git, path.resolve(baseDirname, baselineFile), { tagPattern });
1223
+ latestReleaseTag$1 = baseline.ref;
1224
+ releaseCommit = baseline.commit;
1225
+ mark?.(`read pinned release baseline`);
1226
+ } else {
1227
+ const tagsRemote = await git.listRemote([`--tags`, `origin`]);
1228
+ mark?.(`list remote tags`);
1229
+ latestReleaseTag$1 = latestReleaseTag(tagsRemote, tagPattern);
1230
+ mark?.(`found latest release tag`);
1231
+ if (!latestReleaseTag$1) return {
1232
+ summary: `No tags found matching the pattern "${tagPattern}".`,
1233
+ gitWasClean: true,
1234
+ lastReleaseFound: false
1235
+ };
1236
+ const commonGitDirectory = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
1237
+ await withDirectoryLock(path.join(commonGitDirectory, `breakage-fetch.lock`), () => git.fetch([
1238
+ `origin`,
1239
+ `${latestReleaseTag$1}:${latestReleaseTag$1}`,
1240
+ `--no-tags`,
1241
+ `--no-write-fetch-head`
1242
+ ]));
1243
+ releaseCommit = await git.revparse([`${latestReleaseTag$1}^{commit}`]);
1244
+ mark?.(`fetched latest release tag`);
1245
+ }
1246
+ let productionFiles;
1247
+ try {
1248
+ productionFiles = (await git.raw([
1249
+ `ls-tree`,
1250
+ `-r`,
1251
+ `--name-only`,
1252
+ `-z`,
1253
+ releaseCommit
1254
+ ])).split(`\0`).filter(Boolean);
1255
+ } catch (thrown) {
1256
+ mark?.(`failed to list production files`);
1257
+ const message = thrown instanceof Error ? thrown.message : typeof thrown === `string` ? thrown : void 0;
1258
+ return {
1259
+ summary: `Failed to list production files${message ? `: ${message}` : `.`}`,
1260
+ gitWasClean: true,
1261
+ lastReleaseFound: true,
1262
+ lastReleaseTag: latestReleaseTag$1,
1263
+ testsWereFound: false
1264
+ };
1265
+ }
1266
+ mark?.(`listed all files checked into git`);
1267
+ const productionTestFiles = productionFiles.filter((file) => minimatch(file, testPattern));
1268
+ mark?.(`filtered to public test files`);
1269
+ let releaseEvidence;
1270
+ if (release) {
1271
+ const current = await readManifest(path.join(baseDirname, `package.json`));
1272
+ const prefix = await git.revparse([`--show-prefix`]);
1273
+ const previous = parseManifest(JSON.parse(await git.show([`${releaseCommit}:${prefix}package.json`])), `baseline`);
1274
+ if (previous.name !== current.name) throw new Error(`Release baseline belongs to another package.`);
1275
+ const transition = await resolveReleaseDelta(release, {
1276
+ packageName: current.name,
1277
+ packageDirectory: path.resolve(baseDirname),
1278
+ currentVersion: current.version,
1279
+ baseline: {
1280
+ version: previous.version,
1281
+ ref: latestReleaseTag$1,
1282
+ commit: releaseCommit
1283
+ }
1284
+ });
1285
+ if (transition !== void 0) releaseEvidence = versionDelta(transition.previousVersion, transition.version, release.policy);
1286
+ }
1287
+ const prepared = await withTestFileState(git, baseDirname, async (state) => {
1288
+ if (!await state.isClean()) return {
1289
+ summary: `The git repository must be clean to run this command.`,
1290
+ gitWasClean: false
1291
+ };
1292
+ if (productionTestFiles.length === 0) return {
1293
+ summary: `No tests were found matching the pattern "${testPattern}".`,
1294
+ gitWasClean: true,
1295
+ lastReleaseFound: true,
1296
+ lastReleaseTag: latestReleaseTag$1,
1297
+ testsWereFound: false
1298
+ };
1299
+ const restore = await state.replaceTests(releaseCommit, productionTestFiles);
1300
+ mark?.(`restored public tests from latest release tag`);
1301
+ return {
1302
+ latestReleaseTag: latestReleaseTag$1,
1303
+ productionTestFiles,
1304
+ restore
1305
+ };
1306
+ });
1307
+ if (prepared.summary !== void 0) return prepared;
1308
+ const { restore } = prepared;
1309
+ try {
1310
+ const result = await command(test ?? testCommand, baseDirname);
1311
+ if (result.code > 1) return {
1312
+ gitWasClean: true,
1313
+ setupError: result.output,
1314
+ summary: `Public test setup failed (${result.code}).`
1315
+ };
1316
+ const common = {
1317
+ gitWasClean: true,
1318
+ lastReleaseFound: true,
1319
+ lastReleaseTag: latestReleaseTag$1,
1320
+ testsWereFound: true,
1321
+ testsFound: productionTestFiles,
1322
+ ...releaseEvidence ? { release: releaseEvidence } : {}
1323
+ };
1324
+ if (result.code === 0) return {
1325
+ ...common,
1326
+ breakingChangesFound: false,
1327
+ summary: `No breaking changes were detected.`
1328
+ };
1329
+ return {
1330
+ ...common,
1331
+ breakingChangesFound: true,
1332
+ testResult: result.output,
1333
+ summary: releaseEvidence?.breaking ? `Breaking changes are covered by release ${releaseEvidence.previousVersion} → ${releaseEvidence.version}.` : `Breaking changes require a breaking release.`
1334
+ };
1335
+ } catch (error) {
1336
+ return {
1337
+ gitWasClean: true,
1338
+ setupError: errorMessage(error),
1339
+ summary: `Public test setup failed: ${errorMessage(error)}`
1340
+ };
1341
+ } finally {
1342
+ await restore();
1343
+ mark?.(`restored original public tests`);
1344
+ logMarks?.();
1345
+ }
1346
+ }
1347
+ export { configFields as a, configSchema as c, loadConfig as d, resolveConfig as f, errorMessage as h, npmTest as i, defaultConfig as l, breakagePrelude as m, formatNpmReport as n, configOptions as o, validateConfig as p, npmCheck as r, configPath as s, breakage as t, defineConfig as u };
1348
+
1349
+ //# sourceMappingURL=breakage-DIY49fzP.js.map