breakage 0.8.5 → 0.9.1

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 (45) hide show
  1. package/README.md +163 -47
  2. package/dist/{breakage-B7dJsgcu.js → breakage-DJiIiLzL.js} +958 -221
  3. package/dist/breakage-DJiIiLzL.js.map +1 -0
  4. package/dist/breakage.d.ts +239 -71
  5. package/dist/breakage.d.ts.map +1 -1
  6. package/dist/breakage.js +3 -1
  7. package/dist/breakage.x.js +216 -79
  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 +198 -44
  18. package/package.json +45 -6
  19. package/src/breakage.ts +142 -73
  20. package/src/breakage.x.ts +174 -127
  21. package/src/changesets.ts +51 -0
  22. package/src/config.ts +131 -0
  23. package/src/npm/check.ts +70 -50
  24. package/src/npm/cli.ts +30 -22
  25. package/src/npm/config.ts +38 -6
  26. package/src/npm/environment.ts +10 -10
  27. package/src/npm/matrix-cli.ts +170 -0
  28. package/src/npm/matrix-model.ts +395 -0
  29. package/src/npm/matrix.ts +657 -0
  30. package/src/npm/release.ts +9 -25
  31. package/src/npm/types.ts +5 -45
  32. package/src/release.ts +153 -0
  33. package/src/{npm/command.ts → task.ts} +2 -0
  34. package/dist/breakage-B7dJsgcu.js.map +0 -1
  35. package/dist/breakage.$configPath.schema.json +0 -33
  36. package/dist/breakage.help.schema.json +0 -6
  37. package/dist/breakage.main.schema.json +0 -33
  38. package/dist/breakage.npm.check.schema.json +0 -16
  39. package/dist/breakage.npm.help.schema.json +0 -6
  40. package/dist/breakage.npm.schema.json +0 -6
  41. package/dist/breakage.npm.test.$peer.$range.schema.json +0 -16
  42. package/dist/breakage.npm.test.schema.json +0 -16
  43. package/dist/breakage.prelude.$configPath.schema.json +0 -31
  44. package/dist/breakage.prelude.schema.json +0 -31
  45. package/dist/breakage.schema.schema.json +0 -10
@@ -1,15 +1,125 @@
1
+ import { a as validateRelease, n as releaseSchema, o as versionDelta, r as resolveReleaseDelta, t as rejectCertificationOptions } from "./release-BsdIDT4g.js";
1
2
  import { createRequire } from "node:module";
2
- import { exec, spawn } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { minimatch } from "minimatch";
5
5
  import { simpleGit } from "simple-git";
6
6
  import logger from "takua";
7
- import { randomUUID } from "node:crypto";
7
+ import { spawn } from "node:child_process";
8
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 { setTimeout } from "node:timers/promises";
9
+ import { chmod, copyFile, glob, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
11
10
  import semver from "semver";
12
- //#region src/directory-lock.ts
11
+ import { type } from "arktype";
12
+ import { createHash, 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
13
123
  async function withDirectoryLock(lock, action, waitForCleanup = false) {
14
124
  const deadline = Date.now() + 6e4;
15
125
  for (;;) try {
@@ -26,8 +136,7 @@ async function withDirectoryLock(lock, action, waitForCleanup = false) {
26
136
  await rmdir(lock);
27
137
  }
28
138
  }
29
- //#endregion
30
- //#region src/release-tag.ts
139
+ // src/release-tag.ts
31
140
  function compareInteger(a, b) {
32
141
  return a.length - b.length || (a > b ? 1 : a < b ? -1 : 0);
33
142
  }
@@ -69,8 +178,7 @@ function latestReleaseTag(remoteTags, tagPattern) {
69
178
  }
70
179
  return latest?.ref;
71
180
  }
72
- //#endregion
73
- //#region src/baseline.ts
181
+ // src/baseline.ts
74
182
  const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
75
183
  function isBaseline(value) {
76
184
  if (typeof value !== `object` || value === null) return false;
@@ -149,8 +257,7 @@ async function readBaseline(git, filename, { tagPattern }) {
149
257
  throw new Error(`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`, { cause });
150
258
  }
151
259
  }
152
- //#endregion
153
- //#region src/test-file-state.ts
260
+ // src/test-file-state.ts
154
261
  function isActiveCheck(value) {
155
262
  if (typeof value !== `object` || value === null) return false;
156
263
  const record = value;
@@ -293,127 +400,48 @@ async function withTestFileState(git, baseDirname, action) {
293
400
  });
294
401
  });
295
402
  }
296
- //#endregion
297
- //#region src/npm/command.ts
298
- /** Capture output per environment so parallel runs cannot interleave their reports. */
299
- async function command(commandLine, cwd) {
300
- return await new Promise((resolve, reject) => {
301
- const child = spawn(commandLine, {
302
- cwd,
303
- shell: true,
304
- env: {
305
- ...process.env,
306
- NODE_PATH: ``,
307
- PATH: [
308
- path.join(cwd, `node_modules/.bin`),
309
- path.join(cwd, `../node_modules/.bin`),
310
- process.env.PATH
311
- ].join(path.delimiter)
312
- },
313
- stdio: [
314
- `ignore`,
315
- `pipe`,
316
- `pipe`
317
- ]
318
- });
319
- let output = ``;
320
- const collect = (chunk) => {
321
- output = (output + chunk.toString()).slice(-1e6);
322
- };
323
- child.stdout.on(`data`, collect);
324
- child.stderr.on(`data`, collect);
325
- child.on(`error`, reject);
326
- child.on(`close`, (code, signal) => resolve({
327
- code: signal ? 2 : code ?? 2,
328
- output
329
- }));
330
- });
331
- }
332
- async function prepare(commandLine, cwd) {
333
- const result = await command(commandLine, cwd);
334
- if (result.code !== 0) throw new Error(`Command failed (${result.code}) in ${cwd}: ${commandLine}\n${result.output}`);
335
- }
336
- function errorMessage(error) {
337
- return error instanceof Error ? `${error.message}${error.cause ? `\n${errorMessage(error.cause)}` : ``}` : String(error);
338
- }
339
- //#endregion
340
- //#region src/npm/manifest.ts
341
- function record(value) {
342
- return typeof value === `object` && value !== null && !Array.isArray(value);
343
- }
344
- function packageName(value) {
345
- return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
346
- }
347
- function parseManifest(value, label) {
348
- 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}.`);
349
- 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}.`);
350
- for (const field of [
351
- `dependencies`,
352
- `optionalDependencies`,
353
- `devDependencies`,
354
- `peerDependencies`
355
- ]) {
356
- const entries = value[field];
357
- if (entries !== void 0 && (!record(entries) || Object.entries(entries).some(([name, spec]) => !packageName(name) || typeof spec !== `string`))) throw new Error(`Invalid ${field} in ${label}.`);
358
- }
359
- 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}.`);
360
- return value;
361
- }
362
- async function readManifest(filename) {
363
- return parseManifest(JSON.parse(await readFile(filename, `utf8`)), filename);
364
- }
365
- /** Current support is authored explicitly; only historical shorthand is resolved. */
366
- async function normalizeWorkspacePeers(manifest, versionAtBaseline) {
367
- if (!manifest.peerDependencies) return manifest;
368
- const entries = await Promise.all(Object.entries(manifest.peerDependencies).map(async ([peer, range]) => {
369
- if (!range.startsWith(`workspace:`)) return [peer, range];
370
- const requested = range.slice(`workspace:`.length);
371
- if ([
372
- ``,
373
- `*`,
374
- `^`,
375
- `~`
376
- ].includes(requested)) {
377
- 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.`);
378
- const version = await versionAtBaseline(peer);
379
- if (!semver.valid(version)) throw new Error(`Invalid historical workspace version for ${peer}: ${version}.`);
380
- return [peer, `${requested === `^` || requested === `~` ? requested : ``}${version}`];
381
- }
382
- try {
383
- eras(requested);
384
- } catch {
385
- throw new Error(`Unsupported workspace peer range for ${peer}: ${range}.`);
386
- }
387
- return [peer, requested];
388
- }));
389
- return {
390
- ...manifest,
391
- peerDependencies: Object.fromEntries(entries)
392
- };
393
- }
394
- function eras(range) {
395
- const parts = range.split(`||`).map((part) => part.trim());
396
- if (parts.some((part) => !part || semver.validRange(part) === null)) throw new Error(`Invalid peer range: ${range}.`);
397
- return parts;
398
- }
399
- /** Previews only represent an explicitly enrolled exact preview. */
400
- function matches(version, range) {
401
- return eras(range).some((era) => semver.prerelease(version) ? semver.valid(era.replace(/^=\s*/, ``)) === semver.valid(version) : semver.satisfies(version, era));
402
- }
403
- //#endregion
404
- //#region src/npm/config.ts
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
+ });
405
433
  function nonempty(value) {
406
434
  return typeof value === `string` && value.trim().length > 0;
407
435
  }
408
436
  function validateConfig(value) {
409
437
  if (!record(value)) throw new Error(`Configuration must have an npm object.`);
410
- for (const key of [
411
- `artifact`,
412
- `lockfile`,
413
- `prepareCommand`
414
- ]) if (!nonempty(value[key])) throw new Error(`npm.${key} must be a nonempty string.`);
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.`);
415
441
  if (path.isAbsolute(value.lockfile) || value.lockfile.split(/[\\/]/).includes(`..`)) throw new Error(`npm.lockfile must stay inside each representative.`);
416
- for (const key of [`packageDirectory`, `installCommand`]) if (value[key] !== void 0 && !nonempty(value[key])) throw new Error(`Invalid npm.${key}.`);
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}.`);
417
445
  const globs = typeof value.representatives === `string` ? [value.representatives] : value.representatives;
418
446
  if (!Array.isArray(globs) || !globs.length || globs.some((glob) => !nonempty(glob))) throw new Error(`npm.representatives must contain directory globs.`);
419
447
  if (!record(value.suites) || !Object.keys(value.suites).length) throw new Error(`Configure at least one npm peer suite.`);
@@ -423,23 +451,327 @@ function validateConfig(value) {
423
451
  if (!record(value.localPeers)) throw new Error(`Invalid npm.localPeers.`);
424
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}.`);
425
453
  }
426
- for (const hook of [`releaseIntent`, `certifyBreaking`]) if (value[hook] !== void 0 && typeof value[hook] !== `function`) throw new Error(`npm.${hook} must be a function.`);
427
454
  }
428
455
  function validatePeerRanges(peers) {
429
456
  for (const range of Object.values(peers)) eras(range);
430
457
  }
431
- //#endregion
432
- //#region src/npm/release.ts
433
- async function intent(config, context) {
434
- if (!config.releaseIntent) return { status: `none` };
435
- const result = await config.releaseIntent(context);
436
- if (record(result) && result.status === `none`) return { status: `none` };
437
- if (!record(result) || result.status !== `release` || typeof result.version !== `string` || !semver.valid(result.version) || semver.lt(result.version, context.currentVersion)) throw new Error(`Invalid release intent for ${context.packageName}; expected none or a current/newer exact version.`);
458
+ // src/npm/matrix-model.ts
459
+ const npmMatrixConfigSchema = type({
460
+ "+": `reject`,
461
+ configs: `string | string[]`,
462
+ prepare: type(`string`).or(type(`Function`).as()),
463
+ "label?": type(`Function`).as()
464
+ });
465
+ const transition = type({
466
+ previousVersion: `string`,
467
+ version: `string`,
468
+ delta: `'major' | 'premajor' | 'minor' | 'preminor' | 'patch' | 'prepatch' | 'prerelease' | 'release' | 'none'`
469
+ });
470
+ const identity = type({
471
+ version: `string`,
472
+ source: `'installed' | 'candidate'`
473
+ });
474
+ const reportSchema = type({
475
+ package: {
476
+ name: `string`,
477
+ version: `string`
478
+ },
479
+ mode: `'check' | 'test' | 'coverage-only'`,
480
+ exitCode: `0 | 1 | 2`,
481
+ errors: `string[]`,
482
+ coverage: type({
483
+ peer: `string`,
484
+ era: `string`,
485
+ representatives: `string[]`
486
+ }).array(),
487
+ support: {
488
+ status: `'unchanged' | 'breaking-release' | 'end-of-support' | 'first-release' | 'not-checked' | 'setup-error'`,
489
+ changes: type({
490
+ peer: `string`,
491
+ reason: `'narrowed-range' | 'required-peer'`,
492
+ previous: `string | null`,
493
+ current: `string | null`
494
+ }).array(),
495
+ "previousVersion?": `string`,
496
+ "release?": transition.and({ breaking: `boolean` }),
497
+ "error?": `string`
498
+ },
499
+ representatives: type({
500
+ path: `string`,
501
+ status: `'passed' | 'audited' | 'skipped' | 'compatibility-failure' | 'setup-error'`,
502
+ peers: { "[string]": identity.and({
503
+ path: `string`,
504
+ "archiveVersion?": `string`
505
+ }) },
506
+ suites: type({
507
+ peer: `string`,
508
+ status: `'passed' | 'compatibility-failure' | 'setup-error' | 'not-run'`,
509
+ "output?": `string`
510
+ }).array(),
511
+ warnings: `string[]`,
512
+ "error?": `string`
513
+ }).array()
514
+ }).as();
515
+ const npmPlanSchema = type({
516
+ "+": `reject`,
517
+ formatVersion: `1`,
518
+ id: `string`,
519
+ runId: `string`,
520
+ commit: `string`,
521
+ configs: `string[]`,
522
+ packages: type({
523
+ config: `string`,
524
+ directory: `string`,
525
+ name: `string`,
526
+ configDigest: `string`,
527
+ inputDigest: `string`,
528
+ audit: reportSchema,
529
+ releases: type({
530
+ context: {
531
+ packageName: `string`,
532
+ packageDirectory: `string`,
533
+ currentVersion: `string`,
534
+ "baseline?": {
535
+ version: `string`,
536
+ "ref?": `string`,
537
+ "commit?": `string`
538
+ }
539
+ },
540
+ "transition?": transition
541
+ }).array()
542
+ }).array(),
543
+ environments: type({
544
+ id: `string`,
545
+ label: `string`,
546
+ packageName: `string`,
547
+ representative: `string`,
548
+ suites: `string[]`,
549
+ peers: { "[string]": identity }
550
+ }).array(),
551
+ archives: type({
552
+ path: `string`,
553
+ file: `string`,
554
+ sha256: `string`
555
+ }).array()
556
+ });
557
+ const npmReceiptSchema = type({
558
+ "+": `reject`,
559
+ formatVersion: `1`,
560
+ planId: `string`,
561
+ id: `string`,
562
+ exitCode: `0 | 1 | 2`,
563
+ "report?": reportSchema,
564
+ "error?": `string`
565
+ });
566
+ /** Stable object ordering; arrays retain their semantic order. */
567
+ function canonical(value) {
568
+ if (Array.isArray(value)) return `[${value.map(canonical).join(`,`)}]`;
569
+ if (value && typeof value === `object`) return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(`,`)}}`;
570
+ return JSON.stringify(value) ?? `undefined`;
571
+ }
572
+ function digest(value) {
573
+ return createHash(`sha256`).update(value).digest(`hex`);
574
+ }
575
+ function planDigest(plan) {
576
+ return digest(canonical(plan));
577
+ }
578
+ function peerIdentities(peers) {
579
+ return Object.fromEntries(Object.entries(peers).map(([name, { version, source }]) => [name, {
580
+ version,
581
+ source
582
+ }]));
583
+ }
584
+ function requireMatrix(condition, message) {
585
+ if (!condition) throw new Error(message);
586
+ }
587
+ function sameNames(actual, expected, message) {
588
+ const counts = (names) => {
589
+ const result = /* @__PURE__ */ new Map();
590
+ for (const name of names) result.set(name, (result.get(name) ?? 0) + 1);
591
+ return result;
592
+ };
593
+ const actualCounts = counts(actual);
594
+ const expectedCounts = counts(expected);
595
+ const missing = [...expectedCounts].filter(([name, count]) => (actualCounts.get(name) ?? 0) < count).map(([name]) => name);
596
+ const extra = [...actualCounts].filter(([name, count]) => count > (expectedCounts.get(name) ?? 0)).map(([name]) => name);
597
+ requireMatrix(!missing.length && !extra.length, `${message}${missing.length ? ` Missing: ${missing.join(`, `)}.` : ``}${extra.length ? ` Unexpected or duplicated: ${extra.join(`, `)}.` : ``}`);
598
+ }
599
+ /** Validate the audit as well as the plan envelope; empty or partial plans never pass. */
600
+ function validateNpmPlan(value) {
601
+ const plan = npmPlanSchema.assert(value);
602
+ const { id, ...payload } = plan;
603
+ requireMatrix(id === planDigest(payload), `Plan digest does not match its contents.`);
604
+ requireMatrix(plan.runId.length > 0 && /^[a-f0-9]{40,64}$/.test(plan.commit), `Invalid plan source identity.`);
605
+ requireMatrix(plan.configs.length && plan.packages.length && plan.environments.length && plan.archives.length, `Plan must contain packages, environments, and archives.`);
606
+ for (const [name, values] of [
607
+ [`package`, plan.packages.map((p) => p.name)],
608
+ [`configuration`, plan.packages.map((p) => p.config)],
609
+ [`environment`, plan.environments.map((e) => e.id)],
610
+ [`archive`, plan.archives.map((a) => a.path)]
611
+ ]) requireMatrix(new Set(values).size === values.length, `Duplicate ${name} identities in plan.`);
612
+ for (const archive of plan.archives) requireMatrix(/^[a-f0-9]{64}$/.test(archive.sha256) && archive.file === `archives/${archive.sha256}.tgz`, `Invalid archive digest or bundle path: ${archive.path}.`);
613
+ sameNames([...new Set(plan.environments.map((e) => e.packageName))], plan.packages.map((p) => p.name), `Plan environments do not match package audits.`);
614
+ for (const pkg of plan.packages) {
615
+ const audit = pkg.audit;
616
+ requireMatrix(audit.package.name === pkg.name && audit.mode === `coverage-only` && audit.exitCode === 0 && audit.errors.length === 0 && !audit.support.error, `Coverage audit failed for ${pkg.name}.`);
617
+ requireMatrix([
618
+ `unchanged`,
619
+ `breaking-release`,
620
+ `first-release`
621
+ ].includes(audit.support.status), `Support audit did not pass for ${pkg.name}.`);
622
+ requireMatrix(audit.coverage.length && audit.coverage.every((c) => c.representatives.length), `Uncovered peer era for ${pkg.name}.`);
623
+ const expected = plan.environments.filter((e) => e.packageName === pkg.name);
624
+ requireMatrix(new Set(expected.map((e) => e.representative)).size === expected.length, `Duplicate representative paths for ${pkg.name}.`);
625
+ sameNames(audit.representatives.map((r) => r.path), expected.map((e) => e.representative), `Missing or duplicate audited environments for ${pkg.name}.`);
626
+ for (const env of expected) {
627
+ const result = audit.representatives.find((r) => r.path === env.representative);
628
+ requireMatrix(result.status === `audited` && !result.error && env.suites.length, `Environment was not audited: ${pkg.name} / ${env.representative}.`);
629
+ sameNames(result.suites.map((s) => s.peer), env.suites, `Audited suites differ: ${env.id}.`);
630
+ requireMatrix(new Set(env.suites).size === env.suites.length && result.suites.every((s) => s.status === `not-run`), `Invalid audited suites: ${env.id}.`);
631
+ requireMatrix(canonical(peerIdentities(result.peers)) === canonical(env.peers), `Audited peer identities differ: ${env.id}.`);
632
+ }
633
+ for (const coverage of audit.coverage) for (const name of coverage.representatives) requireMatrix(expected.some((e) => e.representative === name && e.suites.includes(coverage.peer)), `Coverage references an unplanned suite: ${pkg.name} / ${name} / ${coverage.peer}.`);
634
+ }
635
+ return plan;
636
+ }
637
+ /** Pure receipt verification; runner-local installation paths are diagnostic only. */
638
+ function verifyNpmReceipts(value, receipts) {
639
+ const errors = [];
640
+ let environments = 0;
641
+ try {
642
+ const plan = validateNpmPlan(value);
643
+ environments = plan.environments.length;
644
+ const parsed = receipts.map((r, i) => {
645
+ try {
646
+ return npmReceiptSchema.assert(r);
647
+ } catch (cause) {
648
+ throw new Error(`Malformed receipt ${i + 1}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
649
+ }
650
+ });
651
+ const environmentName = (id) => {
652
+ const env = plan.environments.find((e) => e.id === id);
653
+ return env ? `${env.packageName} / ${env.representative} (${id})` : id;
654
+ };
655
+ sameNames(parsed.map((r) => environmentName(r.id)), plan.environments.map((e) => environmentName(e.id)), `Missing, duplicate, or unexpected environment receipts.`);
656
+ for (const env of plan.environments) try {
657
+ const receipt = parsed.find((r) => r.id === env.id);
658
+ requireMatrix(receipt.planId === plan.id, `Receipt belongs to a different plan.`);
659
+ requireMatrix(receipt.exitCode === 0 && !receipt.error && receipt.report, receipt.error ?? `Environment execution failed.`);
660
+ const report = receipt.report;
661
+ const pkg = plan.packages.find((p) => p.name === env.packageName);
662
+ requireMatrix(canonical(report.package) === canonical(pkg.audit.package), `Wrong package identity.`);
663
+ requireMatrix(report.mode === `test` && report.exitCode === 0 && report.errors.length === 0 && report.support.status === `not-checked` && !report.support.error, `Receipt does not contain a successful execution report.`);
664
+ sameNames(report.representatives.map((r) => r.path), pkg.audit.representatives.map((r) => r.path), `Missing or duplicate representatives in execution report.`);
665
+ for (const result of report.representatives) {
666
+ if (result.path !== env.representative) {
667
+ requireMatrix(result.status === `skipped` && result.suites.length === 0 && !result.error, `Unexpected execution: ${result.path}.`);
668
+ continue;
669
+ }
670
+ requireMatrix(result.status === `passed` && !result.error, `Environment did not execute successfully.`);
671
+ sameNames(result.suites.map((s) => s.peer), env.suites, `Missing, duplicate, or unexpected relationship suites.`);
672
+ for (const suite of result.suites) requireMatrix(suite.status === `passed`, `Suite did not pass: ${suite.peer}.`);
673
+ requireMatrix(canonical(peerIdentities(result.peers)) === canonical(env.peers), `Installed peer identities differ from the audit.`);
674
+ }
675
+ } catch (error) {
676
+ errors.push(`${env.packageName} / ${env.representative}: ${error instanceof Error ? error.message : String(error)}`);
677
+ }
678
+ } catch (error) {
679
+ errors.push(error instanceof Error ? error.message : String(error));
680
+ }
681
+ return {
682
+ exitCode: errors.length ? 2 : 0,
683
+ errors,
684
+ environments
685
+ };
686
+ }
687
+ // src/config.ts
688
+ const configFields = {
689
+ tagPattern: type(`string`),
690
+ baselineFile: type(`string`),
691
+ testPattern: type(`string`),
692
+ testCommand: type(`string`),
693
+ test: type(`string`),
694
+ baseDirname: type(`string`),
695
+ verbose: type(`boolean`),
696
+ out: type(`string`)
697
+ };
698
+ const configSchema = type({
699
+ "+": `reject`,
700
+ "tagPattern?": configFields.tagPattern.or(`undefined`),
701
+ "baselineFile?": configFields.baselineFile.or(`undefined`),
702
+ "testPattern?": configFields.testPattern,
703
+ "testCommand?": configFields.testCommand,
704
+ "test?": configFields.test,
705
+ "baseDirname?": configFields.baseDirname,
706
+ "verbose?": configFields.verbose.or(`undefined`),
707
+ "out?": configFields.out,
708
+ "release?": releaseSchema,
709
+ "npm?": npmConfigSchema,
710
+ "npmMatrix?": npmMatrixConfigSchema
711
+ });
712
+ /** The complete config, with no fields exposed to argv by this loader. */
713
+ const configOptions = options(`Breakage configuration`, configSchema, {
714
+ tagPattern: { configOnly: true },
715
+ baselineFile: { configOnly: true },
716
+ testPattern: { configOnly: true },
717
+ testCommand: { configOnly: true },
718
+ test: { configOnly: true },
719
+ baseDirname: { configOnly: true },
720
+ verbose: { configOnly: true },
721
+ out: { configOnly: true },
722
+ release: { configOnly: true },
723
+ npm: { configOnly: true },
724
+ npmMatrix: { configOnly: true }
725
+ });
726
+ /** Share the root release model with npm checks unless they supply their own. */
727
+ function defineConfig(config) {
728
+ if (config && typeof config === `object`) {
729
+ rejectCertificationOptions(config);
730
+ if (config.npm) rejectCertificationOptions(config.npm);
731
+ if (config.release !== void 0) validateRelease(config.release);
732
+ if (config.npm?.release !== void 0) validateRelease(config.npm.release);
733
+ }
734
+ configSchema.assert(config);
438
735
  return {
439
- status: `release`,
440
- version: result.version
736
+ ...config,
737
+ ...config.npm ? { npm: {
738
+ ...config.npm,
739
+ ...config.release && config.npm.release === void 0 ? { release: config.release } : {}
740
+ } } : {}
441
741
  };
442
742
  }
743
+ function defaultConfig(directory) {
744
+ const filename = path.join(directory, `breakage.config.ts`);
745
+ 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.`);
746
+ return filename;
747
+ }
748
+ /** Comline ignores absent discovered files; explicit paths must exist. */
749
+ function configPath(filename, required = true) {
750
+ const absolute = path.resolve(filename);
751
+ if (path.extname(absolute) !== `.ts`) throw new Error(`Configuration must be a .ts module with a default export; JSON configuration was removed.`);
752
+ if (required && !existsSync(absolute)) throw new Error(`Configuration file does not exist: ${absolute}`);
753
+ return absolute;
754
+ }
755
+ /** Apply application semantics after Comline validates and merges configuration. */
756
+ function resolveConfig(config, filename) {
757
+ return {
758
+ ...defineConfig(config),
759
+ baseDirname: path.resolve(filename ? path.dirname(filename) : process.cwd(), config.baseDirname ?? `.`)
760
+ };
761
+ }
762
+ /** Use Comline's module loading and full-schema validation for programmatic and npm consumers. */
763
+ async function loadConfig(filename) {
764
+ const absolute = configPath(filename);
765
+ return resolveConfig((await cli({
766
+ cliName: `breakage`,
767
+ routeOptions: { "": configOptions },
768
+ discoverConfigPath: () => absolute
769
+ }).async([process.execPath, `breakage`])).inputs.opts, absolute);
770
+ }
771
+ // src/npm/release.ts
772
+ async function releaseVersion(config, context) {
773
+ return config.release ? (await resolveReleaseDelta(config.release, context))?.version : void 0;
774
+ }
443
775
  /**
444
776
  * Compare the union, including adjacent alternatives. semver.subset compares each
445
777
  * conjunction to a single conjunction and cannot prove coverage by their union.
@@ -518,8 +850,7 @@ async function previousManifest(config, directory) {
518
850
  return historical.version;
519
851
  });
520
852
  }
521
- //#endregion
522
- //#region src/npm/environment.ts
853
+ // src/npm/environment.ts
523
854
  function within(parent, child) {
524
855
  const relative = path.relative(parent, child);
525
856
  return relative === `` || !relative.startsWith(`..${path.sep}`) && relative !== `..` && !path.isAbsolute(relative);
@@ -624,13 +955,13 @@ async function maySelectEnvironment(config, base, directory, packageManifest, se
624
955
  const packageDirectory = path.resolve(base, local.directory);
625
956
  const current = await readManifest(path.join(packageDirectory, `package.json`));
626
957
  if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
627
- const release = await intent(config, {
958
+ const release = await releaseVersion(config, {
628
959
  packageName: peer,
629
960
  packageDirectory,
630
961
  currentVersion: current.version
631
962
  });
632
- if (release.status === `none`) throw new Error(`Candidate ${peer} has no certified release intent.`);
633
- return matches(release.version, range);
963
+ if (release === void 0) throw new Error(`Candidate ${peer} has no release version.`);
964
+ return matches(release, range);
634
965
  }
635
966
  const declared = declarations[peer];
636
967
  return declared !== void 0 && matches(declared, range);
@@ -652,17 +983,17 @@ async function prepareEnvironment(config, base, directory, stage, packageManifes
652
983
  const localManifest = await normalizeWorkspacePeers(await readManifest(path.join(localDirectory, `package.json`)));
653
984
  if (localManifest.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
654
985
  validatePeerRanges(localManifest.peerDependencies ?? {});
655
- const evidence = await intent(config, {
986
+ const evidence = await releaseVersion(config, {
656
987
  packageName: peer,
657
988
  packageDirectory: localDirectory,
658
989
  currentVersion: localManifest.version
659
990
  });
660
- if (evidence.status === `none`) throw new Error(`Candidate ${peer} has no certified release intent.`);
991
+ if (evidence === void 0) throw new Error(`Candidate ${peer} has no release version.`);
661
992
  candidates[peer] = {
662
993
  directory: localDirectory,
663
994
  artifact: path.resolve(base, local.artifact),
664
995
  currentVersion: localManifest.version,
665
- version: evidence.version,
996
+ version: evidence,
666
997
  manifest: localManifest
667
998
  };
668
999
  }
@@ -866,8 +1197,7 @@ async function verifyResolution(manifest, environment) {
866
1197
  } else pending.push(await resolvePackage(directory, dependency, installation));
867
1198
  }
868
1199
  }
869
- //#endregion
870
- //#region src/npm/check.ts
1200
+ // src/npm/check.ts
871
1201
  async function checkSupport(config, base, manifest) {
872
1202
  try {
873
1203
  const previous = await previousManifest(config, base);
@@ -882,29 +1212,20 @@ async function checkSupport(config, base, manifest) {
882
1212
  previousVersion: previous.version,
883
1213
  changes
884
1214
  };
885
- const evidence = await intent(config, {
1215
+ const evidence = config.release ? await resolveReleaseDelta(config.release, {
886
1216
  packageName: manifest.name,
887
1217
  packageDirectory: base,
888
- currentVersion: manifest.version
889
- });
890
- if (evidence.status === `none`) return {
1218
+ currentVersion: manifest.version,
1219
+ baseline: { version: previous.version }
1220
+ }) : void 0;
1221
+ if (evidence === void 0) return {
891
1222
  status: `end-of-support`,
892
1223
  previousVersion: previous.version,
893
1224
  changes
894
1225
  };
895
- if (!semver.gt(evidence.version, previous.version)) throw new Error(`The intended release must be newer than baseline ${previous.version}.`);
896
- const release = {
897
- version: evidence.version,
898
- delta: semver.diff(previous.version, evidence.version) ?? `none`
899
- };
900
- const certified = config.certifyBreaking ? await config.certifyBreaking({
901
- previousVersion: previous.version,
902
- release,
903
- changes
904
- }) : false;
905
- if (typeof certified !== `boolean`) throw new Error(`certifyBreaking must return a boolean.`);
1226
+ const release = versionDelta(evidence.previousVersion, evidence.version, config.release?.policy);
906
1227
  return {
907
- status: certified ? `certified` : `end-of-support`,
1228
+ status: release.breaking ? `breaking-release` : `end-of-support`,
908
1229
  previousVersion: previous.version,
909
1230
  changes,
910
1231
  release
@@ -925,21 +1246,21 @@ async function checkLocalReleases(config, base, manifest, representatives) {
925
1246
  const directory = path.resolve(base, local.directory);
926
1247
  const current = await readManifest(path.join(directory, `package.json`));
927
1248
  if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
928
- const release = await intent(config, {
1249
+ const release = await releaseVersion(config, {
929
1250
  packageName: peer,
930
1251
  packageDirectory: directory,
931
1252
  currentVersion: current.version
932
1253
  });
933
- if (release.status === `none`) return [];
934
- if (!matches(release.version, range)) return [`Declared support for ${peer}: ${range} does not include its planned local release ${release.version}. Update the peer range to include the intended version.`];
935
- if (!representatives.some((result) => result.peers[peer]?.source === `candidate` && result.peers[peer]?.version === release.version && result.suites.some((suite) => suite.peer === peer))) return [`No coordinated representative covers planned local peer ${peer}@${release.version}. Enroll ${peer} with breakage.candidates and configure its relationship suite.`];
1254
+ if (release === void 0) return [];
1255
+ 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.`];
1256
+ 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.`];
936
1257
  return [];
937
1258
  } catch (error) {
938
1259
  return [`Cannot inspect planned local release for ${peer}: ${errorMessage(error)}`];
939
1260
  }
940
1261
  }))).flat();
941
1262
  }
942
- async function runRepresentative(config, base, manifest, directory, coverageOnly, selection) {
1263
+ async function runRepresentative(config, base, manifest, directory, coverageOnly, selection, plannedSuites) {
943
1264
  const result = {
944
1265
  path: path.relative(base, directory).split(path.sep).join(`/`),
945
1266
  status: `skipped`,
@@ -962,6 +1283,7 @@ async function runRepresentative(config, base, manifest, directory, coverageOnly
962
1283
  const peers = Object.keys(config.suites).filter((peer) => {
963
1284
  const evidence = result.peers[peer];
964
1285
  if (!evidence) return false;
1286
+ if (plannedSuites !== void 0) return plannedSuites.includes(peer);
965
1287
  if (selection && `peer` in selection) return peer === selection.peer;
966
1288
  if (selection && `representative` in selection) return true;
967
1289
  return matches(evidence.version, manifest.peerDependencies[peer]);
@@ -996,7 +1318,7 @@ async function runRepresentative(config, base, manifest, directory, coverageOnly
996
1318
  return result;
997
1319
  }
998
1320
  }
999
- async function run(config, baseDirectory, coverageOnly, selection) {
1321
+ async function run(config, baseDirectory, coverageOnly, selection, plannedSuites) {
1000
1322
  const report = {
1001
1323
  package: {
1002
1324
  name: `(unresolved)`,
@@ -1028,20 +1350,28 @@ async function run(config, baseDirectory, coverageOnly, selection) {
1028
1350
  eras(selection.range);
1029
1351
  }
1030
1352
  const releases = /* @__PURE__ */ new Map();
1031
- const configuredHook = config.releaseIntent;
1353
+ const resolveDelta = (context) => {
1354
+ const key = JSON.stringify([
1355
+ context.packageDirectory,
1356
+ context.currentVersion,
1357
+ context.baseline
1358
+ ]);
1359
+ let pending = releases.get(key);
1360
+ if (!pending) {
1361
+ pending = resolveReleaseDelta(config.release, context);
1362
+ releases.set(key, pending);
1363
+ }
1364
+ return pending;
1365
+ };
1032
1366
  const effective = {
1033
1367
  ...config,
1034
- releaseIntent: (context) => {
1035
- let pending = releases.get(context.packageDirectory);
1036
- if (!pending) {
1037
- pending = Promise.resolve().then(() => configuredHook ? configuredHook(context) : { status: `none` });
1038
- releases.set(context.packageDirectory, pending);
1039
- }
1040
- return pending;
1041
- }
1368
+ ...config.release ? { release: {
1369
+ ...config.release,
1370
+ resolveDelta
1371
+ } } : {}
1042
1372
  };
1043
1373
  const directories = await discover(config, base);
1044
- 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)))]);
1374
+ 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, plannedSuites)))]);
1045
1375
  report.support = support;
1046
1376
  report.representatives = representatives;
1047
1377
  if (!selection) {
@@ -1070,6 +1400,10 @@ async function npmCheck(config, options = {}) {
1070
1400
  async function npmTest(config, selection, options = {}) {
1071
1401
  return await run(config, options.baseDirectory ?? process.cwd(), false, selection);
1072
1402
  }
1403
+ /** Internal matrix entrypoint: keep the full config for validation and peer identity checks. */
1404
+ async function npmTestPlanned(config, selection, options = {}) {
1405
+ return await run(config, options.baseDirectory ?? process.cwd(), false, { representative: selection.representative }, selection.suites);
1406
+ }
1073
1407
  function formatNpmReport(report) {
1074
1408
  const lines = [`${report.package.name}@${report.package.version} — npm ${report.mode}`];
1075
1409
  for (const result of report.representatives) {
@@ -1090,9 +1424,401 @@ function formatNpmReport(report) {
1090
1424
  if (report.mode === `coverage-only`) lines.push(`Coverage audit only; compatibility assertions were not executed.`);
1091
1425
  return `${lines.join(`\n`)}\n`;
1092
1426
  }
1093
- //#endregion
1094
- //#region src/breakage.ts
1095
- async function breakage({ tagPattern, baselineFile, testPattern, testCommand, certifyCommand, baseDirname = process.cwd(), verbose = false }) {
1427
+ // src/npm/matrix.ts
1428
+ function relative(root, filename) {
1429
+ const result = path.relative(root, path.resolve(filename)).split(path.sep).join(`/`) || `.`;
1430
+ requireMatrix(!result.startsWith(`../`) && result !== `..` && !path.isAbsolute(result), `Matrix input escapes the source checkout: ${filename}.`);
1431
+ return result;
1432
+ }
1433
+ function portablePath(value) {
1434
+ requireMatrix(value.length && !value.includes(`\\`) && !path.posix.isAbsolute(value) && !/^[A-Za-z]:/.test(value) && !value.split(`/`).includes(`..`), `Invalid portable path: ${value}.`);
1435
+ }
1436
+ async function contained(root, filename) {
1437
+ portablePath(filename);
1438
+ const absolute = path.resolve(root, filename);
1439
+ let ancestor = absolute;
1440
+ for (;;) try {
1441
+ await lstat(ancestor);
1442
+ break;
1443
+ } catch (error) {
1444
+ if (error.code !== `ENOENT`) throw error;
1445
+ ancestor = path.dirname(ancestor);
1446
+ }
1447
+ requireMatrix(within(await realpath(root), await realpath(ancestor)), `Path escapes its root through a symlink: ${filename}.`);
1448
+ return absolute;
1449
+ }
1450
+ async function fileDigest(filename) {
1451
+ requireMatrix((await lstat(filename)).isFile(), `Expected a regular file: ${filename}.`);
1452
+ return digest(await readFile(filename));
1453
+ }
1454
+ async function sourceIdentity(root) {
1455
+ const git = simpleGit(root);
1456
+ requireMatrix(await realpath(await git.revparse([`--show-toplevel`])) === await realpath(root), `Matrix rootDirectory must be the Git checkout root.`);
1457
+ requireMatrix(!(await git.status()).files.length, `Matrix planning and execution require a clean checkout, including untracked files. Ignore generated archives, bundles, and receipts.`);
1458
+ return await git.revparse([`HEAD`]);
1459
+ }
1460
+ async function ignoredOutput(root, filename) {
1461
+ const git = simpleGit(root);
1462
+ requireMatrix(!(await git.raw([
1463
+ `ls-files`,
1464
+ `--`,
1465
+ filename
1466
+ ])).trim() && (await git.raw([
1467
+ `check-ignore`,
1468
+ `--`,
1469
+ filename
1470
+ ])).trim(), `Matrix output must be gitignored and untracked: ${filename}.`);
1471
+ }
1472
+ async function discoverConfigs(root, patterns) {
1473
+ const files = /* @__PURE__ */ new Set();
1474
+ for (const pattern of patterns) portablePath(pattern);
1475
+ for await (const filename of glob(patterns, {
1476
+ cwd: root,
1477
+ exclude: [
1478
+ `**/node_modules/**`,
1479
+ `**/.git/**`,
1480
+ `**/test/**`
1481
+ ]
1482
+ })) {
1483
+ const name = relative(root, path.resolve(root, filename));
1484
+ if ((await loadConfig(await contained(root, name))).npm) files.add(name);
1485
+ }
1486
+ requireMatrix(files.size, `No npm configurations matched the matrix configs.`);
1487
+ return [...files].sort();
1488
+ }
1489
+ function configDigest(config, root, base) {
1490
+ return digest(canonical({
1491
+ ...config,
1492
+ packageDirectory: relative(root, base),
1493
+ artifact: relative(root, path.resolve(base, config.artifact)),
1494
+ baseline: `baselineFile` in config.baseline && config.baseline.baselineFile ? {
1495
+ ...config.baseline,
1496
+ baselineFile: relative(root, path.resolve(base, config.baseline.baselineFile))
1497
+ } : config.baseline,
1498
+ localPeers: Object.fromEntries(Object.entries(config.localPeers ?? {}).map(([name, peer]) => [name, {
1499
+ directory: relative(root, path.resolve(base, peer.directory)),
1500
+ artifact: relative(root, path.resolve(base, peer.artifact))
1501
+ }])),
1502
+ release: config.release ? {
1503
+ resolveDelta: !!config.release.resolveDelta,
1504
+ policy: !!config.release.policy
1505
+ } : void 0
1506
+ }));
1507
+ }
1508
+ async function packageConfig(root, filename) {
1509
+ const config = (await loadConfig(await contained(root, filename))).npm;
1510
+ validateConfig(config);
1511
+ const baseDirectory = path.dirname(path.resolve(root, filename));
1512
+ const base = path.resolve(baseDirectory, config.packageDirectory ?? `.`);
1513
+ await contained(root, relative(root, base));
1514
+ return {
1515
+ config,
1516
+ base,
1517
+ baseDirectory
1518
+ };
1519
+ }
1520
+ function portableContext(root, context) {
1521
+ return {
1522
+ ...context,
1523
+ packageDirectory: relative(root, context.packageDirectory)
1524
+ };
1525
+ }
1526
+ async function archiveRequests(root, config, base) {
1527
+ const manifest = await readManifest(path.join(base, `package.json`));
1528
+ const requests = [{
1529
+ packageName: manifest.name,
1530
+ directory: relative(root, base),
1531
+ artifact: relative(root, path.resolve(base, config.artifact))
1532
+ }];
1533
+ for (const [packageName, peer] of Object.entries(config.localPeers ?? {})) {
1534
+ const directory = relative(root, path.resolve(base, peer.directory));
1535
+ try {
1536
+ await lstat(await contained(root, directory));
1537
+ } catch (error) {
1538
+ if (error.code === `ENOENT` && !manifest.peerDependencies?.[packageName]) continue;
1539
+ throw error;
1540
+ }
1541
+ requests.push({
1542
+ packageName,
1543
+ directory,
1544
+ artifact: relative(root, path.resolve(base, peer.artifact))
1545
+ });
1546
+ }
1547
+ return requests;
1548
+ }
1549
+ async function inputDigest(root, filename, config, base) {
1550
+ const files = /* @__PURE__ */ new Set([filename, relative(root, path.join(base, `package.json`))]);
1551
+ const directories = await discover(config, base);
1552
+ for (const directory of directories) for (const name of [`package.json`, config.lockfile]) files.add(relative(root, path.join(directory, name)));
1553
+ for (const request of await archiveRequests(root, config, base)) files.add(relative(root, path.resolve(root, request.directory, `package.json`)));
1554
+ if (`baselineFile` in config.baseline && config.baseline.baselineFile) files.add(relative(root, path.resolve(base, config.baseline.baselineFile)));
1555
+ const exclude = [
1556
+ `**/node_modules/**`,
1557
+ `**/.git/**`,
1558
+ ...directories.flatMap((directory) => {
1559
+ const stage = relative(base, path.join(directory, `test`));
1560
+ return [stage, `${stage}/**`];
1561
+ })
1562
+ ];
1563
+ for (const suite of Object.values(config.suites)) for (const pattern of suite.files) {
1564
+ portablePath(pattern);
1565
+ let count = 0;
1566
+ for await (const file of glob(pattern, {
1567
+ cwd: base,
1568
+ exclude
1569
+ })) {
1570
+ const absolute = path.resolve(base, file);
1571
+ if ((await lstat(absolute)).isDirectory()) continue;
1572
+ files.add(relative(root, absolute));
1573
+ count++;
1574
+ }
1575
+ requireMatrix(count, `Suite pattern matched no files: ${pattern}.`);
1576
+ }
1577
+ return digest(canonical(await Promise.all([...files].sort().map(async (file) => [file, await fileDigest(await contained(root, file))]))));
1578
+ }
1579
+ /** Audit all configured packages and prepare one immutable, portable archive bundle. */
1580
+ async function npmPlan(config, options = {}) {
1581
+ npmMatrixConfigSchema.assert(config);
1582
+ const root = path.resolve(options.rootDirectory ?? process.cwd());
1583
+ const output = options.bundleDirectory ?? `.breakage/npm-plan`;
1584
+ const bundle = await contained(root, relative(root, path.resolve(root, output)));
1585
+ await ignoredOutput(root, path.join(bundle, `plan.json`));
1586
+ await mkdir(bundle, { recursive: true });
1587
+ return await withDirectoryLock(path.join(bundle, `.lock`), async () => {
1588
+ await rm(path.join(bundle, `plan.json`), { force: true });
1589
+ const commit = await sourceIdentity(root);
1590
+ const patterns = typeof config.configs === `string` ? [config.configs] : config.configs;
1591
+ const files = await discoverConfigs(root, patterns);
1592
+ const packages = [];
1593
+ const environments = [];
1594
+ const requests = /* @__PURE__ */ new Map();
1595
+ for (const filename of files) {
1596
+ const { config: npm, base, baseDirectory } = await packageConfig(root, filename);
1597
+ const manifest = await readManifest(path.join(base, `package.json`));
1598
+ for (const peer of Object.keys(manifest.peerDependencies ?? {})) requireMatrix(npm.suites[peer] || manifest.peerDependenciesMeta?.[peer]?.optional, `Missing relationship suite for ${manifest.name}: ${peer}.`);
1599
+ const releases = [];
1600
+ const audit = await npmCheck({
1601
+ ...npm,
1602
+ ...npm.release ? { release: {
1603
+ ...npm.release,
1604
+ resolveDelta: async (context) => {
1605
+ const transition = await resolveReleaseDelta(npm.release, context);
1606
+ releases.push({
1607
+ context: portableContext(root, context),
1608
+ ...transition ? { transition } : {}
1609
+ });
1610
+ return transition;
1611
+ }
1612
+ } } : {}
1613
+ }, {
1614
+ baseDirectory,
1615
+ coverageOnly: true
1616
+ });
1617
+ requireMatrix(audit.exitCode === 0, `Cannot plan ${manifest.name}:\n${formatNpmReport(audit)}`);
1618
+ for (const result of audit.representatives) {
1619
+ requireMatrix(result.status === `audited` && result.suites.length, `Environment has no executable audited suites: ${manifest.name} / ${result.path}.`);
1620
+ const environment = JSON.parse(await readFile(path.join(base, result.path, `package.json`), `utf8`));
1621
+ const label = config.label?.({
1622
+ packageName: manifest.name,
1623
+ representative: result.path,
1624
+ peers: result.peers,
1625
+ ...typeof environment.description === `string` ? { description: environment.description } : {}
1626
+ }) ?? `${manifest.name}: ${result.path}`;
1627
+ requireMatrix(typeof label === `string` && label.trim(), `Invalid label for ${manifest.name} / ${result.path}.`);
1628
+ environments.push({
1629
+ id: digest(`${filename}\0${result.path}`).slice(0, 24),
1630
+ label,
1631
+ packageName: manifest.name,
1632
+ representative: result.path,
1633
+ suites: result.suites.map((s) => s.peer).sort(),
1634
+ peers: peerIdentities(result.peers)
1635
+ });
1636
+ for (const peer of Object.values(result.peers)) peer.path = `(runner-local)`;
1637
+ }
1638
+ packages.push({
1639
+ config: filename,
1640
+ directory: relative(root, base),
1641
+ name: manifest.name,
1642
+ configDigest: configDigest(npm, root, base),
1643
+ inputDigest: await inputDigest(root, filename, npm, base),
1644
+ audit,
1645
+ releases: releases.sort((a, b) => canonical(a.context).localeCompare(canonical(b.context)))
1646
+ });
1647
+ for (const request of await archiveRequests(root, npm, base)) {
1648
+ const previous = requests.get(request.artifact);
1649
+ requireMatrix(!previous || canonical(previous) === canonical(request), `Conflicting archive requests: ${request.artifact}.`);
1650
+ requests.set(request.artifact, request);
1651
+ }
1652
+ }
1653
+ const archives = [...requests.values()].sort((a, b) => a.artifact.localeCompare(b.artifact));
1654
+ for (const request of archives) {
1655
+ await contained(root, request.directory);
1656
+ await contained(root, request.artifact);
1657
+ await ignoredOutput(root, path.resolve(root, request.artifact));
1658
+ }
1659
+ if (typeof config.prepare === `string`) await prepare(config.prepare, root);
1660
+ else await config.prepare({
1661
+ rootDirectory: root,
1662
+ archives
1663
+ });
1664
+ const packed = [];
1665
+ for (const archive of archives) {
1666
+ const filename = await contained(root, archive.artifact);
1667
+ const sha256 = await fileDigest(filename);
1668
+ const file = `archives/${sha256}.tgz`;
1669
+ await mkdir(path.join(bundle, `archives`), { recursive: true });
1670
+ await copyFile(filename, await contained(bundle, file));
1671
+ packed.push({
1672
+ path: archive.artifact,
1673
+ file,
1674
+ sha256
1675
+ });
1676
+ }
1677
+ requireMatrix(await sourceIdentity(root) === commit, `Source changed while preparing the matrix.`);
1678
+ const payload = {
1679
+ formatVersion: 1,
1680
+ runId: randomUUID(),
1681
+ commit,
1682
+ configs: patterns,
1683
+ packages,
1684
+ environments,
1685
+ archives: packed
1686
+ };
1687
+ const plan = validateNpmPlan({
1688
+ ...payload,
1689
+ id: planDigest(payload)
1690
+ });
1691
+ await validateWorkspace(plan, {
1692
+ rootDirectory: root,
1693
+ bundleDirectory: bundle
1694
+ });
1695
+ await writeMatrixJson(path.join(bundle, `plan.json`), plan);
1696
+ return plan;
1697
+ });
1698
+ }
1699
+ async function validateWorkspace(plan, options) {
1700
+ const root = path.resolve(options.rootDirectory ?? process.cwd());
1701
+ const bundle = path.resolve(root, options.bundleDirectory ?? `.breakage/npm-plan`);
1702
+ requireMatrix(await sourceIdentity(root) === plan.commit, `Plan belongs to a different source commit.`);
1703
+ requireMatrix(canonical(await discoverConfigs(root, plan.configs)) === canonical(plan.packages.map((p) => p.config).sort()), `Plan no longer enumerates every configured npm package.`);
1704
+ const configs = /* @__PURE__ */ new Map();
1705
+ const requiredArchives = /* @__PURE__ */ new Set();
1706
+ for (const pkg of plan.packages) {
1707
+ const loaded = await packageConfig(root, pkg.config);
1708
+ const { config, base } = loaded;
1709
+ for (const request of await archiveRequests(root, config, base)) requiredArchives.add(request.artifact);
1710
+ requireMatrix(relative(root, base) === pkg.directory && configDigest(config, root, base) === pkg.configDigest, `Configuration changed for ${pkg.name}.`);
1711
+ requireMatrix(await inputDigest(root, pkg.config, config, base) === pkg.inputDigest, `Contract, manifest, or lockfile inputs changed for ${pkg.name}.`);
1712
+ requireMatrix(canonical((await discover(config, base)).map((d) => relative(base, d)).sort()) === canonical(pkg.audit.representatives.map((r) => r.path).sort()), `Representative discovery changed for ${pkg.name}.`);
1713
+ for (const release of pkg.releases) {
1714
+ requireMatrix(config.release, `Release configuration disappeared for ${pkg.name}.`);
1715
+ requireMatrix(canonical(await resolveReleaseDelta(config.release, {
1716
+ ...release.context,
1717
+ packageDirectory: await contained(root, release.context.packageDirectory)
1718
+ })) === canonical(release.transition), `Release evidence changed for ${pkg.name} / ${release.context.packageName}.`);
1719
+ }
1720
+ if (pkg.audit.support.release) {
1721
+ const expected = pkg.audit.support.release;
1722
+ requireMatrix(canonical(versionDelta(expected.previousVersion, expected.version, config.release?.policy)) === canonical(expected), `Release policy changed for ${pkg.name}.`);
1723
+ }
1724
+ configs.set(pkg.name, loaded);
1725
+ }
1726
+ requireMatrix(canonical([...requiredArchives].sort()) === canonical(plan.archives.map((a) => a.path).sort()), `Planned archives do not match the configured packages and local peers.`);
1727
+ for (const archive of plan.archives) {
1728
+ portablePath(archive.path);
1729
+ requireMatrix(archive.file === `archives/${archive.sha256}.tgz`, `Invalid bundled archive path: ${archive.file}.`);
1730
+ requireMatrix(await fileDigest(await contained(bundle, archive.file)) === archive.sha256, `Candidate archive changed: ${archive.path}.`);
1731
+ }
1732
+ return {
1733
+ root,
1734
+ bundle,
1735
+ configs
1736
+ };
1737
+ }
1738
+ /** Execute exactly one audited environment, producing failure evidence whenever the plan is valid. */
1739
+ async function npmExecute(value, id, options = {}) {
1740
+ const plan = validateNpmPlan(value);
1741
+ const receipt = {
1742
+ formatVersion: 1,
1743
+ planId: plan.id,
1744
+ id,
1745
+ exitCode: 2
1746
+ };
1747
+ try {
1748
+ const env = plan.environments.find((e) => e.id === id);
1749
+ requireMatrix(env, `Unknown planned environment: ${id}.`);
1750
+ const { root, bundle, configs } = await validateWorkspace(plan, options);
1751
+ const { config, baseDirectory } = configs.get(env.packageName);
1752
+ const byPath = new Map(plan.archives.map((a) => [a.path, path.join(bundle, a.file)]));
1753
+ const base = path.resolve(baseDirectory, config.packageDirectory ?? `.`);
1754
+ const artifactFor = (filename) => {
1755
+ const archive = byPath.get(relative(root, path.resolve(base, filename)));
1756
+ requireMatrix(archive, `Plan is missing archive: ${filename}.`);
1757
+ return archive;
1758
+ };
1759
+ receipt.report = await npmTestPlanned({
1760
+ ...config,
1761
+ artifact: artifactFor(config.artifact),
1762
+ localPeers: Object.fromEntries(Object.entries(config.localPeers ?? {}).map(([name, peer]) => [name, {
1763
+ ...peer,
1764
+ artifact: byPath.get(relative(root, path.resolve(base, peer.artifact))) ?? peer.artifact
1765
+ }]))
1766
+ }, {
1767
+ representative: env.representative,
1768
+ suites: env.suites
1769
+ }, { baseDirectory });
1770
+ receipt.exitCode = receipt.report.exitCode;
1771
+ await validateWorkspace(plan, options);
1772
+ if (receipt.exitCode === 0) {
1773
+ const result = receipt.report.representatives.find((r) => r.path === env.representative);
1774
+ requireMatrix(result?.status === `passed` && canonical(result.suites.map((s) => s.peer).sort()) === canonical(env.suites) && result.suites.every((s) => s.status === `passed`) && canonical(peerIdentities(result.peers)) === canonical(env.peers), `Execution differs from the planned suites or peer identities: ${env.label}.`);
1775
+ }
1776
+ } catch (error) {
1777
+ receipt.exitCode = 2;
1778
+ receipt.error = errorMessage(error);
1779
+ }
1780
+ return receipt;
1781
+ }
1782
+ /** Verify source/config/archive identities and then require all planned execution receipts. */
1783
+ async function npmVerify(value, receipts, options = {}) {
1784
+ try {
1785
+ const plan = validateNpmPlan(value);
1786
+ await validateWorkspace(plan, options);
1787
+ return verifyNpmReceipts(plan, receipts);
1788
+ } catch (error) {
1789
+ return {
1790
+ exitCode: 2,
1791
+ errors: [errorMessage(error)],
1792
+ environments: 0
1793
+ };
1794
+ }
1795
+ }
1796
+ function npmMatrix(value) {
1797
+ return { include: validateNpmPlan(value).environments.map(({ id, label, packageName, representative }) => ({
1798
+ id,
1799
+ label,
1800
+ packageName,
1801
+ representative
1802
+ })) };
1803
+ }
1804
+ async function writeMatrixJson(filename, value) {
1805
+ await mkdir(path.dirname(filename), { recursive: true });
1806
+ const temporary = `${filename}.${randomUUID()}.tmp`;
1807
+ try {
1808
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: `wx` });
1809
+ await rename(temporary, filename);
1810
+ } finally {
1811
+ await rm(temporary, { force: true });
1812
+ }
1813
+ }
1814
+ // src/breakage.ts
1815
+ async function breakage(options) {
1816
+ rejectCertificationOptions(options);
1817
+ const { tagPattern, baselineFile, testPattern, testCommand, test, release, baseDirname = process.cwd(), verbose = false } = options;
1818
+ if (!testPattern || !test && !testCommand) throw new Error(`required arguments were not provided: testPattern and test or testCommand.`);
1819
+ if (test !== void 0 && testCommand !== void 0) throw new Error(`Choose test or testCommand, not both.`);
1820
+ if (typeof (test ?? testCommand) !== `string` || !(test ?? testCommand).trim()) throw new Error(`test must be a nonempty shell command string.`);
1821
+ if (release !== void 0) validateRelease(release);
1096
1822
  let mark;
1097
1823
  let logMarks;
1098
1824
  if (verbose) {
@@ -1163,6 +1889,24 @@ async function breakage({ tagPattern, baselineFile, testPattern, testCommand, ce
1163
1889
  mark?.(`listed all files checked into git`);
1164
1890
  const productionTestFiles = productionFiles.filter((file) => minimatch(file, testPattern));
1165
1891
  mark?.(`filtered to public test files`);
1892
+ let releaseEvidence;
1893
+ if (release) {
1894
+ const current = await readManifest(path.join(baseDirname, `package.json`));
1895
+ const prefix = await git.revparse([`--show-prefix`]);
1896
+ const previous = parseManifest(JSON.parse(await git.show([`${releaseCommit}:${prefix}package.json`])), `baseline`);
1897
+ if (previous.name !== current.name) throw new Error(`Release baseline belongs to another package.`);
1898
+ const transition = await resolveReleaseDelta(release, {
1899
+ packageName: current.name,
1900
+ packageDirectory: path.resolve(baseDirname),
1901
+ currentVersion: current.version,
1902
+ baseline: {
1903
+ version: previous.version,
1904
+ ref: latestReleaseTag$1,
1905
+ commit: releaseCommit
1906
+ }
1907
+ });
1908
+ if (transition !== void 0) releaseEvidence = versionDelta(transition.previousVersion, transition.version, release.policy);
1909
+ }
1166
1910
  const prepared = await withTestFileState(git, baseDirname, async (state) => {
1167
1911
  if (!await state.isClean()) return {
1168
1912
  summary: `The git repository must be clean to run this command.`,
@@ -1186,50 +1930,43 @@ async function breakage({ tagPattern, baselineFile, testPattern, testCommand, ce
1186
1930
  if (prepared.summary !== void 0) return prepared;
1187
1931
  const { restore } = prepared;
1188
1932
  try {
1189
- const noBreakingChangesDetected = await new Promise((resolve, reject) => {
1190
- const result = exec(testCommand, { cwd: baseDirname }, (_, __, stderr) => {
1191
- if (result.exitCode === 0) resolve({
1192
- summary: `No breaking changes were detected.`,
1193
- gitWasClean: true,
1194
- lastReleaseFound: true,
1195
- lastReleaseTag: latestReleaseTag$1,
1196
- testsWereFound: true,
1197
- testsFound: productionTestFiles,
1198
- breakingChangesFound: false
1199
- });
1200
- else reject(stderr);
1201
- });
1202
- });
1203
- mark?.(`passed tests`);
1204
- return noBreakingChangesDetected;
1205
- } catch (thrown) {
1206
- mark?.(`failed tests`);
1207
- return await new Promise((resolve) => {
1208
- const result = exec(certifyCommand, { cwd: baseDirname }, (_, stdout, stderr) => {
1209
- const wereCertified = result.exitCode === 0;
1210
- mark?.(`${wereCertified ? `passed` : `failed`} certification`);
1211
- resolve({
1212
- summary: wereCertified ? `Breaking changes were found and certified.` : `Breaking changes were found, but not certified.`,
1213
- gitWasClean: true,
1214
- lastReleaseFound: true,
1215
- lastReleaseTag: latestReleaseTag$1,
1216
- testsWereFound: true,
1217
- testsFound: productionTestFiles,
1218
- breakingChangesFound: true,
1219
- testResult: thrown,
1220
- breakingChangesCertified: wereCertified,
1221
- certificationStdout: stdout,
1222
- certificationStderr: stderr
1223
- });
1224
- });
1225
- });
1933
+ const result = await command(test ?? testCommand, baseDirname);
1934
+ if (result.code > 1) return {
1935
+ gitWasClean: true,
1936
+ setupError: result.output,
1937
+ summary: `Public test setup failed (${result.code}).`
1938
+ };
1939
+ const common = {
1940
+ gitWasClean: true,
1941
+ lastReleaseFound: true,
1942
+ lastReleaseTag: latestReleaseTag$1,
1943
+ testsWereFound: true,
1944
+ testsFound: productionTestFiles,
1945
+ ...releaseEvidence ? { release: releaseEvidence } : {}
1946
+ };
1947
+ if (result.code === 0) return {
1948
+ ...common,
1949
+ breakingChangesFound: false,
1950
+ summary: `No breaking changes were detected.`
1951
+ };
1952
+ return {
1953
+ ...common,
1954
+ breakingChangesFound: true,
1955
+ testResult: result.output,
1956
+ summary: releaseEvidence?.breaking ? `Breaking changes are covered by release ${releaseEvidence.previousVersion} → ${releaseEvidence.version}.` : `Breaking changes require a breaking release.`
1957
+ };
1958
+ } catch (error) {
1959
+ return {
1960
+ gitWasClean: true,
1961
+ setupError: errorMessage(error),
1962
+ summary: `Public test setup failed: ${errorMessage(error)}`
1963
+ };
1226
1964
  } finally {
1227
1965
  await restore();
1228
1966
  mark?.(`restored original public tests`);
1229
1967
  logMarks?.();
1230
1968
  }
1231
1969
  }
1232
- //#endregion
1233
- export { validateConfig as a, breakagePrelude as c, npmTest as i, formatNpmReport as n, record as o, npmCheck as r, errorMessage as s, breakage as t };
1970
+ export { resolveConfig as _, npmVerify as a, breakagePrelude as b, npmCheck as c, configOptions as d, configPath as f, loadConfig as g, defineConfig as h, npmPlan as i, npmTest as l, defaultConfig as m, npmExecute as n, writeMatrixJson as o, configSchema as p, npmMatrix as r, formatNpmReport as s, breakage as t, configFields as u, verifyNpmReceipts as v, errorMessage as x, validateConfig as y };
1234
1971
 
1235
- //# sourceMappingURL=breakage-B7dJsgcu.js.map
1972
+ //# sourceMappingURL=breakage-DJiIiLzL.js.map