react-doctor 0.9.3 → 0.9.4-dev.25dbf6d

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.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="707bd0ef-8490-5120-ba82-f65ad0b490e1")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e74e6a68-20e1-59fe-8db0-62b87e8fe049")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -6666,7 +6666,7 @@ const flatMap$5 = /* @__PURE__ */ dual(2, (self, f) => isNone(self) ? none() : f
6666
6666
  * @category filtering
6667
6667
  * @since 2.0.0
6668
6668
  */
6669
- const filter = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
6669
+ const filter$1 = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
6670
6670
  //#endregion
6671
6671
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Result.js
6672
6672
  /**
@@ -18657,7 +18657,7 @@ function transformOptional(f) {
18657
18657
  */
18658
18658
  function withDefault$1(defaultValue) {
18659
18659
  return new Getter((o) => {
18660
- const filtered = filter(o, isNotUndefined);
18660
+ const filtered = filter$1(o, isNotUndefined);
18661
18661
  return isSome(filtered) ? succeed$2(filtered) : mapEager(defaultValue, some);
18662
18662
  });
18663
18663
  }
@@ -22505,6 +22505,10 @@ var JsonReportProjectEntryV3 = class extends Class("JsonReportProjectEntryV3")({
22505
22505
  scannedFileCount: optional(Number$1),
22506
22506
  elapsedMilliseconds: Number$1
22507
22507
  }) {};
22508
+ var JsonReportSkippedProject = class extends Class("JsonReportSkippedProject")({
22509
+ directory: String$1,
22510
+ reason: Literal("max-duration")
22511
+ }) {};
22508
22512
  /** Original full, diff, and staged report contract. */
22509
22513
  var JsonReportV1 = class extends Class("JsonReportV1")({
22510
22514
  schemaVersion: Literal(1),
@@ -22533,6 +22537,7 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
22533
22537
  reactDetected: optional(Boolean$1),
22534
22538
  diff: NullOr(JsonReportDiffInfo),
22535
22539
  projects: ArraySchema(JsonReportProjectEntry),
22540
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22536
22541
  diagnostics: ArraySchema(Diagnostic),
22537
22542
  summary: JsonReportSummary,
22538
22543
  elapsedMilliseconds: Number$1,
@@ -22561,6 +22566,7 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
22561
22566
  diff: NullOr(JsonReportDiffInfo),
22562
22567
  baseline: JsonReportBaseline,
22563
22568
  projects: ArraySchema(JsonReportProjectEntry),
22569
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22564
22570
  diagnostics: ArraySchema(Diagnostic),
22565
22571
  summary: JsonReportSummary,
22566
22572
  elapsedMilliseconds: Number$1,
@@ -22577,6 +22583,7 @@ var JsonReportV3 = class extends Class("JsonReportV3")({
22577
22583
  diff: NullOr(JsonReportDiffInfo),
22578
22584
  baseline: optional(JsonReportBaseline),
22579
22585
  projects: ArraySchema(JsonReportProjectEntryV3),
22586
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
22580
22587
  diagnostics: ArraySchema(JsonReportDiagnosticV3),
22581
22588
  summary: JsonReportSummary,
22582
22589
  elapsedMilliseconds: Number$1,
@@ -56559,6 +56566,47 @@ const flattenArray = (self) => transformPull$1(self, (pull) => {
56559
56566
  }));
56560
56567
  });
56561
56568
  /**
56569
+ * Filters arrays of elements emitted by a channel, applying the filter
56570
+ * to each element within the arrays and only emitting non-empty filtered arrays.
56571
+ *
56572
+ * **Example** (Filtering array output)
56573
+ *
56574
+ * ```ts
56575
+ * import { Array, Channel } from "effect"
56576
+ *
56577
+ * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty
56578
+ *
56579
+ * // Create a channel that outputs arrays of mixed data
56580
+ * const arrayChannel = Channel.fromIterable([
56581
+ * Array.make(1, 2, 3, 4, 5),
56582
+ * Array.make(6, 7, 8, 9, 10),
56583
+ * Array.make(11, 12, 13, 14, 15)
56584
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
56585
+ *
56586
+ * // Filter arrays to keep only even numbers
56587
+ * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0)
56588
+ * // Outputs: [2, 4], [6, 8, 10], [12, 14]
56589
+ * // Note: Only non-empty filtered arrays are emitted
56590
+ *
56591
+ * // Arrays that would become empty after filtering are discarded entirely
56592
+ * const oddChannel = Channel.fromIterable([
56593
+ * Array.make(1, 3, 5),
56594
+ * Array.make(2, 4),
56595
+ * Array.make(7, 9)
56596
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
56597
+ * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0)
56598
+ * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded)
56599
+ * ```
56600
+ *
56601
+ * @category filtering
56602
+ * @since 4.0.0
56603
+ */
56604
+ const filterArray = /* @__PURE__ */ dual(2, (self, predicate) => transformPull$1(self, (pull) => succeed$2(flatMap$2(pull, function loop(arr) {
56605
+ const passes = [];
56606
+ for (let i = 0; i < arr.length; i++) if (predicate(arr[i])) passes.push(arr[i]);
56607
+ return isReadonlyArrayNonEmpty(passes) ? succeed$2(passes) : flatMap$2(pull, loop);
56608
+ }))));
56609
+ /**
56562
56610
  * Filters and maps each element inside emitted non-empty arrays using a
56563
56611
  * `Filter`.
56564
56612
  *
@@ -58196,6 +58244,30 @@ const concat = /* @__PURE__ */ dual(2, (self, that) => flatten(fromArray([self,
58196
58244
  */
58197
58245
  const merge$1 = /* @__PURE__ */ dual((args) => isStream(args[0]) && isStream(args[1]), (self, that, options) => fromChannel(merge$2(toChannel(self), toChannel(that), options)));
58198
58246
  /**
58247
+ * Filters a stream to the elements that satisfy a predicate.
58248
+ *
58249
+ * **Example** (Filtering stream values)
58250
+ *
58251
+ * ```ts
58252
+ * import { Console, Effect, Stream } from "effect"
58253
+ *
58254
+ * const program = Effect.gen(function*() {
58255
+ * const stream = Stream.make(1, 2, 3, 4).pipe(
58256
+ * Stream.filter((n) => n % 2 === 0)
58257
+ * )
58258
+ * const values = yield* Stream.runCollect(stream)
58259
+ * yield* Console.log(values)
58260
+ * })
58261
+ *
58262
+ * Effect.runPromise(program)
58263
+ * // Output: [ 2, 4 ]
58264
+ * ```
58265
+ *
58266
+ * @category filtering
58267
+ * @since 2.0.0
58268
+ */
58269
+ const filter = /* @__PURE__ */ dual(2, (self, predicate) => fromChannel(filterArray(toChannel(self), predicate)));
58270
+ /**
58199
58271
  * Filters and maps stream elements in one pass using a `Filter`.
58200
58272
  *
58201
58273
  * @category filtering
@@ -65046,6 +65118,7 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
65046
65118
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
65047
65119
  const MINIFIED_SNIFF_BYTES = 65536;
65048
65120
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
65121
+ const GIT_CHECK_ATTR_MAX_BUFFER_BYTES = GIT_LS_FILES_MAX_BUFFER_BYTES;
65049
65122
  const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
65050
65123
  const IGNORED_DIRECTORIES = new Set([
65051
65124
  ".angular",
@@ -65365,13 +65438,9 @@ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
65365
65438
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
65366
65439
  const ABORT_EXIT_CODES = new Set([134, 3221226505]);
65367
65440
  const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
65368
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
65369
- const LINT_PHASE_TIMEOUT_MS = 3e5;
65370
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
65371
65441
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
65372
65442
  const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
65373
65443
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
65374
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
65375
65444
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
65376
65445
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
65377
65446
  const REACT_SERVER_DOM_PACKAGES = [
@@ -65419,7 +65488,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
65419
65488
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
65420
65489
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
65421
65490
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
65422
- const CORE_PACKAGE_VERSION = "0.9.3";
65491
+ const CORE_PACKAGE_VERSION = "0.9.4";
65423
65492
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
65424
65493
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
65425
65494
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -65432,17 +65501,20 @@ const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
65432
65501
  const isErrnoException = (error) => error instanceof Error && "code" in error;
65433
65502
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
65434
65503
  "EACCES",
65504
+ "EBUSY",
65435
65505
  "EPERM",
65436
65506
  "ENOENT",
65437
65507
  "ENOTDIR",
65438
65508
  "EINVAL",
65439
65509
  "ELOOP",
65440
- "ENAMETOOLONG"
65510
+ "ENAMETOOLONG",
65511
+ "ETIMEDOUT",
65512
+ "UNKNOWN"
65441
65513
  ]);
65442
65514
  const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
65443
65515
  const readDirectoryEntries = (directoryPath) => {
65444
65516
  try {
65445
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
65517
+ return fs.readdirSync(directoryPath, { withFileTypes: true });
65446
65518
  } catch (error) {
65447
65519
  if (isIgnorableReaddirError(error)) return [];
65448
65520
  throw error;
@@ -65450,14 +65522,14 @@ const readDirectoryEntries = (directoryPath) => {
65450
65522
  };
65451
65523
  const isFile = (filePath) => {
65452
65524
  try {
65453
- return NFS.statSync(filePath).isFile();
65525
+ return fs.statSync(filePath).isFile();
65454
65526
  } catch {
65455
65527
  return false;
65456
65528
  }
65457
65529
  };
65458
65530
  const isDirectory = (directoryPath) => {
65459
65531
  try {
65460
- return NFS.statSync(directoryPath).isDirectory();
65532
+ return fs.statSync(directoryPath).isDirectory();
65461
65533
  } catch {
65462
65534
  return false;
65463
65535
  }
@@ -65467,6 +65539,66 @@ const isPlainObject = (value) => {
65467
65539
  const prototype = Object.getPrototypeOf(value);
65468
65540
  return prototype === null || prototype === Object.prototype;
65469
65541
  };
65542
+ const LINGUIST_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
65543
+ const DISABLED_ATTRIBUTE_VALUES = new Set([
65544
+ "unspecified",
65545
+ "unset",
65546
+ "false",
65547
+ "0",
65548
+ "off",
65549
+ "no"
65550
+ ]);
65551
+ const buildInput = (relativePaths) => `${relativePaths.join("\0")}\0`;
65552
+ const parseIgnoredPaths = (output) => {
65553
+ const ignoredPaths = /* @__PURE__ */ new Set();
65554
+ const fields = output.split("\0");
65555
+ for (let fieldIndex = 0; fieldIndex + 2 < fields.length; fieldIndex += 3) {
65556
+ const filePath = fields[fieldIndex];
65557
+ const value = fields[fieldIndex + 2]?.toLowerCase();
65558
+ if (filePath && value && !DISABLED_ATTRIBUTE_VALUES.has(value)) ignoredPaths.add(filePath);
65559
+ }
65560
+ return ignoredPaths;
65561
+ };
65562
+ const collectGitLinguistIgnoredPaths = (rootDirectory, relativePaths) => {
65563
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
65564
+ const result = spawnSync("git", [
65565
+ "check-attr",
65566
+ "-z",
65567
+ "--stdin",
65568
+ ...LINGUIST_ATTRIBUTES
65569
+ ], {
65570
+ cwd: rootDirectory,
65571
+ encoding: "utf-8",
65572
+ input: buildInput(relativePaths),
65573
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES
65574
+ });
65575
+ if (result.error || result.status !== 0) return /* @__PURE__ */ new Set();
65576
+ return parseIgnoredPaths(result.stdout);
65577
+ };
65578
+ const collectGitLinguistIgnoredPathsCooperative = (rootDirectory, relativePaths, signal) => {
65579
+ if (relativePaths.length === 0) return Promise.resolve(/* @__PURE__ */ new Set());
65580
+ return new Promise((resolve, reject) => {
65581
+ signal?.throwIfAborted();
65582
+ execFile("git", [
65583
+ "check-attr",
65584
+ "-z",
65585
+ "--stdin",
65586
+ ...LINGUIST_ATTRIBUTES
65587
+ ], {
65588
+ cwd: rootDirectory,
65589
+ encoding: "utf-8",
65590
+ killSignal: "SIGKILL",
65591
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES,
65592
+ signal
65593
+ }, (error, stdout) => {
65594
+ if (signal?.aborted) {
65595
+ reject(signal.reason);
65596
+ return;
65597
+ }
65598
+ resolve(error ? /* @__PURE__ */ new Set() : parseIgnoredPaths(stdout));
65599
+ }).stdin?.end(buildInput(relativePaths));
65600
+ });
65601
+ };
65470
65602
  const lastSourceMappingUrl = (fileContent) => {
65471
65603
  const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
65472
65604
  return lastReference ? lastReference[1] : null;
@@ -65568,6 +65700,9 @@ function* walkSourceTreeFiles(rootDirectory) {
65568
65700
  }
65569
65701
  }
65570
65702
  }
65703
+ const yieldToEventLoop = () => new Promise((resolve) => {
65704
+ setImmediate(resolve);
65705
+ });
65571
65706
  const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
65572
65707
  const entries = [];
65573
65708
  for (const relativePath of relativePaths) {
@@ -65581,23 +65716,55 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
65581
65716
  }
65582
65717
  return entries;
65583
65718
  };
65584
- const gitLsFiles = (rootDirectory, flags) => {
65719
+ const collectSizedSourceFilesCooperative = async (rootDirectory, relativePaths, signal) => {
65720
+ const entries = [];
65721
+ let sliceStartedAt = performance.now();
65722
+ for (const relativePath of relativePaths) {
65723
+ signal?.throwIfAborted();
65724
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
65725
+ const sizeBytes = statSourceFileSize(absolutePath);
65726
+ if (!isLargeMinifiedFile(absolutePath, sizeBytes)) entries.push({
65727
+ path: relativePath,
65728
+ sizeBytes: sizeBytes ?? 0
65729
+ });
65730
+ if (performance.now() - sliceStartedAt >= 12) {
65731
+ await yieldToEventLoop();
65732
+ sliceStartedAt = performance.now();
65733
+ }
65734
+ }
65735
+ return entries;
65736
+ };
65737
+ const parseGitSourceFilePaths = (output) => {
65738
+ const trackedPaths = /* @__PURE__ */ new Set();
65739
+ const untrackedPaths = [];
65740
+ for (const entry of output.split("\0")) {
65741
+ if (entry.length === 0) continue;
65742
+ const trackedPath = /^[0-7]{6} [0-9a-f]+ [0-3]\t([\s\S]+)$/.exec(entry)?.[1];
65743
+ if (trackedPath === void 0) untrackedPaths.push(entry);
65744
+ else trackedPaths.add(trackedPath);
65745
+ }
65746
+ return {
65747
+ trackedPaths: [...trackedPaths],
65748
+ untrackedPaths
65749
+ };
65750
+ };
65751
+ const listGitSourceFilePaths = (rootDirectory) => {
65585
65752
  const result = spawnSync("git", [
65586
65753
  "ls-files",
65587
65754
  "-z",
65588
- ...flags
65755
+ "--stage",
65756
+ "--others",
65757
+ "--exclude-standard"
65589
65758
  ], {
65590
65759
  cwd: rootDirectory,
65591
65760
  encoding: "utf-8",
65592
65761
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
65593
65762
  });
65594
65763
  if (result.error || result.status !== 0) return null;
65595
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
65764
+ return parseGitSourceFilePaths(result.stdout);
65596
65765
  };
65597
- const listSourceFilesViaGit = (rootDirectory) => {
65598
- const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
65599
- const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
65600
- if (trackedPaths === null || untrackedPaths === null) return null;
65766
+ const collectGitCandidateSourceFilePaths = (rootDirectory, paths) => {
65767
+ const { trackedPaths, untrackedPaths } = paths;
65601
65768
  const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
65602
65769
  trackedPaths: new Set(trackedPaths),
65603
65770
  untrackedPaths,
@@ -65605,12 +65772,65 @@ const listSourceFilesViaGit = (rootDirectory) => {
65605
65772
  });
65606
65773
  return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
65607
65774
  };
65775
+ const filterGitSourceFilePaths = (rootDirectory, paths) => {
65776
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
65777
+ const linguistIgnoredPaths = collectGitLinguistIgnoredPaths(rootDirectory, candidatePaths);
65778
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
65779
+ };
65780
+ const listSourceFilesViaGit = (rootDirectory) => {
65781
+ const paths = listGitSourceFilePaths(rootDirectory);
65782
+ return paths === null ? null : filterGitSourceFilePaths(rootDirectory, paths);
65783
+ };
65784
+ const listSourceFilesViaGitCooperative = async (rootDirectory, signal) => {
65785
+ const paths = await new Promise((resolve, reject) => {
65786
+ signal?.throwIfAborted();
65787
+ execFile("git", [
65788
+ "ls-files",
65789
+ "-z",
65790
+ "--stage",
65791
+ "--others",
65792
+ "--exclude-standard"
65793
+ ], {
65794
+ cwd: rootDirectory,
65795
+ encoding: "utf-8",
65796
+ killSignal: "SIGKILL",
65797
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
65798
+ signal
65799
+ }, (error, stdout) => {
65800
+ if (signal?.aborted) {
65801
+ reject(signal.reason);
65802
+ return;
65803
+ }
65804
+ resolve(error ? null : parseGitSourceFilePaths(stdout));
65805
+ });
65806
+ });
65807
+ if (paths === null) return null;
65808
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
65809
+ const linguistIgnoredPaths = await collectGitLinguistIgnoredPathsCooperative(rootDirectory, candidatePaths, signal);
65810
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
65811
+ };
65608
65812
  const listSourceFilesViaFilesystem = (rootDirectory) => {
65609
65813
  const filePaths = [];
65610
65814
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
65611
65815
  return filePaths;
65612
65816
  };
65613
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
65817
+ const listSourceFilesViaFilesystemCooperative = async (rootDirectory, signal) => {
65818
+ const filePaths = [];
65819
+ let sliceStartedAt = performance.now();
65820
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
65821
+ signal?.throwIfAborted();
65822
+ if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
65823
+ if (performance.now() - sliceStartedAt >= 12) {
65824
+ await yieldToEventLoop();
65825
+ sliceStartedAt = performance.now();
65826
+ }
65827
+ }
65828
+ return filePaths;
65829
+ };
65830
+ const listSourceFilePaths = (rootDirectory) => (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort();
65831
+ const listSourceFilePathsCooperative = async (rootDirectory, signal) => (await listSourceFilesViaGitCooperative(rootDirectory, signal) ?? await listSourceFilesViaFilesystemCooperative(rootDirectory, signal)).sort();
65832
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilePaths(rootDirectory));
65833
+ const listSourceFilesCooperative = async (rootDirectory, signal) => (await collectSizedSourceFilesCooperative(rootDirectory, await listSourceFilePathsCooperative(rootDirectory, signal), signal)).map((entry) => entry.path);
65614
65834
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
65615
65835
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
65616
65836
  const UTF8_BOM_CHARACTER = "";
@@ -65733,7 +65953,12 @@ const mergeCompilerOptions = (inherited, current) => {
65733
65953
  };
65734
65954
  };
65735
65955
  const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
65736
- const realPath = NFS.realpathSync.native(tsConfigPath);
65956
+ let realPath;
65957
+ try {
65958
+ realPath = NFS.realpathSync.native(tsConfigPath);
65959
+ } catch {
65960
+ return null;
65961
+ }
65737
65962
  if (visitedPaths.has(realPath)) return null;
65738
65963
  const tsConfig = readTsConfig(realPath);
65739
65964
  if (!tsConfig) return null;
@@ -66261,27 +66486,30 @@ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedEx
66261
66486
  }
66262
66487
  return null;
66263
66488
  };
66264
- const configExpressionMayDefineProperty = (expression, propertyName, analysis) => {
66489
+ const configExpressionMayDefineProperty = (expression, propertyName, analysis, visitedExpressions = /* @__PURE__ */ new Set()) => {
66265
66490
  let resolvedExpression = expression;
66266
66491
  while (ts.isParenthesizedExpression(resolvedExpression) || ts.isAsExpression(resolvedExpression) || ts.isTypeAssertionExpression(resolvedExpression) || ts.isSatisfiesExpression(resolvedExpression) || ts.isNonNullExpression(resolvedExpression)) resolvedExpression = resolvedExpression.expression;
66492
+ if (visitedExpressions.has(resolvedExpression)) return true;
66493
+ const nextVisitedExpressions = new Set(visitedExpressions);
66494
+ nextVisitedExpressions.add(resolvedExpression);
66267
66495
  if (ts.isIdentifier(resolvedExpression)) {
66268
66496
  if (analysis.localBindings.has(resolvedExpression.text)) {
66269
66497
  const localReference = analysis.localBindings.get(resolvedExpression.text);
66270
- return localReference?.expression ? configExpressionMayDefineProperty(localReference.expression, propertyName, localReference.analysis) : true;
66498
+ return localReference?.expression ? configExpressionMayDefineProperty(localReference.expression, propertyName, localReference.analysis, nextVisitedExpressions) : true;
66271
66499
  }
66272
66500
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
66273
- if (scopedBinding.wasFound) return scopedBinding.initializer ? configExpressionMayDefineProperty(scopedBinding.initializer, propertyName, analysis) : true;
66501
+ if (scopedBinding.wasFound) return scopedBinding.initializer ? configExpressionMayDefineProperty(scopedBinding.initializer, propertyName, analysis, nextVisitedExpressions) : true;
66274
66502
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, resolvedExpression.text);
66275
- return topLevelBinding && ts.isExpression(topLevelBinding) ? configExpressionMayDefineProperty(topLevelBinding, propertyName, analysis) : true;
66503
+ return topLevelBinding && ts.isExpression(topLevelBinding) ? configExpressionMayDefineProperty(topLevelBinding, propertyName, analysis, nextVisitedExpressions) : true;
66276
66504
  }
66277
66505
  if (ts.isConditionalExpression(resolvedExpression)) {
66278
- if (isStaticallyTruthyConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis);
66279
- if (isStaticallyDisabledConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis);
66280
- return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis) || configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis);
66506
+ if (isStaticallyTruthyConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis, nextVisitedExpressions);
66507
+ if (isStaticallyDisabledConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis, nextVisitedExpressions);
66508
+ return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis, nextVisitedExpressions) || configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis, nextVisitedExpressions);
66281
66509
  }
66282
66510
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return true;
66283
66511
  return resolvedExpression.properties.some((property) => {
66284
- if (ts.isSpreadAssignment(property)) return configExpressionMayDefineProperty(property.expression, propertyName, analysis);
66512
+ if (ts.isSpreadAssignment(property)) return configExpressionMayDefineProperty(property.expression, propertyName, analysis, nextVisitedExpressions);
66285
66513
  if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
66286
66514
  const staticPropertyName = getStaticPropertyName$2(property.name);
66287
66515
  return staticPropertyName === null || staticPropertyName === propertyName;
@@ -66357,7 +66585,13 @@ const analyzeConfigModuleExport = (filePath, exportName, allowCompilerTransform,
66357
66585
  if (visitedModules.has(moduleVisitKey)) return false;
66358
66586
  const nextVisitedModules = new Set(visitedModules);
66359
66587
  nextVisitedModules.add(moduleVisitKey);
66360
- return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, NFS.readFileSync(filePath, "utf-8")), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList, argumentAnalysis);
66588
+ let sourceText;
66589
+ try {
66590
+ sourceText = NFS.readFileSync(filePath, "utf-8");
66591
+ } catch {
66592
+ return false;
66593
+ }
66594
+ return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, sourceText), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList, argumentAnalysis);
66361
66595
  };
66362
66596
  const analyzeConfigSourceFileExport = (sourceFile, filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = [], argumentAnalysis) => {
66363
66597
  const analysis = {
@@ -67185,13 +67419,7 @@ const hasReactDependency = (packageJson) => {
67185
67419
  };
67186
67420
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
67187
67421
  };
67188
- const getPreactVersion = (packageJson) => {
67189
- return {
67190
- ...packageJson.peerDependencies,
67191
- ...packageJson.dependencies,
67192
- ...packageJson.devDependencies
67193
- }.preact ?? null;
67194
- };
67422
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
67195
67423
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
67196
67424
  if (version === null || !isCatalogReference(version)) return version;
67197
67425
  const catalogName = extractCatalogName(version);
@@ -67933,7 +68161,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
67933
68161
  if (!queries) return void 0;
67934
68162
  let requirement;
67935
68163
  let hasUnsupportedTarget = false;
67936
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
68164
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68165
+ let browserTargets = [];
68166
+ try {
68167
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
68168
+ browserTargets = (0, import_browserslist.default)(queries);
68169
+ } finally {
68170
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
68171
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
68172
+ }
68173
+ for (const browserTarget of browserTargets) {
67937
68174
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
67938
68175
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
67939
68176
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -68039,9 +68276,9 @@ const cachedProjectInfos = /* @__PURE__ */ new Map();
68039
68276
  * framework-agnostic rules. Throws only when the directory has nothing
68040
68277
  * to scan (no enclosing project and no source files of its own).
68041
68278
  */
68042
- const discoverProjectWithoutPackageJson = (directory) => {
68043
- const sourceFileCount = countSourceFiles(directory);
68044
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68279
+ const discoverProjectWithoutPackageJson = (directory, options) => {
68280
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68281
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
68045
68282
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
68046
68283
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
68047
68284
  if (enclosingProject !== null) return {
@@ -68107,12 +68344,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
68107
68344
  sourceFileCount
68108
68345
  };
68109
68346
  };
68110
- const discoverProject = (directory) => {
68347
+ const discoverProject = (directory, options = {}) => {
68111
68348
  const cached = cachedProjectInfos.get(directory);
68112
- if (cached !== void 0) return cached;
68349
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
68350
+ ...cached,
68351
+ sourceFileCount: options.sourceFileCount
68352
+ };
68113
68353
  const packageJsonPath = Path.join(directory, "package.json");
68114
68354
  if (!isFile(packageJsonPath)) {
68115
- const synthesized = discoverProjectWithoutPackageJson(directory);
68355
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
68116
68356
  cachedProjectInfos.set(directory, synthesized);
68117
68357
  return synthesized;
68118
68358
  }
@@ -68174,8 +68414,8 @@ const discoverProject = (directory) => {
68174
68414
  const tailwindVersion = tailwindcss.version;
68175
68415
  const zodVersion = zod.version;
68176
68416
  const projectName = packageJson.name ?? Path.basename(directory);
68177
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
68178
- const sourceFileCount = countSourceFiles(directory);
68417
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
68418
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
68179
68419
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
68180
68420
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
68181
68421
  rootDirectory: directory,
@@ -68304,6 +68544,155 @@ const discoverProject = (directory) => {
68304
68544
  * single source file has nothing to analyze.
68305
68545
  */
68306
68546
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
68547
+ const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
68548
+ "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
68549
+ "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
68550
+ "effect/no-derived-state": "react-doctor/no-derived-state",
68551
+ "effect/no-event-handler": "react-doctor/no-event-handler",
68552
+ "effect/no-initialize-state": "react-doctor/no-initialize-state",
68553
+ "effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
68554
+ "effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
68555
+ "effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
68556
+ "jsx-a11y/alt-text": "react-doctor/alt-text",
68557
+ "jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
68558
+ "jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
68559
+ "jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
68560
+ "jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
68561
+ "jsx-a11y/aria-props": "react-doctor/aria-props",
68562
+ "jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
68563
+ "jsx-a11y/aria-role": "react-doctor/aria-role",
68564
+ "jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
68565
+ "jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
68566
+ "jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
68567
+ "jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
68568
+ "jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
68569
+ "jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
68570
+ "jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
68571
+ "jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
68572
+ "jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
68573
+ "jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
68574
+ "jsx-a11y/lang": "react-doctor/lang",
68575
+ "jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
68576
+ "jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
68577
+ "jsx-a11y/no-access-key": "react-doctor/no-access-key",
68578
+ "jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
68579
+ "jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
68580
+ "jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
68581
+ "jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
68582
+ "jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
68583
+ "jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
68584
+ "jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
68585
+ "jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
68586
+ "jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
68587
+ "jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
68588
+ "jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
68589
+ "jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
68590
+ "jsx-a11y/scope": "react-doctor/scope",
68591
+ "jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
68592
+ "react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
68593
+ "react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
68594
+ "react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68595
+ "react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68596
+ "react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68597
+ "react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68598
+ "react-refresh/only-export-components": "react-doctor/only-export-components",
68599
+ "react/button-has-type": "react-doctor/button-has-type",
68600
+ "react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
68601
+ "react/display-name": "react-doctor/display-name",
68602
+ "react/exhaustive-deps": "react-doctor/exhaustive-deps",
68603
+ "react/forbid-component-props": "react-doctor/forbid-component-props",
68604
+ "react/forbid-dom-props": "react-doctor/forbid-dom-props",
68605
+ "react/forbid-elements": "react-doctor/forbid-elements",
68606
+ "react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
68607
+ "react/hook-use-state": "react-doctor/hook-use-state",
68608
+ "react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
68609
+ "react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
68610
+ "react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
68611
+ "react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
68612
+ "react/jsx-fragments": "react-doctor/jsx-fragments",
68613
+ "react/jsx-handler-names": "react-doctor/jsx-handler-names",
68614
+ "react/jsx-key": "react-doctor/jsx-key",
68615
+ "react/jsx-max-depth": "react-doctor/jsx-max-depth",
68616
+ "react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
68617
+ "react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
68618
+ "react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
68619
+ "react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68620
+ "react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68621
+ "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68622
+ "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68623
+ "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
68624
+ "react/jsx-no-undef": "react-doctor/jsx-no-undef",
68625
+ "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
68626
+ "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
68627
+ "react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
68628
+ "react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
68629
+ "react/no-array-index-key": "react-doctor/no-array-index-key",
68630
+ "react/no-children-prop": "react-doctor/no-children-prop",
68631
+ "react/no-clone-element": "react-doctor/no-clone-element",
68632
+ "react/no-danger": "react-doctor/no-danger",
68633
+ "react/no-danger-with-children": "react-doctor/no-danger-with-children",
68634
+ "react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
68635
+ "react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
68636
+ "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
68637
+ "react/no-find-dom-node": "react-doctor/no-find-dom-node",
68638
+ "react/no-is-mounted": "react-doctor/no-is-mounted",
68639
+ "react/no-multi-comp": "react-doctor/no-multi-comp",
68640
+ "react/no-namespace": "react-doctor/no-namespace",
68641
+ "react/no-react-children": "react-doctor/no-react-children",
68642
+ "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
68643
+ "react/no-render-return-value": "react-doctor/no-render-return-value",
68644
+ "react/no-set-state": "react-doctor/no-set-state",
68645
+ "react/no-string-refs": "react-doctor/no-string-refs",
68646
+ "react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
68647
+ "react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
68648
+ "react/no-unknown-property": "react-doctor/no-unknown-property",
68649
+ "react/no-unsafe": "react-doctor/no-unsafe",
68650
+ "react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
68651
+ "react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
68652
+ "react/only-export-components": "react-doctor/only-export-components",
68653
+ "react/prefer-es6-class": "react-doctor/prefer-es6-class",
68654
+ "react/prefer-function-component": "react-doctor/prefer-function-component",
68655
+ "react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
68656
+ "react/require-render-return": "react-doctor/require-render-return",
68657
+ "react/rules-of-hooks": "react-doctor/rules-of-hooks",
68658
+ "react/self-closing-comp": "react-doctor/self-closing-comp",
68659
+ "react/state-in-constructor": "react-doctor/state-in-constructor",
68660
+ "react/style-prop-object": "react-doctor/style-prop-object",
68661
+ "react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
68662
+ };
68663
+ const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
68664
+ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
68665
+ const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
68666
+ aliases.push(legacyRuleKey);
68667
+ NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
68668
+ }
68669
+ const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
68670
+ const REACT_DOCTOR_RULE_KEY_PREFIX = "react-doctor/";
68671
+ const canonicalizeRuleKey = (ruleKey) => {
68672
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
68673
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
68674
+ };
68675
+ const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
68676
+ /**
68677
+ * Canonicalizes a rule key as users write it in config: a legacy alias
68678
+ * (`react/jsx-key`) maps to its native key, and a bare short id (`no-eval`)
68679
+ * qualifies as `react-doctor/<id>` — mirroring `isSameRuleKey`'s matching —
68680
+ * so telemetry groups every spelling of one rule under one key.
68681
+ */
68682
+ const canonicalizeUserRuleKey = (ruleKey) => {
68683
+ const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68684
+ return nativeRuleKey.includes("/") ? nativeRuleKey : `${REACT_DOCTOR_RULE_KEY_PREFIX}${nativeRuleKey}`;
68685
+ };
68686
+ const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
68687
+ const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
68688
+ const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
68689
+ if (canonicalCandidate === canonicalTarget) return true;
68690
+ return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
68691
+ };
68692
+ const getEquivalentRuleKeys = (ruleKey) => {
68693
+ const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68694
+ return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
68695
+ };
68307
68696
  const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
68308
68697
  var InvalidGlobPatternError = class extends Error {
68309
68698
  pattern;
@@ -68357,6 +68746,11 @@ const warnConfigIssue = (message) => {
68357
68746
  };
68358
68747
  const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
68359
68748
  const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
68749
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
68750
+ if (ruleIds.size === 0) return true;
68751
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
68752
+ return false;
68753
+ };
68360
68754
  const validateOverrideEntry = (entry, index) => {
68361
68755
  if (!isPlainObject(entry)) {
68362
68756
  warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
@@ -68397,7 +68791,7 @@ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) =>
68397
68791
  if (overrides.length === 0) return false;
68398
68792
  const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
68399
68793
  const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
68400
- return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
68794
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
68401
68795
  };
68402
68796
  const SEVERITY_FOR_OVERRIDE = {
68403
68797
  error: "error",
@@ -68432,155 +68826,6 @@ const buildRuleSeverityControls = (config) => {
68432
68826
  const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
68433
68827
  const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
68434
68828
  const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
68435
- const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
68436
- "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
68437
- "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
68438
- "effect/no-derived-state": "react-doctor/no-derived-state",
68439
- "effect/no-event-handler": "react-doctor/no-event-handler",
68440
- "effect/no-initialize-state": "react-doctor/no-initialize-state",
68441
- "effect/no-pass-data-to-parent": "react-doctor/no-pass-data-to-parent",
68442
- "effect/no-pass-live-state-to-parent": "react-doctor/no-pass-live-state-to-parent",
68443
- "effect/no-reset-all-state-on-prop-change": "react-doctor/no-reset-all-state-on-prop-change",
68444
- "jsx-a11y/alt-text": "react-doctor/alt-text",
68445
- "jsx-a11y/anchor-ambiguous-text": "react-doctor/anchor-ambiguous-text",
68446
- "jsx-a11y/anchor-has-content": "react-doctor/anchor-has-content",
68447
- "jsx-a11y/anchor-is-valid": "react-doctor/anchor-is-valid",
68448
- "jsx-a11y/aria-activedescendant-has-tabindex": "react-doctor/aria-activedescendant-has-tabindex",
68449
- "jsx-a11y/aria-props": "react-doctor/aria-props",
68450
- "jsx-a11y/aria-proptypes": "react-doctor/aria-proptypes",
68451
- "jsx-a11y/aria-role": "react-doctor/aria-role",
68452
- "jsx-a11y/aria-unsupported-elements": "react-doctor/aria-unsupported-elements",
68453
- "jsx-a11y/autocomplete-valid": "react-doctor/autocomplete-valid",
68454
- "jsx-a11y/click-events-have-key-events": "react-doctor/click-events-have-key-events",
68455
- "jsx-a11y/control-has-associated-label": "react-doctor/control-has-associated-label",
68456
- "jsx-a11y/heading-has-content": "react-doctor/heading-has-content",
68457
- "jsx-a11y/html-has-lang": "react-doctor/html-has-lang",
68458
- "jsx-a11y/iframe-has-title": "react-doctor/iframe-has-title",
68459
- "jsx-a11y/img-redundant-alt": "react-doctor/img-redundant-alt",
68460
- "jsx-a11y/interactive-supports-focus": "react-doctor/interactive-supports-focus",
68461
- "jsx-a11y/label-has-associated-control": "react-doctor/label-has-associated-control",
68462
- "jsx-a11y/lang": "react-doctor/lang",
68463
- "jsx-a11y/media-has-caption": "react-doctor/media-has-caption",
68464
- "jsx-a11y/mouse-events-have-key-events": "react-doctor/mouse-events-have-key-events",
68465
- "jsx-a11y/no-access-key": "react-doctor/no-access-key",
68466
- "jsx-a11y/no-aria-hidden-on-focusable": "react-doctor/no-aria-hidden-on-focusable",
68467
- "jsx-a11y/no-autofocus": "react-doctor/no-autofocus",
68468
- "jsx-a11y/no-distracting-elements": "react-doctor/no-distracting-elements",
68469
- "jsx-a11y/no-interactive-element-to-noninteractive-role": "react-doctor/no-interactive-element-to-noninteractive-role",
68470
- "jsx-a11y/no-noninteractive-element-interactions": "react-doctor/no-noninteractive-element-interactions",
68471
- "jsx-a11y/no-noninteractive-element-to-interactive-role": "react-doctor/no-noninteractive-element-to-interactive-role",
68472
- "jsx-a11y/no-noninteractive-tabindex": "react-doctor/no-noninteractive-tabindex",
68473
- "jsx-a11y/no-redundant-roles": "react-doctor/no-redundant-roles",
68474
- "jsx-a11y/no-static-element-interactions": "react-doctor/no-static-element-interactions",
68475
- "jsx-a11y/prefer-tag-over-role": "react-doctor/prefer-tag-over-role",
68476
- "jsx-a11y/role-has-required-aria-props": "react-doctor/role-has-required-aria-props",
68477
- "jsx-a11y/role-supports-aria-props": "react-doctor/role-supports-aria-props",
68478
- "jsx-a11y/scope": "react-doctor/scope",
68479
- "jsx-a11y/tabindex-no-positive": "react-doctor/tabindex-no-positive",
68480
- "react-hooks/exhaustive-deps": "react-doctor/exhaustive-deps",
68481
- "react-hooks/rules-of-hooks": "react-doctor/rules-of-hooks",
68482
- "react-perf/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68483
- "react-perf/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68484
- "react-perf/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68485
- "react-perf/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68486
- "react-refresh/only-export-components": "react-doctor/only-export-components",
68487
- "react/button-has-type": "react-doctor/button-has-type",
68488
- "react/checked-requires-onchange-or-readonly": "react-doctor/checked-requires-onchange-or-readonly",
68489
- "react/display-name": "react-doctor/display-name",
68490
- "react/exhaustive-deps": "react-doctor/exhaustive-deps",
68491
- "react/forbid-component-props": "react-doctor/forbid-component-props",
68492
- "react/forbid-dom-props": "react-doctor/forbid-dom-props",
68493
- "react/forbid-elements": "react-doctor/forbid-elements",
68494
- "react/forward-ref-uses-ref": "react-doctor/forward-ref-uses-ref",
68495
- "react/hook-use-state": "react-doctor/hook-use-state",
68496
- "react/iframe-missing-sandbox": "react-doctor/iframe-missing-sandbox",
68497
- "react/jsx-boolean-value": "react-doctor/jsx-boolean-value",
68498
- "react/jsx-curly-brace-presence": "react-doctor/jsx-curly-brace-presence",
68499
- "react/jsx-filename-extension": "react-doctor/jsx-filename-extension",
68500
- "react/jsx-fragments": "react-doctor/jsx-fragments",
68501
- "react/jsx-handler-names": "react-doctor/jsx-handler-names",
68502
- "react/jsx-key": "react-doctor/jsx-key",
68503
- "react/jsx-max-depth": "react-doctor/jsx-max-depth",
68504
- "react/jsx-no-comment-textnodes": "react-doctor/jsx-no-comment-textnodes",
68505
- "react/jsx-no-constructed-context-values": "react-doctor/jsx-no-constructed-context-values",
68506
- "react/jsx-no-duplicate-props": "react-doctor/jsx-no-duplicate-props",
68507
- "react/jsx-no-jsx-as-prop": "react-doctor/jsx-no-jsx-as-prop",
68508
- "react/jsx-no-new-array-as-prop": "react-doctor/jsx-no-new-array-as-prop",
68509
- "react/jsx-no-new-function-as-prop": "react-doctor/jsx-no-new-function-as-prop",
68510
- "react/jsx-no-new-object-as-prop": "react-doctor/jsx-no-new-object-as-prop",
68511
- "react/jsx-no-script-url": "react-doctor/jsx-no-script-url",
68512
- "react/jsx-no-undef": "react-doctor/jsx-no-undef",
68513
- "react/jsx-no-useless-fragment": "react-doctor/jsx-no-useless-fragment",
68514
- "react/jsx-pascal-case": "react-doctor/jsx-pascal-case",
68515
- "react/jsx-props-no-spread-multi": "react-doctor/jsx-props-no-spread-multi",
68516
- "react/jsx-props-no-spreading": "react-doctor/jsx-props-no-spreading",
68517
- "react/no-array-index-key": "react-doctor/no-array-index-key",
68518
- "react/no-children-prop": "react-doctor/no-children-prop",
68519
- "react/no-clone-element": "react-doctor/no-clone-element",
68520
- "react/no-danger": "react-doctor/no-danger",
68521
- "react/no-danger-with-children": "react-doctor/no-danger-with-children",
68522
- "react/no-did-mount-set-state": "react-doctor/no-did-mount-set-state",
68523
- "react/no-did-update-set-state": "react-doctor/no-did-update-set-state",
68524
- "react/no-direct-mutation-state": "react-doctor/no-direct-mutation-state",
68525
- "react/no-find-dom-node": "react-doctor/no-find-dom-node",
68526
- "react/no-is-mounted": "react-doctor/no-is-mounted",
68527
- "react/no-multi-comp": "react-doctor/no-multi-comp",
68528
- "react/no-namespace": "react-doctor/no-namespace",
68529
- "react/no-react-children": "react-doctor/no-react-children",
68530
- "react/no-redundant-should-component-update": "react-doctor/no-redundant-should-component-update",
68531
- "react/no-render-return-value": "react-doctor/no-render-return-value",
68532
- "react/no-set-state": "react-doctor/no-set-state",
68533
- "react/no-string-refs": "react-doctor/no-string-refs",
68534
- "react/no-this-in-sfc": "react-doctor/no-this-in-sfc",
68535
- "react/no-unescaped-entities": "react-doctor/no-unescaped-entities",
68536
- "react/no-unknown-property": "react-doctor/no-unknown-property",
68537
- "react/no-unsafe": "react-doctor/no-unsafe",
68538
- "react/no-unstable-nested-components": "react-doctor/no-unstable-nested-components",
68539
- "react/no-will-update-set-state": "react-doctor/no-will-update-set-state",
68540
- "react/only-export-components": "react-doctor/only-export-components",
68541
- "react/prefer-es6-class": "react-doctor/prefer-es6-class",
68542
- "react/prefer-function-component": "react-doctor/prefer-function-component",
68543
- "react/react-in-jsx-scope": "react-doctor/react-in-jsx-scope",
68544
- "react/require-render-return": "react-doctor/require-render-return",
68545
- "react/rules-of-hooks": "react-doctor/rules-of-hooks",
68546
- "react/self-closing-comp": "react-doctor/self-closing-comp",
68547
- "react/state-in-constructor": "react-doctor/state-in-constructor",
68548
- "react/style-prop-object": "react-doctor/style-prop-object",
68549
- "react/void-dom-elements-no-children": "react-doctor/void-dom-elements-no-children"
68550
- };
68551
- const NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS = /* @__PURE__ */ new Map();
68552
- for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY)) {
68553
- const aliases = NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(nativeRuleKey) ?? [];
68554
- aliases.push(legacyRuleKey);
68555
- NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
68556
- }
68557
- const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
68558
- const REACT_DOCTOR_RULE_KEY_PREFIX = "react-doctor/";
68559
- const canonicalizeRuleKey = (ruleKey) => {
68560
- const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
68561
- return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
68562
- };
68563
- const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
68564
- /**
68565
- * Canonicalizes a rule key as users write it in config: a legacy alias
68566
- * (`react/jsx-key`) maps to its native key, and a bare short id (`no-eval`)
68567
- * qualifies as `react-doctor/<id>` — mirroring `isSameRuleKey`'s matching —
68568
- * so telemetry groups every spelling of one rule under one key.
68569
- */
68570
- const canonicalizeUserRuleKey = (ruleKey) => {
68571
- const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68572
- return nativeRuleKey.includes("/") ? nativeRuleKey : `${REACT_DOCTOR_RULE_KEY_PREFIX}${nativeRuleKey}`;
68573
- };
68574
- const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
68575
- const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
68576
- const canonicalTarget = canonicalizeRuleKey(targetRuleKey);
68577
- if (canonicalCandidate === canonicalTarget) return true;
68578
- return isReactDoctorShortIdOf(canonicalCandidate, canonicalTarget) || isReactDoctorShortIdOf(canonicalTarget, canonicalCandidate);
68579
- };
68580
- const getEquivalentRuleKeys = (ruleKey) => {
68581
- const nativeRuleKey = canonicalizeRuleKey(ruleKey);
68582
- return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
68583
- };
68584
68829
  const stripDescriptionTail = (ruleList) => {
68585
68830
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
68586
68831
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -69227,12 +69472,8 @@ const buildDiagnosticPipeline = (input) => {
69227
69472
  if (rule.tags.includes("migration-hint")) return false;
69228
69473
  return getFileContext(diagnostic.filePath) !== "production";
69229
69474
  };
69230
- const isRuleIgnored = (ruleIdentifier) => {
69231
- for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
69232
- return false;
69233
- };
69234
- const isAppOnlyRule = (ruleIdentifier) => {
69235
- for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
69475
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
69476
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
69236
69477
  return false;
69237
69478
  };
69238
69479
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -69274,12 +69515,13 @@ const buildDiagnosticPipeline = (input) => {
69274
69515
  if (explicitSeverityOverride === "off") return suppress(current, "config");
69275
69516
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
69276
69517
  }
69518
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
69277
69519
  if (explicitRuleOverride === void 0) {
69278
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
69520
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
69279
69521
  }
69280
69522
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
69281
69523
  if (userConfig) {
69282
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
69524
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
69283
69525
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
69284
69526
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
69285
69527
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -69288,7 +69530,6 @@ const buildDiagnosticPipeline = (input) => {
69288
69530
  if (respectInlineDisables && current.line > 0) {
69289
69531
  const lines = getFileLines(current.filePath);
69290
69532
  if (lines) {
69291
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
69292
69533
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
69293
69534
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
69294
69535
  if (evaluation.nearMissHint) current = {
@@ -69564,7 +69805,8 @@ const readSystemFacts$1 = () => ({
69564
69805
  /**
69565
69806
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
69566
69807
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
69567
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69808
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
69809
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
69568
69810
  *
69569
69811
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
69570
69812
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -69580,7 +69822,7 @@ const readSystemFacts$1 = () => ({
69580
69822
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
69581
69823
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
69582
69824
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
69583
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
69825
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
69584
69826
  };
69585
69827
  const resolveLintBatchOrdering = () => {
69586
69828
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -69593,24 +69835,22 @@ const resolveLintBatchOrdering = () => {
69593
69835
  */
69594
69836
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
69595
69837
  /**
69596
- * Effect-side cap on the lint phase. The env var lets CI / eval runners
69597
- * raise the phase budget for slow large repos without recompiling.
69838
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
69839
+ * configured through the env var or a test layer.
69598
69840
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
69599
69841
  */
69600
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
69842
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
69601
69843
  /**
69602
- * Effect-side cap on the dead-code phase, sitting above the in-worker
69603
- * timeout as a runtime-independent backstop. The env var raises it for
69604
- * type-heavy projects; tests override via
69844
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
69845
+ * configured through the env var or a test layer. Tests override via
69605
69846
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
69606
69847
  */
69607
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
69848
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
69608
69849
  /**
69609
- * Overall scan deadline backstop, bounding everything the per-phase
69610
- * timeouts don't (wedged git / IO). The env var raises it for very
69611
- * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
69850
+ * Optional overall scan deadline. Unbounded unless explicitly configured
69851
+ * through the env var or a test layer.
69612
69852
  */
69613
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
69853
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
69614
69854
  /**
69615
69855
  * Wall-clock budget for the supply-chain check when it runs on a background
69616
69856
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -69683,14 +69923,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
69683
69923
  /**
69684
69924
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
69685
69925
  * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
69686
- * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
69687
- * an even share of files AND bytes, so no 100-file chunk is a straggler while
69926
+ * batch count as greedy chunking, but every batch gets an even share of files
69927
+ * AND bytes, so no full chunk is a straggler while
69688
69928
  * the remainder-batch worker idles — and the heavy files are SPREAD across
69689
69929
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
69690
69930
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
69691
69931
  * measurably regressing size-skewed repos, which is why it never earned the
69692
69932
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
69693
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
69933
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
69694
69934
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
69695
69935
  * never reach this — they pass user-scoped `includePaths` that skip discovery
69696
69936
  * and stay in arrival order; only the full-scan branch reads it.
@@ -69785,6 +70025,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
69785
70025
  score: ["design"],
69786
70026
  ciFailure: ["design"]
69787
70027
  };
70028
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69788
70029
  const VALID_RULE_SEVERITIES = [
69789
70030
  "error",
69790
70031
  "warn",
@@ -69827,7 +70068,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
69827
70068
  ];
69828
70069
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
69829
70070
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
69830
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69831
70071
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
69832
70072
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
69833
70073
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -69854,8 +70094,45 @@ const validateStringArrayField = (fieldName, value) => {
69854
70094
  return false;
69855
70095
  });
69856
70096
  };
70097
+ const validateIgnoreOverride = (rawOverride, index) => {
70098
+ const fieldName = `ignore.overrides[${index}]`;
70099
+ if (!isRecord$1(rawOverride)) {
70100
+ warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawOverride}); ignoring this override.`);
70101
+ return;
70102
+ }
70103
+ const files = validateStringArrayField(`${fieldName}.files`, rawOverride.files);
70104
+ if (files === void 0) return void 0;
70105
+ const validatedOverride = { files };
70106
+ if (rawOverride.rules !== void 0) {
70107
+ const rules = validateStringArrayField(`${fieldName}.rules`, rawOverride.rules);
70108
+ if (rules !== void 0) validatedOverride.rules = rules;
70109
+ }
70110
+ return validatedOverride;
70111
+ };
70112
+ const validateIgnoreField = (rawIgnore) => {
70113
+ if (!isRecord$1(rawIgnore)) {
70114
+ warnConfigIssue(`config field "ignore" must be an object (got ${typeof rawIgnore}); ignoring this field.`);
70115
+ return;
70116
+ }
70117
+ const validatedIgnore = {};
70118
+ for (const fieldName of [
70119
+ "rules",
70120
+ "files",
70121
+ "tags"
70122
+ ]) {
70123
+ if (rawIgnore[fieldName] === void 0) continue;
70124
+ const validatedValues = validateStringArrayField(`ignore.${fieldName}`, rawIgnore[fieldName]);
70125
+ if (validatedValues !== void 0) validatedIgnore[fieldName] = validatedValues;
70126
+ }
70127
+ if (rawIgnore.overrides !== void 0) if (!Array.isArray(rawIgnore.overrides)) warnConfigIssue(`config field "ignore.overrides" must be an array (got ${typeof rawIgnore.overrides}); ignoring this field.`);
70128
+ else validatedIgnore.overrides = rawIgnore.overrides.flatMap((rawOverride, index) => {
70129
+ const validatedOverride = validateIgnoreOverride(rawOverride, index);
70130
+ return validatedOverride ? [validatedOverride] : [];
70131
+ });
70132
+ return validatedIgnore;
70133
+ };
69857
70134
  const validateSurfaceControls = (surface, rawControls) => {
69858
- if (!isPlainObject$1(rawControls)) {
70135
+ if (!isRecord$1(rawControls)) {
69859
70136
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
69860
70137
  return;
69861
70138
  }
@@ -69879,7 +70156,7 @@ const validateSurfaceControls = (surface, rawControls) => {
69879
70156
  return validatedSurfaceControls;
69880
70157
  };
69881
70158
  const validateSurfacesField = (rawSurfaces) => {
69882
- if (!isPlainObject$1(rawSurfaces)) {
70159
+ if (!isRecord$1(rawSurfaces)) {
69883
70160
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
69884
70161
  return;
69885
70162
  }
@@ -69895,7 +70172,7 @@ const validateSurfacesField = (rawSurfaces) => {
69895
70172
  return validated;
69896
70173
  };
69897
70174
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
69898
- if (!isPlainObject$1(rawMap)) {
70175
+ if (!isRecord$1(rawMap)) {
69899
70176
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
69900
70177
  return;
69901
70178
  }
@@ -69929,6 +70206,7 @@ const validateConfigTypes = (config) => {
69929
70206
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
69930
70207
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
69931
70208
  for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
70209
+ applyFieldValidator(config, validated, "ignore", validateIgnoreField);
69932
70210
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
69933
70211
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
69934
70212
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -70256,6 +70534,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
70256
70534
  }
70257
70535
  return uniqueDiagnostics;
70258
70536
  };
70537
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
70538
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
70539
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
70540
+ };
70259
70541
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
70260
70542
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
70261
70543
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -71617,7 +71899,7 @@ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
71617
71899
  if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
71618
71900
  }
71619
71901
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
71620
- const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
71902
+ const tagEvidence = ts.isIdentifier(node.tagName) || ts.isPropertyAccessExpression(node.tagName) ? resolveMotionExpressionEvidence(node.tagName, typeChecker, program) : EMPTY_MOTION_EXPRESSION_EVIDENCE;
71621
71903
  if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
71622
71904
  if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
71623
71905
  }
@@ -71832,7 +72114,7 @@ const readScannedFile = (candidate) => {
71832
72114
  return null;
71833
72115
  }
71834
72116
  };
71835
- function* collectSecurityScanFiles(rootDirectory) {
72117
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
71836
72118
  const priorityCandidates = [];
71837
72119
  const artifactCandidates = [];
71838
72120
  const otherCandidates = [];
@@ -71847,7 +72129,7 @@ function* collectSecurityScanFiles(rootDirectory) {
71847
72129
  for (const entry of readDirectoryEntries(current.absolutePath)) {
71848
72130
  const absolutePath = Path.join(current.absolutePath, entry.name);
71849
72131
  if (entry.isDirectory()) {
71850
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
72132
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
71851
72133
  absolutePath,
71852
72134
  depth: current.depth + 1
71853
72135
  });
@@ -72028,10 +72310,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
72028
72310
  }
72029
72311
  return true;
72030
72312
  };
72313
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
72031
72314
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
72032
- const yieldToEventLoop = () => new Promise((resolve) => {
72033
- setImmediate(resolve);
72034
- });
72035
72315
  const createSecurityScanSession = (rootDirectory, options) => {
72036
72316
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
72037
72317
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -72084,19 +72364,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
72084
72364
  };
72085
72365
  };
72086
72366
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
72367
+ const throwIfScanAborted = () => {
72368
+ options.signal?.throwIfAborted();
72369
+ };
72370
+ const didReachDeadline = () => {
72371
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
72372
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
72373
+ return hasDeadlineElapsed;
72374
+ };
72375
+ throwIfScanAborted();
72087
72376
  const session = createSecurityScanSession(rootDirectory, options);
72088
72377
  if (session === null) return [];
72378
+ if (didReachDeadline()) return [];
72089
72379
  let sliceStartedAt = performance.now();
72090
- for (const file of collectSecurityScanFiles(rootDirectory)) {
72380
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
72381
+ throwIfScanAborted();
72382
+ if (didReachDeadline()) return session.diagnostics;
72091
72383
  if (file === null) {
72092
72384
  if (performance.now() - sliceStartedAt >= 12) {
72093
72385
  await yieldToEventLoop();
72386
+ throwIfScanAborted();
72387
+ if (didReachDeadline()) return session.diagnostics;
72094
72388
  sliceStartedAt = performance.now();
72095
72389
  }
72096
72390
  continue;
72097
72391
  }
72098
72392
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
72099
72393
  await yieldToEventLoop();
72394
+ throwIfScanAborted();
72395
+ if (didReachDeadline()) return session.diagnostics;
72100
72396
  sliceStartedAt = performance.now();
72101
72397
  }
72102
72398
  }
@@ -72275,25 +72571,18 @@ const collectIgnorePatterns = (rootDirectory) => {
72275
72571
  cachedPatternsByRoot.set(rootDirectory, patterns);
72276
72572
  return patterns;
72277
72573
  };
72278
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
72279
- const KNIP_JSON_FILENAME = "knip.json";
72280
- const readJsonFileSafe = (filePath) => {
72281
- let rawContents;
72282
- try {
72283
- rawContents = fs.readFileSync(filePath, "utf-8");
72284
- } catch {
72285
- return null;
72286
- }
72574
+ const failOpenReadJson = (filePath, fallback) => {
72287
72575
  try {
72288
- return JSON.parse(rawContents);
72576
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
72289
72577
  } catch {
72290
- return null;
72578
+ return fallback;
72291
72579
  }
72292
72580
  };
72581
+ const KNIP_JSON_FILENAME = "knip.json";
72293
72582
  const readKnipConfig = (rootDirectory) => {
72294
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
72583
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
72295
72584
  if (isRecord$1(knipJson)) return knipJson;
72296
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
72585
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
72297
72586
  const packageKnipConfig = isRecord$1(packageJson) ? packageJson.knip : null;
72298
72587
  return isRecord$1(packageKnipConfig) ? packageKnipConfig : null;
72299
72588
  };
@@ -72341,13 +72630,6 @@ const atomicWriteJson = (filePath, value) => {
72341
72630
  return;
72342
72631
  }
72343
72632
  };
72344
- const failOpenReadJson = (filePath, fallback) => {
72345
- try {
72346
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
72347
- } catch {
72348
- return fallback;
72349
- }
72350
- };
72351
72633
  const hashFileContents = (filePath) => {
72352
72634
  try {
72353
72635
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -73026,7 +73308,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
73026
73308
  return true;
73027
73309
  };
73028
73310
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
73029
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
73030
73311
  /**
73031
73312
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
73032
73313
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -73034,25 +73315,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
73034
73315
  * tipped over by any concurrent load — silently dropping every dead-code
73035
73316
  * finding. Scaling the budget with file count (and inversely with the core
73036
73317
  * share when overlapped) lets the pass complete, while the ceiling still
73037
- * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
73038
- * and the Effect-side phase backstop that sits a margin above it.
73318
+ * reclaims a genuinely wedged worker.
73039
73319
  */
73040
73320
  const resolveDeadCodeTimeout = (input) => {
73041
73321
  const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
73042
- const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
73043
- return {
73044
- workerTimeoutMs,
73045
- phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
73046
- };
73322
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
73047
73323
  };
73048
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
73324
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
73049
73325
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
73050
73326
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
73051
- return listSourceFiles(rootDirectory).filter((filePath) => {
73327
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
73052
73328
  if (!isLintableSourceFile(filePath)) return false;
73053
73329
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
73054
73330
  });
73055
73331
  };
73332
+ const filterPathsOutsideDirectories = (input) => {
73333
+ const excludedPrefixes = input.excludedDirectories.map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.rootDirectory)).map((excludedDirectory) => Path.relative(input.rootDirectory, excludedDirectory).replaceAll("\\", "/")).sort((left, right) => left.length - right.length).filter((candidatePrefix, index, prefixes) => !prefixes.slice(0, index).some((prefix) => candidatePrefix.startsWith(`${prefix}/`)));
73334
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
73335
+ return input.relativePaths.filter((relativePath) => {
73336
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
73337
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
73338
+ });
73339
+ };
73056
73340
  var Config = class Config extends Service()("react-doctor/Config") {
73057
73341
  static layerNode = effect(Config, gen(function* () {
73058
73342
  const cache = yield* make$3({
@@ -73114,6 +73398,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
73114
73398
  static layerNode = succeed$3(Files, Files.of({
73115
73399
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
73116
73400
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
73401
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
73117
73402
  isFile: (filePath) => sync(() => isFile(filePath)),
73118
73403
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
73119
73404
  }));
@@ -73140,6 +73425,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
73140
73425
  }
73141
73426
  return files;
73142
73427
  }),
73428
+ listSourceFilesCooperative: (input) => sync(() => {
73429
+ const rootDirectory = input.rootDirectory;
73430
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
73431
+ const files = [];
73432
+ for (const absolute of tree.keys()) {
73433
+ if (!absolute.startsWith(prefix)) continue;
73434
+ files.push(absolute.slice(prefix.length));
73435
+ }
73436
+ return files;
73437
+ }),
73143
73438
  isFile: (filePath) => sync(() => tree.has(filePath)),
73144
73439
  isDirectory: (filePath) => sync(() => {
73145
73440
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -73604,7 +73899,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
73604
73899
  changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
73605
73900
  isCurrentChanges: false
73606
73901
  };
73607
- }).pipe(withSpan("Git.diffSelection")),
73902
+ }).pipe(catchReasons("ReactDoctorError", { GitInvocationFailed: () => succeed$2(null) }, (_reason, error) => fail$4(error)), withSpan("Git.diffSelection")),
73608
73903
  stagedFilePaths: (directory) => runGit(directory, [
73609
73904
  "diff",
73610
73905
  "--no-ext-diff",
@@ -73715,7 +74010,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
73715
74010
  for (const filePath of includePaths) {
73716
74011
  const entryLength = filePath.length + 1;
73717
74012
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
73718
- const exceedsFileCount = currentBatch.length >= 100;
74013
+ const exceedsFileCount = currentBatch.length >= 200;
73719
74014
  if (exceedsArgLength || exceedsFileCount) {
73720
74015
  batches.push(currentBatch);
73721
74016
  currentBatch = [];
@@ -73820,6 +74115,33 @@ const detectUserLintConfigPaths = (rootDirectory) => {
73820
74115
  return [];
73821
74116
  };
73822
74117
  const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
74118
+ const neutralizedFileLeases = /* @__PURE__ */ new Map();
74119
+ const activeNeutralizationInitializations = /* @__PURE__ */ new Set();
74120
+ const directoriesOverlap = (left, right) => {
74121
+ const isWithin = (parent, candidate) => {
74122
+ const relativePath = Path.relative(parent, candidate);
74123
+ return relativePath.length === 0 || !relativePath.startsWith(`..${Path.sep}`) && relativePath !== ".." && !Path.isAbsolute(relativePath);
74124
+ };
74125
+ return isWithin(left, right) || isWithin(right, left);
74126
+ };
74127
+ const withOverlappingInitializationLock = async (rootDirectory, operation) => {
74128
+ const blockers = [...activeNeutralizationInitializations].filter((initialization) => directoriesOverlap(initialization.rootDirectory, rootDirectory)).map((initialization) => initialization.done);
74129
+ let release;
74130
+ const initialization = {
74131
+ rootDirectory,
74132
+ done: new Promise((resolve) => {
74133
+ release = resolve;
74134
+ })
74135
+ };
74136
+ activeNeutralizationInitializations.add(initialization);
74137
+ await Promise.all(blockers);
74138
+ try {
74139
+ return await operation();
74140
+ } finally {
74141
+ activeNeutralizationInitializations.delete(initialization);
74142
+ release();
74143
+ }
74144
+ };
73823
74145
  const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths) => {
73824
74146
  const program = gen(function* () {
73825
74147
  return yield* (yield* Git).grep({
@@ -73862,34 +74184,84 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
73862
74184
  };
73863
74185
  const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
73864
74186
  const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
73865
- const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
73866
- const filePaths = await findFilesWithDisableDirectives(rootDirectory, includePaths);
73867
- const originalContents = /* @__PURE__ */ new Map();
74187
+ const collectNeutralizationCandidatePaths = (rootDirectory, discoveredRelativePaths, includePaths) => {
74188
+ const resolvedRootDirectory = NFS.realpathSync(rootDirectory);
74189
+ const requestedPaths = includePaths && includePaths.length > 0 ? new Set(includePaths.flatMap((includePath) => {
74190
+ try {
74191
+ return [NFS.realpathSync(Path.resolve(resolvedRootDirectory, includePath))];
74192
+ } catch {
74193
+ return [];
74194
+ }
74195
+ })) : null;
74196
+ const candidatePaths = /* @__PURE__ */ new Set();
74197
+ for (const relativePath of discoveredRelativePaths) try {
74198
+ candidatePaths.add(NFS.realpathSync(Path.resolve(resolvedRootDirectory, relativePath)));
74199
+ } catch {
74200
+ continue;
74201
+ }
74202
+ for (const leasedPath of neutralizedFileLeases.keys()) {
74203
+ const relativePath = Path.relative(resolvedRootDirectory, leasedPath);
74204
+ if (!(relativePath.length === 0 || !relativePath.startsWith(`..${Path.sep}`) && relativePath !== ".." && !Path.isAbsolute(relativePath)) || requestedPaths && !requestedPaths.has(leasedPath)) continue;
74205
+ candidatePaths.add(leasedPath);
74206
+ }
74207
+ return candidatePaths;
74208
+ };
74209
+ const neutralizeDisableDirectives = async (rootDirectory, includePaths, options = {}) => {
74210
+ const resolvedRootDirectory = NFS.realpathSync(rootDirectory);
74211
+ const leasedPaths = /* @__PURE__ */ new Set();
73868
74212
  let isRestored = false;
73869
74213
  const restore = () => {
73870
74214
  if (isRestored) return;
73871
74215
  isRestored = true;
73872
- for (const [absolutePath, originalContent] of originalContents) try {
73873
- NFS.writeFileSync(absolutePath, originalContent);
73874
- } catch (error) {
73875
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
74216
+ for (const absolutePath of leasedPaths) {
74217
+ const lease = neutralizedFileLeases.get(absolutePath);
74218
+ if (!lease) continue;
74219
+ lease.referenceCount -= 1;
74220
+ if (lease.referenceCount > 0) continue;
74221
+ neutralizedFileLeases.delete(absolutePath);
74222
+ try {
74223
+ NFS.writeFileSync(absolutePath, lease.originalContent);
74224
+ } catch (error) {
74225
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
74226
+ }
73876
74227
  }
73877
74228
  };
73878
74229
  const onExit = () => restore();
73879
74230
  process.once("exit", onExit);
73880
- for (const relativePath of filePaths) {
73881
- const absolutePath = Path.join(rootDirectory, relativePath);
74231
+ const leaseCandidatePath = (absolutePath) => {
74232
+ if (leasedPaths.has(absolutePath)) return;
74233
+ const existingLease = neutralizedFileLeases.get(absolutePath);
74234
+ if (existingLease) {
74235
+ existingLease.referenceCount += 1;
74236
+ leasedPaths.add(absolutePath);
74237
+ return;
74238
+ }
73882
74239
  let originalContent;
73883
74240
  try {
73884
74241
  originalContent = NFS.readFileSync(absolutePath, "utf-8");
73885
74242
  } catch {
73886
- continue;
74243
+ return;
73887
74244
  }
73888
74245
  const neutralizedContent = neutralizeContent(originalContent);
73889
74246
  if (neutralizedContent !== originalContent) {
73890
- originalContents.set(absolutePath, originalContent);
73891
74247
  NFS.writeFileSync(absolutePath, neutralizedContent);
74248
+ neutralizedFileLeases.set(absolutePath, {
74249
+ originalContent,
74250
+ referenceCount: 1
74251
+ });
74252
+ leasedPaths.add(absolutePath);
73892
74253
  }
74254
+ };
74255
+ try {
74256
+ await withOverlappingInitializationLock(resolvedRootDirectory, async () => {
74257
+ for (const absolutePath of collectNeutralizationCandidatePaths(resolvedRootDirectory, [], includePaths)) if (neutralizedFileLeases.has(absolutePath)) leaseCandidatePath(absolutePath);
74258
+ const discoveredRelativePaths = await (options.findFiles ?? findFilesWithDisableDirectives)(resolvedRootDirectory, includePaths);
74259
+ for (const absolutePath of collectNeutralizationCandidatePaths(resolvedRootDirectory, discoveredRelativePaths, includePaths)) leaseCandidatePath(absolutePath);
74260
+ });
74261
+ } catch (error) {
74262
+ restore();
74263
+ process.removeListener("exit", onExit);
74264
+ throw error;
73893
74265
  }
73894
74266
  return () => {
73895
74267
  restore();
@@ -74062,7 +74434,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
74062
74434
  }
74063
74435
  return enabled;
74064
74436
  };
74065
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
74437
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, projectIndexModuleSources, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
74066
74438
  const hasIncludedTags = includedTags.size > 0;
74067
74439
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
74068
74440
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -74126,7 +74498,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
74126
74498
  }))
74127
74499
  } : {},
74128
74500
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
74129
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
74501
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
74502
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
74130
74503
  } },
74131
74504
  rules: {
74132
74505
  ...reactCompilerRules,
@@ -74375,11 +74748,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
74375
74748
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
74376
74749
  };
74377
74750
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
74751
+ const decodeDiagnostic = (raw) => {
74752
+ const diagnostic = validateDiagnostic$1(raw);
74753
+ return {
74754
+ ...diagnostic,
74755
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
74756
+ };
74757
+ };
74378
74758
  const decodeFileDiagnostics = (raw) => {
74379
74759
  if (!Array.isArray(raw)) return null;
74380
74760
  try {
74381
- for (const entry of raw) validateDiagnostic$1(entry);
74382
- return raw;
74761
+ return raw.map((entry) => decodeDiagnostic(entry));
74383
74762
  } catch {
74384
74763
  return null;
74385
74764
  }
@@ -75611,6 +75990,12 @@ const buildProfiledNodeArguments = (input) => [
75611
75990
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
75612
75991
  ...input.argumentsList
75613
75992
  ];
75993
+ const lowerChildProcessPriority = (processId) => {
75994
+ if (processId === void 0) return;
75995
+ try {
75996
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
75997
+ } catch {}
75998
+ };
75614
75999
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
75615
76000
  /**
75616
76001
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -75649,6 +76034,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
75649
76034
  "pipe"
75650
76035
  ]
75651
76036
  });
76037
+ lowerChildProcessPriority(child.pid);
75652
76038
  const onAbort = () => {
75653
76039
  child.kill("SIGKILL");
75654
76040
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -75771,7 +76157,7 @@ const isParallelismRelatedSpawnError = (error) => {
75771
76157
  * loop with a slimmer config in that case.
75772
76158
  */
75773
76159
  const spawnLintBatches = async (input) => {
75774
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
76160
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
75775
76161
  const resolveSourcePath = (filePath) => {
75776
76162
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
75777
76163
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -75779,6 +76165,12 @@ const spawnLintBatches = async (input) => {
75779
76165
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
75780
76166
  let rescueDeadlineEpochMs;
75781
76167
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
76168
+ const resolveSpawnTimeoutMs = () => {
76169
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
76170
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
76171
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
76172
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
76173
+ };
75782
76174
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
75783
76175
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
75784
76176
  const allDiagnostics = [];
@@ -75796,12 +76188,13 @@ const spawnLintBatches = async (input) => {
75796
76188
  const batchArgs = [...baseArgs, ...batch];
75797
76189
  try {
75798
76190
  const spawnBatch = () => {
75799
- if (isPastDeadline()) {
76191
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
76192
+ if (effectiveSpawnTimeoutMs === 0) {
75800
76193
  deadlineSkippedFiles.push(...batch);
75801
76194
  batchState.deadlineSkippedFileCount += batch.length;
75802
76195
  return Promise.resolve(null);
75803
76196
  }
75804
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
76197
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
75805
76198
  if (batchState.didStart) return;
75806
76199
  batchState.didStart = true;
75807
76200
  startedFileCount += batchState.initialFileCount;
@@ -75956,11 +76349,36 @@ const validateRuleRegistration = () => {
75956
76349
  ].filter((entry) => entry !== null).join("; ");
75957
76350
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
75958
76351
  };
76352
+ const PROJECT_INDEX_MODULE_SOURCES = [
76353
+ "next/og",
76354
+ "@vercel/og",
76355
+ "satori",
76356
+ "remotion"
76357
+ ];
76358
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
76359
+ const foundModuleSources = /* @__PURE__ */ new Set();
76360
+ let sliceStartedAt = performance.now();
76361
+ for (const candidateFile of candidateFiles) {
76362
+ let sourceBuffer;
76363
+ try {
76364
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
76365
+ } catch {
76366
+ continue;
76367
+ }
76368
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
76369
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
76370
+ if (performance.now() - sliceStartedAt >= 12) {
76371
+ await yieldToEventLoop();
76372
+ sliceStartedAt = performance.now();
76373
+ }
76374
+ }
76375
+ return [...foundModuleSources].sort();
76376
+ };
75959
76377
  /**
75960
76378
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
75961
- * `batchIncludePaths` greedily fills 100-file batches and leaves a small
75962
- * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
75963
- * is set by whichever 100-file chunk happened to collect the most work while
76379
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
76380
+ * remainder batch, so the parallel pool's wall clock is set by whichever
76381
+ * full chunk happened to collect the most work while
75964
76382
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
75965
76383
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
75966
76384
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -75989,7 +76407,7 @@ const validateRuleRegistration = () => {
75989
76407
  */
75990
76408
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
75991
76409
  if (files.length === 0) return [];
75992
- const batchCount = Math.ceil(files.length / 100);
76410
+ const batchCount = Math.ceil(files.length / 200);
75993
76411
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
75994
76412
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
75995
76413
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -76518,6 +76936,7 @@ const runOxlint = async (options) => {
76518
76936
  await yieldToEventLoop();
76519
76937
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
76520
76938
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
76939
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
76521
76940
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
76522
76941
  rootDirectory,
76523
76942
  candidateFiles
@@ -76540,6 +76959,7 @@ const runOxlint = async (options) => {
76540
76959
  runtimeGlobals,
76541
76960
  unpluginAutoImportGlobalScopes,
76542
76961
  serverAuthFunctionNames,
76962
+ projectIndexModuleSources,
76543
76963
  severityControls,
76544
76964
  userPlugins,
76545
76965
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -77007,10 +77427,10 @@ const translateProjectInfoError = (cause, directory) => {
77007
77427
  }) });
77008
77428
  };
77009
77429
  var Project = class Project extends Service()("react-doctor/Project") {
77010
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
77430
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
77011
77431
  return yield* try_({
77012
- try: () => discoverProject(directory),
77013
- catch: (cause) => translateProjectInfoError(cause, directory)
77432
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
77433
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
77014
77434
  });
77015
77435
  }) }));
77016
77436
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -77595,9 +78015,10 @@ const checkSupplyChain = (input) => gen(function* () {
77595
78015
  * outage.
77596
78016
  */
77597
78017
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
77598
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
77599
- ...input,
77600
- totalTimeoutMs
78018
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
78019
+ rootDirectory: input.rootDirectory,
78020
+ userConfig: input.userConfig,
78021
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
77601
78022
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
77602
78023
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
77603
78024
  };
@@ -77632,10 +78053,6 @@ const NO_HOOKS = {
77632
78053
  beforeLint: () => void_,
77633
78054
  afterLint: () => void_
77634
78055
  };
77635
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
77636
- const result = transform(value);
77637
- return result === null ? none() : some(result);
77638
- });
77639
78056
  const fileReader = (filesService, rootDirectory) => (filePath) => {
77640
78057
  const lines = runSync(filesService.readLines({
77641
78058
  filePath,
@@ -77659,7 +78076,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
77659
78076
  * The GitHub viewer-permission lookup is forked onto a background
77660
78077
  * fiber here and joined late (it feeds score metadata, not
77661
78078
  * diagnostics).
77662
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
78079
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
77663
78080
  * 3. environment checks (reduced-motion + pnpm hardening +
77664
78081
  * expo/react-native), collected synchronously. The heavier
77665
78082
  * content-regex security scan is forked instead (like supply-chain
@@ -77672,13 +78089,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
77672
78089
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
77673
78090
  * socket can't drag out its join; on timeout it fails open to no
77674
78091
  * diagnostics — the same outcome class as a Socket outage.
77675
- * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
77676
- * fiber) ONLY when the memory gate has headroom to run the 8 GB
77677
- * dead-code child alongside the oxlint workers — or when overlap is
77678
- * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
77679
- * runs sequentially after lint, exactly as it did pre-overlap. The
77680
- * fiber is joined (or interrupted, SIGKILLing its worker, on lint
77681
- * failure) before diagnostics are concatenated. The afterLint hook
78092
+ * 5. Linter.run runs; DeadCode.run forks only when
78093
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
78094
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
78095
+ * SIGKILLing its worker, on lint failure) before diagnostics are
78096
+ * concatenated. The afterLint hook
77682
78097
  * fires between lint and dead-code. Progress spinner labels AND the
77683
78098
  * final diagnostic / score order stay independent of execution
77684
78099
  * order, so terminal output is identical either way; supply-chain
@@ -77713,8 +78128,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77713
78128
  const partialFailuresRef = yield* LintPartialFailures;
77714
78129
  const resolvedConfig = yield* configService.resolve(input.directory);
77715
78130
  const scanDirectory = resolvedConfig.resolvedDirectory;
77716
- const project = yield* projectService.discover(scanDirectory);
78131
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
78132
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
78133
+ const isExcludedProjectDiagnostic = (diagnostic) => {
78134
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
78135
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
78136
+ };
78137
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
78138
+ rootDirectory: scanDirectory,
78139
+ signal: input.signal
78140
+ }) : null;
78141
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
78142
+ rootDirectory: scanDirectory,
78143
+ relativePaths: precomputedSourceFiles,
78144
+ excludedDirectories: excludedProjectDirectories
78145
+ }) : precomputedSourceFiles;
78146
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
78147
+ const project = yield* projectService.discover({
78148
+ directory: scanDirectory,
78149
+ sourceFileCount
78150
+ });
77717
78151
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
78152
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
77718
78153
  const [repo, sha, defaultBranch] = yield* all([
77719
78154
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
77720
78155
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -77725,11 +78160,16 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77725
78160
  directory: scanDirectory,
77726
78161
  repo
77727
78162
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
77728
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
77729
- const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
78163
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
78164
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
78165
+ rootDirectory: scanDirectory,
78166
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
78167
+ excludedDirectories: excludedProjectDirectories
78168
+ });
78169
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
77730
78170
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
77731
78171
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
77732
- yield* beforeLint(project, lintIncludePaths ?? void 0);
78172
+ yield* beforeLint(project);
77733
78173
  const isDiffMode = input.includePaths.length > 0;
77734
78174
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
77735
78175
  const transform = buildDiagnosticPipeline({
@@ -77739,7 +78179,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77739
78179
  respectInlineDisables: input.respectInlineDisables,
77740
78180
  showWarnings
77741
78181
  });
77742
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
78182
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
78183
+ const filteredDiagnostic = transform.apply(diagnostic);
78184
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
78185
+ })));
77743
78186
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
77744
78187
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
77745
78188
  ...checkReducedMotion(scanDirectory),
@@ -77749,17 +78192,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77749
78192
  ...checkReactNativeProject(scanDirectory, project)
77750
78193
  ])));
77751
78194
  const securityScanFailedRef = yield* make$13(false);
77752
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
78195
+ let didSecurityScanReachDeadline = false;
78196
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
77753
78197
  project,
77754
78198
  ignoredTags: input.ignoredTags,
77755
78199
  includedTags: input.includedTags,
77756
- includeTagDefaults: input.includeTagDefaults
78200
+ includeTagDefaults: input.includeTagDefaults,
78201
+ excludedDirectories: new Set(excludedProjectDirectories),
78202
+ deadlineEpochMs: input.deadlineEpochMs,
78203
+ signal,
78204
+ onDeadlineExceeded: () => {
78205
+ didSecurityScanReachDeadline = true;
78206
+ }
77757
78207
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
78208
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
78209
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
78210
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
78211
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
78212
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
78213
+ };
77758
78214
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
77759
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
78215
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
77760
78216
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
77761
78217
  rootDirectory: scanDirectory,
77762
- userConfig: resolvedConfig.config
78218
+ userConfig: resolvedConfig.config,
78219
+ timeoutMs: supplyChainOverlapTimeout
77763
78220
  }))).pipe(map$3((diagnostics) => ({
77764
78221
  diagnostics,
77765
78222
  timedOut: false
@@ -77783,46 +78240,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77783
78240
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
77784
78241
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
77785
78242
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
77786
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
77787
78243
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
77788
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
77789
78244
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
77790
78245
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
77791
78246
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
77792
78247
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
77793
78248
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
77794
- const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
77795
- rootDirectory: scanDirectory,
77796
- parseConcurrency: deadCodeParseConcurrency,
77797
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
77798
- onCacheOutcome: (didHitCache) => {
77799
- deadCodeCacheHit = didHitCache;
77800
- },
77801
- onSummaryCacheStats: (stats) => {
77802
- deadCodeSummaryCacheHits = stats.hits;
77803
- deadCodeSummaryCacheMisses = stats.misses;
77804
- }
77805
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
77806
- yield* set(deadCodeFailure, {
77807
- didFail: true,
77808
- reason: error.message
77809
- });
77810
- return empty$4;
77811
- })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
77812
- onNone: () => set(deadCodeFailure, {
77813
- didFail: true,
77814
- reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
77815
- }).pipe(as([])),
77816
- onSome: succeed$2
77817
- })));
78249
+ const buildCollectDeadCode = (deadCodeTimeout) => {
78250
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
78251
+ rootDirectory: scanDirectory,
78252
+ parseConcurrency: deadCodeParseConcurrency,
78253
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
78254
+ onCacheOutcome: (didHitCache) => {
78255
+ deadCodeCacheHit = didHitCache;
78256
+ },
78257
+ onSummaryCacheStats: (stats) => {
78258
+ deadCodeSummaryCacheHits = stats.hits;
78259
+ deadCodeSummaryCacheMisses = stats.misses;
78260
+ }
78261
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
78262
+ yield* set(deadCodeFailure, {
78263
+ didFail: true,
78264
+ reason: error.message
78265
+ });
78266
+ return empty$4;
78267
+ }))))));
78268
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
78269
+ if (phaseTimeoutMs === null) return collectDeadCode;
78270
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
78271
+ onNone: () => set(deadCodeFailure, {
78272
+ didFail: true,
78273
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
78274
+ }).pipe(as([])),
78275
+ onSome: succeed$2
78276
+ })));
78277
+ };
77818
78278
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
77819
- sourceFileCount: project.sourceFileCount,
78279
+ sourceFileCount: deadCodeSourceFileCount,
77820
78280
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
77821
78281
  fullConcurrency: scanConcurrency
77822
78282
  });
77823
78283
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
77824
78284
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
77825
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
78285
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77826
78286
  })) : null;
77827
78287
  const scanProgress = yield* progressService.start("Scanning...");
77828
78288
  const scanStartTime = Date.now();
@@ -77873,7 +78333,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77873
78333
  });
77874
78334
  return empty$4;
77875
78335
  }))));
77876
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
78336
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
78337
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
77877
78338
  onNone: () => set(lintFailure, {
77878
78339
  didFail: true,
77879
78340
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -77903,13 +78364,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77903
78364
  } else {
77904
78365
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
77905
78366
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
77906
- sourceFileCount: totalFileCount,
78367
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
77907
78368
  deadCodeConcurrency: scanConcurrency,
77908
78369
  fullConcurrency: scanConcurrency
77909
78370
  });
77910
78371
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
77911
78372
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
77912
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
78373
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
77913
78374
  });
77914
78375
  }
77915
78376
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -77953,7 +78414,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77953
78414
  metadata: scoreMetadata
77954
78415
  });
77955
78416
  const lintPartialFailures = yield* get$2(partialFailuresRef);
77956
- const securityScanFailed = yield* get$2(securityScanFailedRef);
78417
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
78418
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
78419
+ let securityScanFailureReason = null;
78420
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
78421
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
77957
78422
  return {
77958
78423
  project,
77959
78424
  userConfig: resolvedConfig.config,
@@ -77976,6 +78441,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77976
78441
  scanConcurrency,
77977
78442
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
77978
78443
  securityScanFailed,
78444
+ securityScanFailureReason,
77979
78445
  lintCacheHitFileCount,
77980
78446
  lintCacheTotalFileCount,
77981
78447
  lintSidecarReplayedFileCount,
@@ -77991,7 +78457,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
77991
78457
  "inspect.runDeadCode": input.runDeadCode,
77992
78458
  "inspect.isCi": input.isCi,
77993
78459
  "inspect.scoreSurface": input.scoreSurface ?? "score"
77994
- } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
78460
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
78461
+ if (scanDeadlineMs === null) return scanProgram;
78462
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
78463
+ }));
77995
78464
  const parseNodeVersion = (versionString) => {
77996
78465
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
77997
78466
  return {
@@ -78103,16 +78572,6 @@ var NodeResolver = class NodeResolver extends Service()("react-doctor/NodeResolv
78103
78572
  });
78104
78573
  };
78105
78574
  /**
78106
- * Zip-Slip defense: relative paths come from git (`diff --name-only`), which
78107
- * normalizes during ordinary adds, but a crafted index/pack/symlinked tree can
78108
- * smuggle `..` segments that escape the temp root. Resolve against the temp dir
78109
- * and reject anything that lands outside before writing.
78110
- */
78111
- const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
78112
- const relative = path.relative(parentAbsolutePath, childAbsolutePath);
78113
- return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
78114
- };
78115
- /**
78116
78575
  * Writes a set of source files (supplied by `readContent` — e.g.
78117
78576
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
78118
78577
  * index) into a temp tree that mirrors the project layout, copying the
@@ -78148,7 +78607,7 @@ const materializeSourceTree = (input) => gen(function* () {
78148
78607
  const sourcePath = path.join(input.directory, subdirectory, configFilename);
78149
78608
  const targetPath = path.resolve(resolvedTempDirectory, subdirectory, configFilename);
78150
78609
  if (!isPathInsideDirectory(targetPath, resolvedTempDirectory)) continue;
78151
- if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
78610
+ if (isFile(sourcePath) && !fs.existsSync(targetPath)) {
78152
78611
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
78153
78612
  fs.cpSync(sourcePath, targetPath);
78154
78613
  }
@@ -78400,6 +78859,7 @@ const buildJsonReport = (input) => {
78400
78859
  directory: input.directory,
78401
78860
  diff: toJsonDiff(input.diff),
78402
78861
  projects,
78862
+ ...input.skippedProjects && input.skippedProjects.length > 0 ? { skippedProjects: [...input.skippedProjects] } : {},
78403
78863
  diagnostics: flattenedDiagnostics,
78404
78864
  summary,
78405
78865
  elapsedMilliseconds: input.totalElapsedMilliseconds,
@@ -78424,7 +78884,7 @@ const buildSkippedChecks = (input) => {
78424
78884
  else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
78425
78885
  if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
78426
78886
  if (input.supplyChainOverlapTimedOut) skippedCheckReasons["supply-chain"] = "Supply-chain analysis timed out and was skipped.";
78427
- if (input.securityScanFailed) skippedCheckReasons["security-scan"] = "Security scan failed and was skipped.";
78887
+ if (input.securityScanFailed) skippedCheckReasons["security-scan"] = input.securityScanFailureReason ?? "Security scan failed and was skipped.";
78428
78888
  return {
78429
78889
  skippedChecks,
78430
78890
  skippedCheckReasons
@@ -78564,6 +79024,33 @@ const getChangedLineRanges = (input) => runPromise(gen(function* () {
78564
79024
  * instead of being improvised per diagnostic.
78565
79025
  */
78566
79026
  const buildRuleDocsUrl = (plugin, rule) => `${DOCS_RULES_BASE_URL}/${plugin}/${rule}`;
79027
+ const collectSourceFileCountsByDirectory = async (rootDirectory, projectDirectories, signal) => {
79028
+ const resolvedRootDirectory = Path.resolve(rootDirectory);
79029
+ const sourceFileCounts = /* @__PURE__ */ new Map();
79030
+ const projectDirectoryByRelativePath = /* @__PURE__ */ new Map();
79031
+ for (const projectDirectory of projectDirectories) {
79032
+ const resolvedProjectDirectory = Path.resolve(projectDirectory);
79033
+ const relativeDirectory = Path.relative(resolvedRootDirectory, resolvedProjectDirectory);
79034
+ if (Path.isAbsolute(relativeDirectory) || relativeDirectory.startsWith("..")) continue;
79035
+ sourceFileCounts.set(resolvedProjectDirectory, 0);
79036
+ projectDirectoryByRelativePath.set(relativeDirectory.replaceAll(Path.sep, "/"), resolvedProjectDirectory);
79037
+ }
79038
+ const sourceFilePaths = await listSourceFilesCooperative(resolvedRootDirectory, signal);
79039
+ for (const sourceFilePath of sourceFilePaths) {
79040
+ let relativeDirectory = Path.posix.dirname(sourceFilePath);
79041
+ while (true) {
79042
+ const normalizedRelativeDirectory = relativeDirectory === "." ? "" : relativeDirectory;
79043
+ const projectDirectory = projectDirectoryByRelativePath.get(normalizedRelativeDirectory);
79044
+ if (projectDirectory !== void 0) {
79045
+ sourceFileCounts.set(projectDirectory, (sourceFileCounts.get(projectDirectory) ?? 0) + 1);
79046
+ break;
79047
+ }
79048
+ if (relativeDirectory === ".") break;
79049
+ relativeDirectory = Path.posix.dirname(relativeDirectory);
79050
+ }
79051
+ }
79052
+ return sourceFileCounts;
79053
+ };
78567
79054
  const computeConfigFingerprint = (projectDirectory, version) => {
78568
79055
  const parts = [`v=${version}`];
78569
79056
  let directory = projectDirectory;
@@ -78583,59 +79070,34 @@ const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
78583
79070
  slotCount: resolveScanConcurrency(concurrency),
78584
79071
  createAbortError: createLintPhaseAbortError
78585
79072
  });
78586
- const AI_TRAINING_ENV_VARS = [
78587
- ["HF_DATASETS_CACHE", "huggingface"],
78588
- ["HF_HOME", "huggingface"],
78589
- ["HUGGINGFACE_HUB_CACHE", "huggingface"],
78590
- ["SPACE_ID", "huggingface-spaces"],
78591
- ["CUDA_VISIBLE_DEVICES", "cuda"],
78592
- ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
78593
- ["ROCR_VISIBLE_DEVICES", "rocm"],
78594
- ["TPU_NAME", "google-tpu"],
79073
+ const AI_TRAINING_BY_ENVIRONMENT_VARIABLE = [
78595
79074
  ["WANDB_RUN_ID", "wandb"],
78596
79075
  ["WANDB_SWEEP_ID", "wandb"],
78597
79076
  ["MLFLOW_RUN_ID", "mlflow"],
78598
- ["MLFLOW_TRACKING_URI", "mlflow"],
78599
79077
  ["COMET_EXPERIMENT_KEY", "comet"],
78600
79078
  ["NEPTUNE_RUN_ID", "neptune"],
78601
79079
  ["CLEARML_TASK_ID", "clearml"],
78602
79080
  ["DVC_STAGE", "dvc"],
78603
79081
  ["RAY_WORKER_PROCESS", "ray"],
78604
- ["RAY_ADDRESS", "ray"],
78605
- ["MUJOCO_GL", "mujoco"],
78606
- ["MUJOCO_PATH", "mujoco"],
78607
- ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
78608
79082
  ["SM_TRAINING_ENV", "sagemaker"],
78609
79083
  ["TRAINING_JOB_ARN", "sagemaker"],
78610
- ["SAGEMAKER_BASE_DIR", "sagemaker"],
78611
79084
  ["AZUREML_RUN_ID", "azure-ml"],
78612
- ["AZURE_ML_MODEL_DIR", "azure-ml"],
78613
- ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
78614
- ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
78615
- ["DET_MASTER", "determined-ai"],
78616
- ["LIGHTNING_USER_ID", "lightning-ai"],
78617
79085
  ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
78618
- ["ARGO_WORKFLOW_NAME", "argo-workflows"],
78619
79086
  ["KFP_POD_NAME", "kubeflow-pipelines"],
78620
79087
  ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
78621
79088
  ["COLAB_BACKEND_VERSION", "google-colab"],
78622
- ["DATABRICKS_RUNTIME_VERSION", "databricks"],
78623
79089
  ["DAYTONA_WS_ID", "daytona"],
78624
- ["DAYTONA_WS_NAME", "daytona"],
78625
79090
  ["E2B_SANDBOX_ID", "e2b"],
78626
79091
  ["MODAL_FUNCTION_ID", "modal"],
78627
79092
  ["MODAL_TASK_ID", "modal"],
78628
79093
  ["RUNPOD_POD_ID", "runpod"],
78629
- ["REPLICATE_USERNAME", "replicate"],
78630
79094
  ["VAST_CONTAINERLABEL", "vast-ai"],
78631
- ["HARBOR_URL", "harbor"],
78632
- ["HARBOR_HOSTNAME", "harbor"],
78633
79095
  ["SWE_BENCH_TASK", "swe-bench"],
78634
79096
  ["SWEBENCH_TASK", "swe-bench"],
78635
79097
  ["SWE_AGENT_MODEL", "swe-agent"]
78636
79098
  ];
78637
- const detectAiTrainingEnvironment = () => {
78638
- for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
79099
+ const detectAiTrainingEnvironment = (environment = process.env) => {
79100
+ for (const [environmentVariable, label] of AI_TRAINING_BY_ENVIRONMENT_VARIABLE) if (environment[environmentVariable]?.trim()) return label;
78639
79101
  return null;
78640
79102
  };
78641
79103
  const groupBy = (items, keyFn) => {
@@ -78663,16 +79125,17 @@ const groupBy = (items, keyFn) => {
78663
79125
  * a 404. Gate the directive on this predicate.
78664
79126
  */
78665
79127
  const hasPublishedFixRecipe = (diagnostic) => diagnostic.plugin === "react-doctor" && Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, diagnostic.rule);
78666
- const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
79128
+ const stringValues = (values) => Array.isArray(values) ? values.filter((value) => typeof value === "string") : [];
79129
+ const unionValues = (baseValues, overrideValues) => [...new Set([...stringValues(baseValues), ...stringValues(overrideValues)])];
78667
79130
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
78668
79131
  const mergedIgnore = {
78669
79132
  ...baseIgnore,
78670
79133
  ...overrideIgnore
78671
79134
  };
78672
- if (baseIgnore.rules && overrideIgnore.rules) mergedIgnore.rules = unionValues(baseIgnore.rules, overrideIgnore.rules);
78673
- if (baseIgnore.files && overrideIgnore.files) mergedIgnore.files = unionValues(baseIgnore.files, overrideIgnore.files);
78674
- if (baseIgnore.tags && overrideIgnore.tags) mergedIgnore.tags = unionValues(baseIgnore.tags, overrideIgnore.tags);
78675
- if (baseIgnore.overrides && overrideIgnore.overrides) mergedIgnore.overrides = [...baseIgnore.overrides, ...overrideIgnore.overrides];
79135
+ if (baseIgnore.rules !== void 0 || overrideIgnore.rules !== void 0) mergedIgnore.rules = unionValues(baseIgnore.rules, overrideIgnore.rules);
79136
+ if (baseIgnore.files !== void 0 || overrideIgnore.files !== void 0) mergedIgnore.files = unionValues(baseIgnore.files, overrideIgnore.files);
79137
+ if (baseIgnore.tags !== void 0 || overrideIgnore.tags !== void 0) mergedIgnore.tags = unionValues(baseIgnore.tags, overrideIgnore.tags);
79138
+ if (baseIgnore.overrides !== void 0 || overrideIgnore.overrides !== void 0) mergedIgnore.overrides = [...Array.isArray(baseIgnore.overrides) ? baseIgnore.overrides : [], ...Array.isArray(overrideIgnore.overrides) ? overrideIgnore.overrides : []];
78676
79139
  return mergedIgnore;
78677
79140
  };
78678
79141
  /**
@@ -78867,6 +79330,21 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
78867
79330
  ];
78868
79331
  const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
78869
79332
  //#endregion
79333
+ //#region src/cli/utils/cli-input-error.ts
79334
+ /**
79335
+ * A mistake in *how the CLI was invoked* — a malformed `<file>:<line>`
79336
+ * argument, mutually exclusive flags, or an unknown `--project` name. This is
79337
+ * user input, not a react-doctor bug, so `isExpectedUserError` routes it
79338
+ * through `handleUserError`: a clean, single-line message with no Sentry
79339
+ * report and no "Something went wrong, open a prefilled issue" block.
79340
+ *
79341
+ * The `message` is rendered verbatim, so it must read as a complete,
79342
+ * human-readable sentence that tells the user how to fix their invocation.
79343
+ */
79344
+ var CliInputError = class extends Error {
79345
+ name = "CliInputError";
79346
+ };
79347
+ //#endregion
78870
79348
  //#region src/cli/utils/constants.ts
78871
79349
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
78872
79350
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
@@ -78911,7 +79389,7 @@ const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
78911
79389
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
78912
79390
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
78913
79391
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
78914
- const TUI_REPORT_COMPACT_MAX_ROWS = 29;
79392
+ const TUI_REPORT_COMPACT_MAX_ROWS = 23;
78915
79393
  const TUI_REPORT_DETAIL_WIDTH_FRACTION = .6;
78916
79394
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
78917
79395
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
@@ -78934,8 +79412,11 @@ const METRIC = {
78934
79412
  scanPhaseDuration: "scan.phase_duration",
78935
79413
  scanFiles: "scan.files",
78936
79414
  scanScore: "scan.score",
79415
+ scanScoreRetry: "scan.score_retry",
78937
79416
  scanClean: "scan.clean",
78938
79417
  scanCheckSkipped: "scan.check_skipped",
79418
+ scanProjectSkipped: "scan.project_skipped",
79419
+ scanWorkspaceDeadCodeShared: "scan.workspace_deadcode_shared",
78939
79420
  scanNoReactDetected: "scan.no_react_detected",
78940
79421
  baselineDegraded: "baseline.degraded",
78941
79422
  ruleFired: "rule.fired",
@@ -78962,9 +79443,14 @@ const METRIC = {
78962
79443
  lspScanCompleted: "lsp.scan.completed",
78963
79444
  lspScanDuration: "lsp.scan.duration",
78964
79445
  lspScanDiagnostics: "lsp.scan.diagnostics",
78965
- tuiActionMenuOpened: "tui.action_menu_opened",
78966
79446
  tuiCompactReportShown: "tui.compact_report_shown",
78967
79447
  tuiFindingNavigated: "tui.finding_navigated",
79448
+ tuiIssueStreamShown: "tui.issue_stream_shown",
79449
+ tuiProjectPathContextShown: "tui.project_path_context_shown",
79450
+ tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
79451
+ tuiReportActionSelected: "tui.report_action_selected",
79452
+ tuiCancelled: "tui.cancelled",
79453
+ tuiScanInlineShown: "tui.scan_inline_shown",
78968
79454
  tuiStackedReportCapped: "tui.stacked_report_capped",
78969
79455
  aiTrainingWarningShown: "ai.training.warning_shown",
78970
79456
  jsonOutUsed: "json.out_used"
@@ -79010,7 +79496,7 @@ const makeNoopConsole = () => ({
79010
79496
  });
79011
79497
  //#endregion
79012
79498
  //#region src/cli/utils/version.ts
79013
- const VERSION = "0.9.3";
79499
+ const VERSION = "0.9.4-dev.25dbf6d";
79014
79500
  //#endregion
79015
79501
  //#region src/cli/utils/json-mode.ts
79016
79502
  let context = null;
@@ -79054,9 +79540,14 @@ const enableJsonMode = ({ compact, directory, outputFile }) => {
79054
79540
  directory,
79055
79541
  startTime: performance$1.now(),
79056
79542
  mode: "full",
79057
- outputFile: outputFile ?? null
79543
+ outputFile: null
79058
79544
  };
79059
79545
  installSilentConsole();
79546
+ if (outputFile) {
79547
+ const resolvedOutputFile = Path.resolve(outputFile);
79548
+ if (NFS.existsSync(resolvedOutputFile) && !NFS.statSync(resolvedOutputFile).isFile()) throw new CliInputError(`--json-out must point to a file, not ${resolvedOutputFile}.`);
79549
+ context.outputFile = outputFile;
79550
+ }
79060
79551
  };
79061
79552
  const isJsonModeActive = () => context !== null;
79062
79553
  const setJsonReportDirectory = (directory) => {
@@ -79247,6 +79738,104 @@ const buildSentryScope = (runContext = buildRunContext()) => {
79247
79738
  };
79248
79739
  };
79249
79740
  //#endregion
79741
+ //#region src/cli/utils/is-env-flag-enabled.ts
79742
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
79743
+ //#endregion
79744
+ //#region src/cli/utils/is-environment-error.ts
79745
+ const ENVIRONMENT_ERROR_CODES = new Set([
79746
+ "EACCES",
79747
+ "EIO",
79748
+ "ENOSPC",
79749
+ "ENOTDIR",
79750
+ "EPERM",
79751
+ "EROFS"
79752
+ ]);
79753
+ const ENVIRONMENT_FILESYSTEM_SYSCALLS = new Set([
79754
+ "lstat",
79755
+ "open",
79756
+ "read",
79757
+ "readdir",
79758
+ "readlink",
79759
+ "realpath",
79760
+ "scandir",
79761
+ "stat"
79762
+ ]);
79763
+ const TRANSIENT_FILESYSTEM_ERROR_CODES = new Set([
79764
+ "EBUSY",
79765
+ "ETIMEDOUT",
79766
+ "UNKNOWN"
79767
+ ]);
79768
+ const findEnvironmentError = (error) => {
79769
+ const pendingErrors = [error];
79770
+ const visitedErrors = /* @__PURE__ */ new Set();
79771
+ while (pendingErrors.length > 0) {
79772
+ const currentError = pendingErrors.pop();
79773
+ if (typeof currentError !== "object" || currentError === null) continue;
79774
+ if (visitedErrors.has(currentError)) continue;
79775
+ visitedErrors.add(currentError);
79776
+ if (isErrnoException(currentError) && typeof currentError.code === "string") {
79777
+ const syscall = currentError.syscall ?? "";
79778
+ if (ENVIRONMENT_ERROR_CODES.has(currentError.code) || currentError.code === "ENOENT" && syscall.startsWith("spawn") || TRANSIENT_FILESYSTEM_ERROR_CODES.has(currentError.code) && ENVIRONMENT_FILESYSTEM_SYSCALLS.has(syscall)) return currentError;
79779
+ }
79780
+ if (currentError instanceof Error && currentError.cause !== void 0) pendingErrors.push(currentError.cause);
79781
+ if (isReactDoctorError(currentError) && "cause" in currentError.reason) pendingErrors.push(currentError.reason.cause);
79782
+ }
79783
+ return null;
79784
+ };
79785
+ const isEnvironmentError = (error) => findEnvironmentError(error) !== null;
79786
+ const formatEnvironmentError = (error) => {
79787
+ const environmentError = findEnvironmentError(error);
79788
+ if (!environmentError) return messageFromUnknown(error);
79789
+ switch (environmentError.code) {
79790
+ case "EBUSY": return environmentError.path ? `Resource busy or locked at ${environmentError.path}. Close the process using it and try again.` : "Resource busy or locked. Close the process using it and try again.";
79791
+ case "ENOSPC": return "No space left on device. Free up disk space and try again.";
79792
+ case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
79793
+ case "EROFS": return "Read-only filesystem: cannot write to this location.";
79794
+ case "EACCES":
79795
+ case "EPERM": return environmentError.path ? `Permission denied accessing ${environmentError.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
79796
+ case "ENOTDIR": return environmentError.path ? `A file exists at ${environmentError.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
79797
+ case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
79798
+ case "ETIMEDOUT": return environmentError.path ? `Timed out accessing ${environmentError.path}. Check the network or cloud drive and try again.` : "Filesystem access timed out. Check the network or cloud drive and try again.";
79799
+ case "UNKNOWN": return environmentError.path ? `The operating system could not access ${environmentError.path}. Check the filesystem and try again.` : "The operating system could not access the filesystem. Check it and try again.";
79800
+ default: return environmentError.message;
79801
+ }
79802
+ };
79803
+ //#endregion
79804
+ //#region src/cli/utils/is-expected-user-error.ts
79805
+ /**
79806
+ * Whether `error` is an expected, user-actionable failure — the user's project
79807
+ * or input, not a react-doctor bug. Such failures must be kept out of crash
79808
+ * reporting (Sentry + the alertable error-rate metric) and rendered via
79809
+ * `handleUserError` (a plain message — no "Something went wrong", prefilled
79810
+ * issue, Discord link, or Sentry reference), since there is no bug to report.
79811
+ *
79812
+ * Four distinct shapes reach the CLI's catch blocks:
79813
+ *
79814
+ * - **Project-discovery failures** (`NoReactDependencyError`,
79815
+ * `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
79816
+ * `AmbiguousProjectError`) arrive as their plain legacy classes (so
79817
+ * `isReactDoctorError` is `false` for them) — narrow with
79818
+ * `isProjectDiscoveryError`. Running react-doctor against a directory that
79819
+ * has no React, or a path that doesn't exist, is the canonical example.
79820
+ * - **CLI invocation mistakes** (`CliInputError`): a malformed
79821
+ * `<file>:<line>` argument, mutually exclusive flags, or an unknown
79822
+ * `--project` name.
79823
+ * - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
79824
+ * stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
79825
+ * - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
79826
+ * `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
79827
+ * full / failing / read-only, permission denied, or a path blocked by a
79828
+ * file. React Doctor cannot fix the user's environment; exit cleanly with an
79829
+ * actionable message instead of crashing. See `is-environment-error.ts` for
79830
+ * why the set stays narrow (codes that usually mean our bug keep reaching
79831
+ * Sentry).
79832
+ *
79833
+ * This composes the existing core narrowers rather than introducing a new
79834
+ * error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
79835
+ * knowledge of the `ReactDoctorError` shape.
79836
+ */
79837
+ const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
79838
+ //#endregion
79250
79839
  //#region src/cli/utils/anonymize-text.ts
79251
79840
  /**
79252
79841
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -79368,13 +79957,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
79368
79957
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
79369
79958
  * standard `SENTRY_RELEASE` override.
79370
79959
  */
79371
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.3`;
79960
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.4-dev.25dbf6d`;
79372
79961
  /**
79373
79962
  * Deployment environment shown in Sentry's environment filter. Defaults to
79374
79963
  * `production` for tagged releases and `development` for dev/unbuilt versions,
79375
79964
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
79376
79965
  */
79377
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.3") ? "development" : "production");
79966
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.4-dev.25dbf6d") ? "development" : "production");
79378
79967
  /**
79379
79968
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
79380
79969
  * (set to `0` to disable tracing) and falls back to
@@ -79397,7 +79986,6 @@ const shouldEnableSentry = () => {
79397
79986
  if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
79398
79987
  return true;
79399
79988
  };
79400
- const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
79401
79989
  /**
79402
79990
  * Whether performance traces will actually be recorded — Sentry is live and the
79403
79991
  * resolved sample rate is above zero. Used to gate the per-run root span and
@@ -79440,21 +80028,25 @@ const initializeSentry = () => {
79440
80028
  isInitialized = true;
79441
80029
  resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
79442
80030
  const { tags, contexts } = buildSentryScope();
79443
- Sentry.init({
79444
- dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
79445
- release: resolveSentryRelease(),
79446
- environment: resolveSentryEnvironment(),
79447
- sendDefaultPii: false,
79448
- tracesSampleRate: resolvedTracesSampleRate,
79449
- debug: isEnvFlagEnabled(process.env.SENTRY_DEBUG),
79450
- initialScope: {
79451
- tags,
79452
- contexts
79453
- },
79454
- beforeSend: (event) => scrubSentryEvent(event),
79455
- beforeSendTransaction: (event) => scrubSentryEvent(event),
79456
- beforeSendMetric: (metric) => scrubSentryMetric(metric)
79457
- });
80031
+ try {
80032
+ Sentry.init({
80033
+ dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
80034
+ release: resolveSentryRelease(),
80035
+ environment: resolveSentryEnvironment(),
80036
+ sendDefaultPii: false,
80037
+ tracesSampleRate: resolvedTracesSampleRate,
80038
+ debug: isEnvFlagEnabled(process.env.SENTRY_DEBUG),
80039
+ initialScope: {
80040
+ tags,
80041
+ contexts
80042
+ },
80043
+ beforeSend: (event, hint) => isExpectedUserError(hint.originalException) ? null : scrubSentryEvent(event),
80044
+ beforeSendTransaction: (event) => scrubSentryEvent(event),
80045
+ beforeSendMetric: (metric) => scrubSentryMetric(metric)
80046
+ });
80047
+ } catch {
80048
+ resolvedTracesSampleRate = 0;
80049
+ }
79458
80050
  };
79459
80051
  //#endregion
79460
80052
  //#region src/cli/utils/cli-logger.ts
@@ -83013,27 +83605,21 @@ const prompts = (questions, options = {}) => {
83013
83605
  //#endregion
83014
83606
  //#region src/cli/utils/to-span-attributes.ts
83015
83607
  /**
83016
- * Converts a Sentry tag map (which permits `null` to denote an absent signal)
83017
- * into Sentry/OTel span attributes, which only accept primitives. `null` values
83018
- * are dropped rather than coerced, so an absent signal doesn't become a
83019
- * misleading `"null"` attribute.
83608
+ * Converts a Sentry tag map (which permits `null`/`undefined` to denote an
83609
+ * absent signal) into Sentry/OTel span attributes, which only accept primitives.
83610
+ * Missing values are dropped rather than coerced, so an absent signal doesn't
83611
+ * become a misleading `"null"` or `"undefined"` attribute.
83020
83612
  */
83021
83613
  const toSpanAttributes = (tags) => {
83022
83614
  const attributes = {};
83023
- for (const [key, value] of Object.entries(tags)) if (value !== null) attributes[key] = value;
83615
+ for (const [key, value] of Object.entries(tags)) if (value != null) attributes[key] = value;
83024
83616
  return attributes;
83025
83617
  };
83026
83618
  //#endregion
83027
83619
  //#region src/cli/utils/record-metric.ts
83028
- const cleanAttributes = (attributes) => {
83029
- const cleaned = {};
83030
- if (!attributes) return cleaned;
83031
- for (const [key, value] of Object.entries(attributes)) if (value !== null && value !== void 0) cleaned[key] = value;
83032
- return cleaned;
83033
- };
83034
83620
  const withRunAttributes = (attributes) => ({
83035
83621
  ...toSpanAttributes(buildSentryScope().tags),
83036
- ...cleanAttributes(attributes)
83622
+ ...toSpanAttributes(attributes ?? {})
83037
83623
  });
83038
83624
  /**
83039
83625
  * Emits a Sentry counter. A guarded, swallow-on-throw no-op unless the CLI's
@@ -90775,34 +91361,6 @@ const runCiConfig = async (options = {}) => {
90775
91361
  });
90776
91362
  };
90777
91363
  //#endregion
90778
- //#region src/cli/utils/is-environment-error.ts
90779
- const ENVIRONMENT_ERROR_CODES = new Set([
90780
- "ENOSPC",
90781
- "EIO",
90782
- "EROFS",
90783
- "EACCES",
90784
- "EPERM",
90785
- "ENOTDIR"
90786
- ]);
90787
- const isEnvironmentError = (error) => {
90788
- if (!isErrnoException(error) || typeof error.code !== "string") return false;
90789
- if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
90790
- return ENVIRONMENT_ERROR_CODES.has(error.code);
90791
- };
90792
- const formatEnvironmentError = (error) => {
90793
- if (!isErrnoException(error)) return messageFromUnknown(error);
90794
- switch (error.code) {
90795
- case "ENOSPC": return "No space left on device. Free up disk space and try again.";
90796
- case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
90797
- case "EROFS": return "Read-only filesystem: cannot write to this location.";
90798
- case "EACCES":
90799
- case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
90800
- case "ENOTDIR": return error.path ? `A file exists at ${error.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
90801
- case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
90802
- default: return error.message;
90803
- }
90804
- };
90805
- //#endregion
90806
91364
  //#region src/cli/utils/handle-error.ts
90807
91365
  const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
90808
91366
  const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
@@ -90904,56 +91462,6 @@ const handleUserError = (error, options = {}) => {
90904
91462
  process.exitCode = 1;
90905
91463
  };
90906
91464
  //#endregion
90907
- //#region src/cli/utils/cli-input-error.ts
90908
- /**
90909
- * A mistake in *how the CLI was invoked* — a malformed `<file>:<line>`
90910
- * argument, mutually exclusive flags, or an unknown `--project` name. This is
90911
- * user input, not a react-doctor bug, so `isExpectedUserError` routes it
90912
- * through `handleUserError`: a clean, single-line message with no Sentry
90913
- * report and no "Something went wrong, open a prefilled issue" block.
90914
- *
90915
- * The `message` is rendered verbatim, so it must read as a complete,
90916
- * human-readable sentence that tells the user how to fix their invocation.
90917
- */
90918
- var CliInputError = class extends Error {
90919
- name = "CliInputError";
90920
- };
90921
- //#endregion
90922
- //#region src/cli/utils/is-expected-user-error.ts
90923
- /**
90924
- * Whether `error` is an expected, user-actionable failure — the user's project
90925
- * or input, not a react-doctor bug. Such failures must be kept out of crash
90926
- * reporting (Sentry + the alertable error-rate metric) and rendered via
90927
- * `handleUserError` (a plain message — no "Something went wrong", prefilled
90928
- * issue, Discord link, or Sentry reference), since there is no bug to report.
90929
- *
90930
- * Four distinct shapes reach the CLI's catch blocks:
90931
- *
90932
- * - **Project-discovery failures** (`NoReactDependencyError`,
90933
- * `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
90934
- * `AmbiguousProjectError`) arrive as their plain legacy classes (so
90935
- * `isReactDoctorError` is `false` for them) — narrow with
90936
- * `isProjectDiscoveryError`. Running react-doctor against a directory that
90937
- * has no React, or a path that doesn't exist, is the canonical example.
90938
- * - **CLI invocation mistakes** (`CliInputError`): a malformed
90939
- * `<file>:<line>` argument, mutually exclusive flags, or an unknown
90940
- * `--project` name.
90941
- * - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
90942
- * stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
90943
- * - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
90944
- * `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
90945
- * full / failing / read-only, permission denied, or a path blocked by a
90946
- * file. React Doctor cannot fix the user's environment; exit cleanly with an
90947
- * actionable message instead of crashing. See `is-environment-error.ts` for
90948
- * why the set stays narrow (codes that usually mean our bug keep reaching
90949
- * Sentry).
90950
- *
90951
- * This composes the existing core narrowers rather than introducing a new
90952
- * error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
90953
- * knowledge of the `ReactDoctorError` shape.
90954
- */
90955
- const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
90956
- //#endregion
90957
91465
  //#region src/cli/utils/print-branded-header.ts
90958
91466
  /**
90959
91467
  * Single branded line every command prints first when not in JSON
@@ -91050,6 +91558,19 @@ const ciInstallAction = (options) => runCiCommand("install", runCiInstall, optio
91050
91558
  const ciUpgradeAction = (options) => runCiCommand("upgrade", runCiUpgrade, options);
91051
91559
  const ciConfigAction = (options) => runCiCommand("config", runCiConfig, options);
91052
91560
  //#endregion
91561
+ //#region src/cli/utils/active-scan-abort-registry.ts
91562
+ const activeScanAbortControllers = /* @__PURE__ */ new Set();
91563
+ const activeScanAbortRegistry = {
91564
+ register: (controller) => {
91565
+ activeScanAbortControllers.add(controller);
91566
+ return () => activeScanAbortControllers.delete(controller);
91567
+ },
91568
+ abortAll: () => {
91569
+ for (const controller of activeScanAbortControllers) controller.abort();
91570
+ activeScanAbortControllers.clear();
91571
+ }
91572
+ };
91573
+ //#endregion
91053
91574
  //#region src/cli/utils/sentry-tracer.ts
91054
91575
  const toHrTime = (epochNanoseconds) => [Number(epochNanoseconds / NANOSECONDS_PER_SECOND), Number(epochNanoseconds % NANOSECONDS_PER_SECOND)];
91055
91576
  const SPAN_KIND_TO_OTEL = {
@@ -91466,7 +91987,14 @@ const isInspectResultComplete = (result) => isScanComplete({
91466
91987
  skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
91467
91988
  });
91468
91989
  //#endregion
91990
+ //#region src/cli/utils/resolve-score-unavailable-reason.ts
91991
+ const resolveScoreUnavailableReason = (input) => {
91992
+ if (input.isScoreDisabled) return "disabled";
91993
+ return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
91994
+ };
91995
+ //#endregion
91469
91996
  //#region src/cli/utils/scan-result-cache.ts
91997
+ const createScanResultCacheInvocationState = () => ({ repositoryIdentityByRoot: /* @__PURE__ */ new Map() });
91470
91998
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
91471
91999
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
91472
92000
  "oxlint/package.json",
@@ -91551,7 +92079,7 @@ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
91551
92079
  if (stats.size > 10485760) return null;
91552
92080
  return hashFileContents(absolutePath);
91553
92081
  };
91554
- const buildWorktreeFingerprint = (projectDirectory) => {
92082
+ const buildWorktreeFingerprint = (projectDirectory, repositoryRoot) => {
91555
92083
  const statusOutput = runGit(projectDirectory, [
91556
92084
  "status",
91557
92085
  "--porcelain=v1",
@@ -91563,8 +92091,6 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91563
92091
  const statusEntries = parseWorktreeStatusRecords(statusOutput);
91564
92092
  if (statusEntries === null) return null;
91565
92093
  if (statusEntries.length > 300) return null;
91566
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
91567
- if (repositoryRoot === null) return null;
91568
92094
  const dirtyEntries = [];
91569
92095
  for (const statusEntry of statusEntries) {
91570
92096
  const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
@@ -91580,6 +92106,20 @@ const buildWorktreeFingerprint = (projectDirectory) => {
91580
92106
  return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
91581
92107
  });
91582
92108
  };
92109
+ const resolveRepositoryCacheIdentity = (projectDirectory, invocationState) => {
92110
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
92111
+ if (repositoryRoot === null) return null;
92112
+ const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
92113
+ if (cachedIdentity !== void 0) return cachedIdentity;
92114
+ const worktreeFingerprint = buildWorktreeFingerprint(projectDirectory, repositoryRoot);
92115
+ const headSha = readHeadSha(repositoryRoot);
92116
+ const identity = worktreeFingerprint === null || headSha === null ? null : {
92117
+ headSha,
92118
+ worktreeFingerprint
92119
+ };
92120
+ invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
92121
+ return identity;
92122
+ };
91583
92123
  const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
91584
92124
  const resolveDotenvFingerprint = (projectDirectory) => {
91585
92125
  try {
@@ -91674,16 +92214,14 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
91674
92214
  const buildScanResultCacheKey = (input) => {
91675
92215
  if (isCacheGloballyDisabled()) return null;
91676
92216
  if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
91677
- const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
91678
- if (worktreeFingerprint === null) return null;
91679
- const headSha = readHeadSha(input.projectDirectory);
91680
- if (headSha === null) return null;
92217
+ const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
92218
+ if (repositoryIdentity === null) return null;
91681
92219
  if (stringifyStableJson(input.userConfig) === null) return null;
91682
92220
  const cacheKeyJson = stringifyStableJson({
91683
92221
  schemaVersion: 6,
91684
92222
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
91685
- headSha,
91686
- worktreeFingerprint,
92223
+ headSha: repositoryIdentity.headSha,
92224
+ worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
91687
92225
  dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
91688
92226
  reactDoctorVersion: input.version,
91689
92227
  nodeVersion: process.version,
@@ -91697,6 +92235,8 @@ const buildScanResultCacheKey = (input) => {
91697
92235
  deadCode: input.options.deadCode,
91698
92236
  supplyChain: input.options.supplyChain,
91699
92237
  includePaths: [...input.options.includePaths].sort(),
92238
+ excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
92239
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
91700
92240
  customRulesOnly: input.options.customRulesOnly,
91701
92241
  respectInlineDisables: input.options.respectInlineDisables,
91702
92242
  warnings: input.options.warnings,
@@ -91774,8 +92314,8 @@ const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g
91774
92314
  * browser and stay group-/filter-/aggregate-able in the Spans dataset.
91775
92315
  *
91776
92316
  * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
91777
- * numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
91778
- * `toSpanAttributes` can drop absent signals rather than coerce them).
92317
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
92318
+ * so `toSpanAttributes` can drop absent signals rather than coerce them).
91779
92319
  */
91780
92320
  const withNamespace = (namespace, attributes) => {
91781
92321
  const namespaced = {};
@@ -91841,7 +92381,7 @@ const buildCacheAttributes = (input) => {
91841
92381
  const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
91842
92382
  const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
91843
92383
  return withNamespace("cache", {
91844
- wholeRepoHit: input.wholeRepoCacheHit ?? null,
92384
+ wholeRepoHit: input.wholeRepoCacheHit,
91845
92385
  temperature,
91846
92386
  warmth
91847
92387
  });
@@ -91963,33 +92503,37 @@ const buildOutcomeAttributes = (input) => {
91963
92503
  ...withNamespace("score", {
91964
92504
  value: result.score ? result.score.score : null,
91965
92505
  label: result.score ? result.score.label : null,
91966
- available: result.score !== null
92506
+ available: result.score !== null,
92507
+ unavailableReason: result.score === null ? resolveScoreUnavailableReason({
92508
+ isScoreDisabled: input.noScore,
92509
+ isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
92510
+ }) : null
91967
92511
  }),
91968
92512
  ...withNamespace("lint", {
91969
- failed: input.didLintFail ?? null,
91970
- failureReasonKind: input.lintFailureReasonKind ?? null,
91971
- partialFailureCount: input.lintPartialFailureCount ?? null,
91972
- droppedFileCount: input.lintDroppedFileCount ?? null,
91973
- deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
91974
- cacheHitFiles: result.lintCacheHitFileCount ?? null,
91975
- cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
92513
+ failed: input.didLintFail,
92514
+ failureReasonKind: input.lintFailureReasonKind,
92515
+ partialFailureCount: input.lintPartialFailureCount,
92516
+ droppedFileCount: input.lintDroppedFileCount,
92517
+ deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
92518
+ cacheHitFiles: result.lintCacheHitFileCount,
92519
+ cacheTotalFiles: result.lintCacheTotalFileCount,
91976
92520
  cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
91977
- sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
91978
- sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
92521
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
92522
+ sidecarTotalFiles: result.lintSidecarTotalFileCount,
91979
92523
  sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
91980
92524
  }),
91981
92525
  ...withNamespace("deadCode", {
91982
- failed: input.didDeadCodeFail ?? null,
91983
- overlapped: input.deadCodeOverlapped ?? null,
91984
- cacheHit: result.deadCodeCacheHit ?? null,
91985
- summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
91986
- summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
92526
+ failed: input.didDeadCodeFail,
92527
+ overlapped: input.deadCodeOverlapped,
92528
+ cacheHit: result.deadCodeCacheHit,
92529
+ summaryCacheHits: result.deadCodeSummaryCacheHits,
92530
+ summaryCacheMisses: result.deadCodeSummaryCacheMisses
91987
92531
  }),
91988
- ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
91989
- ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
92532
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
92533
+ ...withNamespace("securityScan", { failed: input.securityScanFailed }),
91990
92534
  ...withNamespace("timing", {
91991
92535
  elapsedMs: result.elapsedMilliseconds,
91992
- scanMs: result.scanElapsedMilliseconds ?? null
92536
+ scanMs: result.scanElapsedMilliseconds
91993
92537
  }),
91994
92538
  ...withNamespace("migration", {
91995
92539
  largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
@@ -92001,7 +92545,7 @@ const buildOutcomeAttributes = (input) => {
92001
92545
  new: summary.totalDiagnosticCount,
92002
92546
  fixed: result.baselineDelta.fixedCount,
92003
92547
  baseTotal: result.baselineDelta.baseTotalCount,
92004
- crossFileMatches: result.baselineDelta.crossFileMatchCount ?? null,
92548
+ crossFileMatches: result.baselineDelta.crossFileMatchCount,
92005
92549
  degraded: false
92006
92550
  }));
92007
92551
  else if (input.gateExempt) attributes["baseline.degraded"] = true;
@@ -92010,7 +92554,7 @@ const buildOutcomeAttributes = (input) => {
92010
92554
  const buildActionAttributes = () => {
92011
92555
  const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
92012
92556
  return withNamespace("action", {
92013
- actorAssociation: githubActorAssociation ?? null,
92557
+ actorAssociation: githubActorAssociation,
92014
92558
  runnerOs: detectRunnerOs(),
92015
92559
  comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
92016
92560
  reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
@@ -92024,7 +92568,7 @@ const buildScanAttributes = (input) => {
92024
92568
  mode: input.mode,
92025
92569
  scope: input.scope,
92026
92570
  parallel: input.parallel,
92027
- workerCount: input.workerCount ?? null,
92571
+ workerCount: input.workerCount,
92028
92572
  maxDurationMs: input.maxDurationMs,
92029
92573
  lint: input.lint,
92030
92574
  deadCode: input.deadCode,
@@ -92038,10 +92582,10 @@ const buildScanAttributes = (input) => {
92038
92582
  hasCustomConfig: input.hasCustomConfig,
92039
92583
  rulesConfigured: ruleKeys.length,
92040
92584
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
92041
- fileCount: input.result?.scannedFileCount ?? null,
92042
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length ?? null,
92043
- htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length ?? null,
92044
- multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length ?? null
92585
+ fileCount: input.result?.scannedFileCount,
92586
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
92587
+ htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
92588
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
92045
92589
  });
92046
92590
  };
92047
92591
  /**
@@ -92053,11 +92597,11 @@ const buildScanAttributes = (input) => {
92053
92597
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
92054
92598
  * in the Spans dataset. Pure and exported so the projection (outcome
92055
92599
  * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
92056
- * without a live Sentry client. `null` values are dropped so absent signals
92057
- * never become misleading `"null"` attributes. The run + project base context
92058
- * (version, command, ci/provider, framework, …) is already on the span from
92059
- * `withSentryRunSpan` / `recordSentryProjectContext`, so this adds only what
92060
- * those don't carry.
92600
+ * without a live Sentry client. `null`/`undefined` values are dropped so absent
92601
+ * signals never become misleading `"null"`/`"undefined"` attributes. The run +
92602
+ * project base context (version, command, ci/provider, framework, …) is already
92603
+ * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
92604
+ * adds only what those don't carry.
92061
92605
  */
92062
92606
  const buildRunEventAttributes = (input) => toSpanAttributes({
92063
92607
  ...buildScanAttributes(input),
@@ -92531,8 +93075,35 @@ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
92531
93075
  //#endregion
92532
93076
  //#region src/cli/utils/build-no-score-message.ts
92533
93077
  const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
92534
- const buildNoScoreMessage = (isScoreDisabled, disabledMessage) => {
92535
- return `${isScoreDisabled ? disabledMessage ?? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
93078
+ const buildNoScoreMessage = (input) => {
93079
+ const unavailableReason = resolveScoreUnavailableReason({
93080
+ isScoreDisabled: input.isScoreDisabled,
93081
+ isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
93082
+ });
93083
+ let reason;
93084
+ switch (unavailableReason) {
93085
+ case "disabled":
93086
+ reason = input.disabledMessage ?? "Score disabled by --no-score.";
93087
+ break;
93088
+ case "analysis-incomplete":
93089
+ reason = "Score not shown because lint or dead-code analysis could not complete.";
93090
+ break;
93091
+ case "api-unavailable":
93092
+ reason = "Score unavailable (could not reach the score API).";
93093
+ break;
93094
+ }
93095
+ return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
93096
+ };
93097
+ //#endregion
93098
+ //#region src/cli/utils/has-incomplete-score-analysis.ts
93099
+ const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
93100
+ //#endregion
93101
+ //#region src/cli/utils/build-empty-report-message.ts
93102
+ const buildEmptyReportMessage = (input) => {
93103
+ const categoryFilters = [...input.categoryFilters];
93104
+ if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
93105
+ if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
93106
+ return "No issues found!";
92536
93107
  };
92537
93108
  //#endregion
92538
93109
  //#region src/cli/utils/render-agent-guidance.ts
@@ -93279,9 +93850,6 @@ const markOnboardingComplete = (options = {}) => {
93279
93850
  if (isGatePending(ONBOARDING_GATE, {}, options)) recordGate(ONBOARDING_GATE, {}, options);
93280
93851
  };
93281
93852
  //#endregion
93282
- //#region src/cli/utils/render-project-detection.ts
93283
- const printProjectDetection = (_input) => void_;
93284
- //#endregion
93285
93853
  //#region src/cli/utils/colorize-by-score.ts
93286
93854
  const colorizeByScore = (text, score) => {
93287
93855
  if (score >= 75) return highlighter.success(text);
@@ -93495,6 +94063,26 @@ const printNoScoreHeader = (noScoreMessage) => gen(function* () {
93495
94063
  yield* log("");
93496
94064
  });
93497
94065
  //#endregion
94066
+ //#region src/cli/utils/build-footer-description-lines.ts
94067
+ const FOOTER_DESCRIPTION_INDENT = " ";
94068
+ const buildFooterDescriptionLines = (description) => {
94069
+ return wrapTextToWidth(description, resolveMeasureWidth(2)).map((line) => highlighter.dim(`${FOOTER_DESCRIPTION_INDENT}${line}`));
94070
+ };
94071
+ //#endregion
94072
+ //#region src/cli/utils/build-footer-link-lines.ts
94073
+ const buildFooterLinkLines = ({ shareUrl }) => [
94074
+ ...shareUrl === null ? [] : [
94075
+ ` ${highlighter.bold("Share:")} ${highlighter.info(shareUrl)}`,
94076
+ ...buildFooterDescriptionLines("Tell others how you did on socials"),
94077
+ ""
94078
+ ],
94079
+ ` ${highlighter.bold("Docs:")} ${highlighter.info(DOCS_URL)}`,
94080
+ ...buildFooterDescriptionLines("Learn more about fixing issues, setting up CI/CD, and configuring rules with a config file"),
94081
+ "",
94082
+ ` ${highlighter.bold("GitHub:")} ${highlighter.info(CANONICAL_GITHUB_URL)}`,
94083
+ ...buildFooterDescriptionLines("Report issues and star the repository!")
94084
+ ];
94085
+ //#endregion
93498
94086
  //#region src/cli/utils/write-diagnostics-directory.ts
93499
94087
  const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
93500
94088
  const readPreviousRuleDumpFileNames = (directory) => {
@@ -93515,11 +94103,6 @@ const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
93515
94103
  };
93516
94104
  //#endregion
93517
94105
  //#region src/cli/utils/render-summary.ts
93518
- const FOOTER_DESCRIPTION_INDENT = " ";
93519
- const printFooterDescription = (description) => gen(function* () {
93520
- const wrapWidth = resolveMeasureWidth(2);
93521
- for (const line of wrapTextToWidth(description, wrapWidth)) yield* log(highlighter.dim(`${FOOTER_DESCRIPTION_INDENT}${line}`));
93522
- });
93523
94106
  const buildShareUrl = (diagnostics, scoreResult, projectName) => {
93524
94107
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
93525
94108
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
@@ -93536,17 +94119,8 @@ const printFooter = (input) => gen(function* () {
93536
94119
  yield* log("");
93537
94120
  yield* log(buildSectionDivider());
93538
94121
  yield* log("");
93539
- if (!input.isOffline) {
93540
- const shareUrl = buildShareUrl(input.diagnostics, input.scoreResult, input.projectName);
93541
- yield* log(` ${highlighter.bold("Share:")} ${highlighter.info(shareUrl)}`);
93542
- yield* printFooterDescription("Tell others how you did on socials");
93543
- yield* log("");
93544
- }
93545
- yield* log(` ${highlighter.bold("Docs:")} ${highlighter.info(DOCS_URL)}`);
93546
- yield* printFooterDescription("Learn more about fixing issues, setting up CI/CD, and configuring rules with a config file");
93547
- yield* log("");
93548
- yield* log(` ${highlighter.bold("GitHub:")} ${highlighter.info(CANONICAL_GITHUB_URL)}`);
93549
- yield* printFooterDescription("Report issues and star the repository!");
94122
+ const shareUrl = input.isOffline ? null : buildShareUrl(input.diagnostics, input.scoreResult, input.projectName);
94123
+ for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
93550
94124
  });
93551
94125
  const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
93552
94126
  const writtenDirectory = yield* try_({
@@ -93643,9 +94217,6 @@ const resolveCliCategories = (categoryFlag) => {
93643
94217
  //#endregion
93644
94218
  //#region src/inspect.ts
93645
94219
  const silentConsole = makeNoopConsole();
93646
- const runConsole = (effect) => {
93647
- runSync(effect);
93648
- };
93649
94220
  const recordOnboardingCompletion = (options) => {
93650
94221
  const forceOnboarding = isOnboardingForced();
93651
94222
  if (shouldRecordOnboarding({
@@ -93655,7 +94226,6 @@ const recordOnboardingCompletion = (options) => {
93655
94226
  isNonInteractiveEnvironment: options.isNonInteractiveEnvironment
93656
94227
  })) markOnboardingComplete();
93657
94228
  };
93658
- const formatCategorySelection = (categoryFilters) => [...categoryFilters].join(", ");
93659
94229
  const buildChangedLineMatcher = (directory, changedLineRanges) => {
93660
94230
  const rangesByFile = /* @__PURE__ */ new Map();
93661
94231
  for (const entry of changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
@@ -93705,7 +94275,10 @@ const mergeInspectOptions = (inputOptions, userConfig) => {
93705
94275
  maxDurationMs: inputOptions.maxDurationMs ?? null,
93706
94276
  baseline: inputOptions.baseline ?? null,
93707
94277
  changedLineRanges: inputOptions.changedLineRanges ?? null,
93708
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false
94278
+ supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
94279
+ excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
94280
+ retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
94281
+ precomputedSourceFileCount: inputOptions.precomputedSourceFileCount
93709
94282
  };
93710
94283
  };
93711
94284
  const deriveScope = (options) => {
@@ -93738,13 +94311,14 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
93738
94311
  const deadlineEpochMs = inputOptions.deadlineEpochMs ?? (inputOptions.maxDurationMs != null ? Date.now() + inputOptions.maxDurationMs : null);
93739
94312
  const isConcurrentScan = inputOptions.concurrentScan === true;
93740
94313
  if (!isConcurrentScan) resetSentryRunState();
93741
- const hasConfigOverride = inputOptions.configOverride !== void 0;
94314
+ const configOverride = inputOptions.configOverride;
94315
+ const hasConfigOverride = configOverride !== void 0;
93742
94316
  let scanDirectory;
93743
94317
  let userConfig;
93744
94318
  let configSourceDirectory;
93745
94319
  if (hasConfigOverride) {
93746
94320
  scanDirectory = directory;
93747
- userConfig = inputOptions.configOverride ?? null;
94321
+ userConfig = configOverride;
93748
94322
  configSourceDirectory = inputOptions.configSourceDirectory ?? null;
93749
94323
  } else {
93750
94324
  const scanTarget = await resolveScanTarget(directory);
@@ -93777,11 +94351,22 @@ const inspectWithOxlintRuntime = async (directory, inputOptions, oxlintRuntime)
93777
94351
  };
93778
94352
  const createInvocationInspect = (requestedOxlintConcurrency) => {
93779
94353
  const concurrency = resolveScanConcurrency(requestedOxlintConcurrency ?? runSync(OxlintConcurrency));
93780
- const oxlintRuntime = {
93781
- concurrency,
93782
- spawnSlots: createOxlintSpawnSlots(concurrency)
94354
+ const spawnSlots = createOxlintSpawnSlots(concurrency);
94355
+ const scanResultCacheInvocationState = createScanResultCacheInvocationState();
94356
+ return async (directory, inputOptions = {}) => {
94357
+ const abortController = new AbortController();
94358
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94359
+ try {
94360
+ return await inspectWithOxlintRuntime(directory, inputOptions, {
94361
+ concurrency,
94362
+ spawnSlots,
94363
+ abortSignal: abortController.signal,
94364
+ scanResultCacheInvocationState
94365
+ });
94366
+ } finally {
94367
+ unregisterAbortController();
94368
+ }
93783
94369
  };
93784
- return (directory, inputOptions = {}) => inspectWithOxlintRuntime(directory, inputOptions, oxlintRuntime);
93785
94370
  };
93786
94371
  const inspect = async (directory, inputOptions = {}) => createInvocationInspect(inputOptions.concurrency)(directory, inputOptions);
93787
94372
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
@@ -93794,12 +94379,27 @@ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(
93794
94379
  */
93795
94380
  const runBaselineComparison = async (params) => {
93796
94381
  const tempDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
94382
+ const baselineIncludePaths = filterPathsOutsideDirectories({
94383
+ rootDirectory: params.directory,
94384
+ relativePaths: params.options.includePaths,
94385
+ excludedDirectories: params.options.excludedProjectDirectories
94386
+ });
94387
+ const baselineBaseFiles = params.baseFiles ? filterPathsOutsideDirectories({
94388
+ rootDirectory: params.directory,
94389
+ relativePaths: params.baseFiles,
94390
+ excludedDirectories: params.options.excludedProjectDirectories
94391
+ }) : void 0;
94392
+ const baselineHeadFiles = params.headFiles ? filterPathsOutsideDirectories({
94393
+ rootDirectory: params.directory,
94394
+ relativePaths: params.headFiles,
94395
+ excludedDirectories: params.options.excludedProjectDirectories
94396
+ }) : void 0;
93797
94397
  const snapshot = await materializeBaselineFiles({
93798
94398
  directory: params.directory,
93799
94399
  ref: params.baselineRef,
93800
- files: params.options.includePaths,
93801
- baseFiles: params.baseFiles,
93802
- headFiles: params.headFiles,
94400
+ files: baselineIncludePaths,
94401
+ baseFiles: baselineBaseFiles,
94402
+ headFiles: baselineHeadFiles,
93803
94403
  tempDirectory
93804
94404
  }).catch((error) => {
93805
94405
  rmSync(tempDirectory, {
@@ -93821,7 +94421,7 @@ const runBaselineComparison = async (params) => {
93821
94421
  const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
93822
94422
  const trackedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
93823
94423
  const expectedHeadFiles = new Set(trackedHeadFiles);
93824
- for (const filePath of params.options.includePaths) {
94424
+ for (const filePath of baselineIncludePaths) {
93825
94425
  const normalizedFilePath = toForwardSlashes(filePath);
93826
94426
  if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
93827
94427
  }
@@ -93858,8 +94458,9 @@ const runBaselineComparison = async (params) => {
93858
94458
  resolveLocalGithubViewerPermission: false,
93859
94459
  suppressScanSummary: true,
93860
94460
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
93861
- deadlineEpochMs: params.deadlineEpochMs ?? void 0
93862
- }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
94461
+ deadlineEpochMs: params.deadlineEpochMs ?? void 0,
94462
+ signal: params.oxlintRuntime.abortSignal
94463
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))), { signal: params.oxlintRuntime.abortSignal });
93863
94464
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
93864
94465
  const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
93865
94466
  const delta = computeDiagnosticDelta({
@@ -93887,6 +94488,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93887
94488
  const isDiffMode = options.includePaths.length > 0;
93888
94489
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
93889
94490
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
94491
+ await yieldToEventLoop();
93890
94492
  const cacheKey = buildScanResultCacheKey({
93891
94493
  projectDirectory: directory,
93892
94494
  version: VERSION,
@@ -93894,19 +94496,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93894
94496
  options,
93895
94497
  userConfig,
93896
94498
  hasConfigOverride,
93897
- configSourceDirectory
94499
+ configSourceDirectory,
94500
+ invocationState: oxlintRuntime.scanResultCacheInvocationState
93898
94501
  });
93899
94502
  const scanResultCache = cacheKey === null ? null : createScanResultCache(directory);
93900
- const cachedPayload = cacheKey === null ? null : scanResultCache?.lookup(cacheKey) ?? null;
94503
+ const cachedPayload = cacheKey === null || scanResultCache === null ? null : scanResultCache.lookup(cacheKey);
93901
94504
  if (cachedPayload) {
93902
94505
  recordSentryProjectContext(cachedPayload.project, rootSentrySpan, { concurrentScan: options.concurrentScan });
93903
94506
  recordCount(METRIC.projectDetected, 1);
93904
- await renderCachedProjectDetection({
93905
- payload: cachedPayload,
93906
- options,
93907
- userConfig,
93908
- isDiffMode
93909
- });
93910
94507
  const baselineDegraded = Boolean(options.baseline) && isDiffMode && cachedPayload.baselineDelta === void 0;
93911
94508
  const result = await renderAndRecordScan({
93912
94509
  payload: cachedPayload,
@@ -93940,6 +94537,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93940
94537
  });
93941
94538
  const program = runInspect({
93942
94539
  directory,
94540
+ precomputedSourceFileCount: options.precomputedSourceFileCount,
93943
94541
  includePaths: options.includePaths,
93944
94542
  customRulesOnly: options.customRulesOnly,
93945
94543
  respectInlineDisables: options.respectInlineDisables,
@@ -93956,26 +94554,19 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
93956
94554
  resolveLocalGithubViewerPermission: !options.noScore,
93957
94555
  suppressScanSummary: options.suppressRendering,
93958
94556
  supplyChainManifestChanged: options.supplyChainManifestChanged,
93959
- concurrentScan: options.concurrentScan,
93960
- deadlineEpochMs: deadlineEpochMs ?? void 0
93961
- }, { beforeLint: (projectInfo, lintIncludePaths) => gen(function* () {
94557
+ deadlineEpochMs: deadlineEpochMs ?? void 0,
94558
+ signal: oxlintRuntime.abortSignal,
94559
+ excludedProjectDirectories: options.excludedProjectDirectories,
94560
+ retainExcludedProjectDeadCodeDiagnostics: options.retainExcludedProjectDeadCodeDiagnostics
94561
+ }, { beforeLint: (projectInfo) => sync(() => {
93962
94562
  recordSentryProjectContext(projectInfo, rootSentrySpan, { concurrentScan: options.concurrentScan });
93963
94563
  recordCount(METRIC.projectDetected, 1);
93964
- if (options.scoreOnly || options.suppressRendering) return;
93965
- const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
93966
- yield* printProjectDetection({
93967
- projectInfo,
93968
- userConfig,
93969
- isDiffMode,
93970
- includePaths: options.includePaths,
93971
- lintSourceFileCount
93972
- });
93973
94564
  }) });
93974
- const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)));
94565
+ const output = await runPromise(restoreLegacyThrow(applyObservability(options.silent ? program.pipe(provide(layers), provideService$2(Console, silentConsole)) : program.pipe(provide(layers)), rootSentrySpan)), { signal: oxlintRuntime.abortSignal });
93975
94566
  const didLintFail = lintBindingMissing || output.didLintFail;
93976
94567
  const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
93977
- if (!options.scoreOnly && !options.uiLayers && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
93978
- else runConsole(error$1(highlighter.error(lintFailureReason)));
94568
+ if (!options.scoreOnly && !options.uiLayers && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runSync(log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
94569
+ else runSync(error$1(highlighter.error(lintFailureReason)));
93979
94570
  let inspectDiagnostics = output.diagnostics;
93980
94571
  let baselineDelta;
93981
94572
  if (options.baseline && isDiffMode && !didLintFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
@@ -94025,6 +94616,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
94025
94616
  lintFailureReasonKind: lintBindingMissing ? "native-binding-missing" : output.lintFailureReasonKind,
94026
94617
  supplyChainOverlapTimedOut: output.supplyChainOverlapTimedOut,
94027
94618
  securityScanFailed: output.securityScanFailed,
94619
+ securityScanFailureReason: output.securityScanFailureReason,
94028
94620
  suppressedRuleCounts: output.suppressedRuleCounts
94029
94621
  };
94030
94622
  if (cacheKey !== null && scanResultCache !== null && shouldStoreScanPayload(payload) && !baselineDegraded) scanResultCache.store(cacheKey, payload);
@@ -94050,16 +94642,6 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
94050
94642
  return result;
94051
94643
  };
94052
94644
  const runMaybeSilent = (effect, silent) => silent ? effect.pipe(provideService$2(Console, silentConsole)) : effect;
94053
- const renderCachedProjectDetection = async (input) => {
94054
- if (input.options.scoreOnly || input.options.suppressRendering) return;
94055
- await runPromise(runMaybeSilent(printProjectDetection({
94056
- projectInfo: input.payload.project,
94057
- userConfig: input.userConfig,
94058
- isDiffMode: input.isDiffMode,
94059
- includePaths: input.options.includePaths,
94060
- lintSourceFileCount: input.payload.scannedFileCount
94061
- }), input.options.silent));
94062
- };
94063
94645
  const renderAndRecordScan = async (input) => {
94064
94646
  const result = await runPromise(runMaybeSilent(finalizeAndRender({
94065
94647
  options: input.options,
@@ -94075,6 +94657,7 @@ const renderAndRecordScan = async (input) => {
94075
94657
  deadCodeFailureReason: input.payload.deadCodeFailureReason,
94076
94658
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
94077
94659
  securityScanFailed: input.payload.securityScanFailed ?? false,
94660
+ securityScanFailureReason: input.payload.securityScanFailureReason ?? null,
94078
94661
  directory: input.payload.directory,
94079
94662
  scannedFileCount: input.payload.scannedFileCount,
94080
94663
  scannedFilePaths: input.payload.scannedFilePaths,
@@ -94126,7 +94709,7 @@ const renderAndRecordScan = async (input) => {
94126
94709
  return result;
94127
94710
  };
94128
94711
  const finalizeAndRender = (input) => gen(function* () {
94129
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, directory, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
94712
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, supplyChainOverlapTimedOut, securityScanFailed, securityScanFailureReason, directory, scannedFileCount, scannedFilePaths, analyzedFiles, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
94130
94713
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
94131
94714
  didLintFail,
94132
94715
  lintFailureReason,
@@ -94134,10 +94717,15 @@ const finalizeAndRender = (input) => gen(function* () {
94134
94717
  didDeadCodeFail,
94135
94718
  deadCodeFailureReason,
94136
94719
  supplyChainOverlapTimedOut,
94137
- securityScanFailed
94720
+ securityScanFailed,
94721
+ securityScanFailureReason
94138
94722
  });
94139
94723
  const hasSkippedChecks = skippedChecks.length > 0;
94140
- const noScoreMessage = buildNoScoreMessage(options.noScore, options.scoreDisabledMessage);
94724
+ const noScoreMessage = buildNoScoreMessage({
94725
+ isScoreDisabled: options.noScore,
94726
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(skippedChecks),
94727
+ disabledMessage: options.scoreDisabledMessage
94728
+ });
94141
94729
  const buildResult = () => ({
94142
94730
  diagnostics: [...diagnostics],
94143
94731
  score,
@@ -94183,9 +94771,11 @@ const finalizeAndRender = (input) => gen(function* () {
94183
94771
  if (hasSkippedChecks) {
94184
94772
  const skippedLabel = skippedChecks.join(" and ");
94185
94773
  yield* warn$1(highlighter.warn(`No issues detected, but ${skippedLabel} checks failed — results are incomplete.`));
94186
- } else if (options.categoryFilters.size > 0) yield* log(highlighter.success(`No issues found in category ${formatCategorySelection(options.categoryFilters)}!`));
94187
- else if (demotedDiagnosticCount > 0) yield* log(highlighter.success(`No issues found! (${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface — see config.surfaces.)`));
94188
- else yield* log(highlighter.success("No issues found!"));
94774
+ } else yield* log(highlighter.success(buildEmptyReportMessage({
94775
+ categoryFilters: options.categoryFilters,
94776
+ demotedDiagnosticCount,
94777
+ outputSurface: options.outputSurface
94778
+ })));
94189
94779
  yield* log("");
94190
94780
  yield* pause;
94191
94781
  if (hasSkippedChecks) {
@@ -94265,6 +94855,32 @@ const materializeStagedFiles = async (input) => {
94265
94855
  };
94266
94856
  };
94267
94857
  //#endregion
94858
+ //#region src/cli/utils/deduplicate-project-scans.ts
94859
+ const deduplicateProjectScans = (projectScans) => {
94860
+ const uniqueProjectScans = [];
94861
+ const seenDirectories = /* @__PURE__ */ new Set();
94862
+ for (const projectScan of projectScans) {
94863
+ if (seenDirectories.has(projectScan.directory)) continue;
94864
+ seenDirectories.add(projectScan.directory);
94865
+ uniqueProjectScans.push(projectScan);
94866
+ }
94867
+ return uniqueProjectScans;
94868
+ };
94869
+ //#endregion
94870
+ //#region src/cli/utils/collect-project-source-file-counts.ts
94871
+ const collectProjectSourceFileCounts = async (rootDirectory, projectDirectories) => {
94872
+ const abortController = new AbortController();
94873
+ const unregisterAbortController = activeScanAbortRegistry.register(abortController);
94874
+ try {
94875
+ return await collectSourceFileCountsByDirectory(rootDirectory, projectDirectories, abortController.signal);
94876
+ } finally {
94877
+ unregisterAbortController();
94878
+ }
94879
+ };
94880
+ //#endregion
94881
+ //#region src/cli/utils/format-skipped-projects-message.ts
94882
+ const formatSkippedProjectsMessage = (projectCount) => `${projectCount} ${projectCount === 1 ? "project was" : "projects were"} skipped because the max scan duration was reached.`;
94883
+ //#endregion
94268
94884
  //#region src/cli/utils/is-share-opted-out.ts
94269
94885
  const isShareOptedOut = (scans, flagNoScore) => Boolean(flagNoScore) || scans.some((scan) => Boolean(scan.config?.noScore) || scan.config?.share === false);
94270
94886
  //#endregion
@@ -94276,7 +94892,7 @@ const buildHandoffPayload = (input) => {
94276
94892
  try {
94277
94893
  outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
94278
94894
  } catch {}
94279
- const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
94895
+ const lines = [`Review and fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "finding" : "findings"} in ${input.projectName}. Leave the rest for a follow-up.`, ""];
94280
94896
  topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
94281
94897
  const representative = ruleDiagnostics[0];
94282
94898
  const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
@@ -94304,7 +94920,7 @@ const buildHandoffPayload = (input) => {
94304
94920
  const ruleSummaries = deferredMigrationBuckets.map((bucket) => `${bucket.title} (${bucket.fileCount} files)`).join(", ");
94305
94921
  lines.push(`Some of the rest are migration-scale (span dozens of files): ${ruleSummaries}. For each, fix a representative sample, confirm the recipe holds, and get the code owner's sign-off before changing the rest in one pass.`, "");
94306
94922
  }
94307
- lines.push("Then work through the rest from the full results above.");
94923
+ lines.push("Stop after this pass. Summarize the remaining findings for a follow-up.");
94308
94924
  return lines.join("\n");
94309
94925
  };
94310
94926
  //#endregion
@@ -94514,20 +95130,20 @@ const resolveOpenCommand = (url) => {
94514
95130
  args: [url]
94515
95131
  };
94516
95132
  };
94517
- const openUrl = (url) => {
95133
+ const openUrl = (url) => new Promise((resolve) => {
94518
95134
  try {
94519
95135
  const { command, args } = resolveOpenCommand(url);
94520
95136
  const child = spawn(command, args, {
94521
95137
  detached: true,
94522
95138
  stdio: "ignore"
94523
95139
  });
94524
- child.on("error", () => {});
95140
+ child.once("spawn", () => resolve(true));
95141
+ child.once("error", () => resolve(false));
94525
95142
  child.unref();
94526
- return true;
94527
95143
  } catch {
94528
- return false;
95144
+ resolve(false);
94529
95145
  }
94530
- };
95146
+ });
94531
95147
  //#endregion
94532
95148
  //#region src/cli/utils/ask-add-to-github-actions.ts
94533
95149
  const CI_YES_CHOICE = "ci-yes";
@@ -94568,7 +95184,7 @@ const askAddToGitHubActions = async (prompt = prompts) => {
94568
95184
  if (ciChoice === void 0) return "cancel";
94569
95185
  if (ciChoice === CI_YES_CHOICE) return "yes";
94570
95186
  if (ciChoice === CI_NO_CHOICE) return "no";
94571
- const opened = openUrl(GITHUB_ACTIONS_SETUP_URL);
95187
+ const opened = await openUrl(GITHUB_ACTIONS_SETUP_URL);
94572
95188
  cliLogger.log(opened ? `Opened ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} in your browser.` : `Visit ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} to learn more.`);
94573
95189
  cliLogger.break();
94574
95190
  }
@@ -95665,7 +96281,7 @@ const runInstallReactDoctor = async (options = {}) => {
95665
96281
  const shouldInstallGitHook = gitHookPath != null && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
95666
96282
  const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
95667
96283
  if (!options.dryRun) {
95668
- if (shouldInstallGitHook && gitHookTarget !== null && gitHookTarget !== void 0) installReactDoctorGitHookStep(gitHookTarget);
96284
+ if (shouldInstallGitHook && gitHookTarget != null) installReactDoctorGitHookStep(gitHookTarget);
95669
96285
  if (shouldInstallAgentHooks) installReactDoctorAgentHooksStep(projectRoot, selectedAgents);
95670
96286
  }
95671
96287
  if (options.dryRun) {
@@ -96283,6 +96899,27 @@ const resolveCliInspectOptions = (flags, userConfig) => {
96283
96899
  };
96284
96900
  };
96285
96901
  //#endregion
96902
+ //#region src/cli/utils/resolve-workspace-dead-code-owner.ts
96903
+ const resolveWorkspaceDeadCodeOwner = (input) => {
96904
+ if (!input.isRootDeadCodeEnabled) return null;
96905
+ const resolvedRootDirectory = Path.resolve(input.rootDirectory);
96906
+ return input.projectDirectories.find((projectDirectory) => Path.resolve(projectDirectory) === resolvedRootDirectory) ?? null;
96907
+ };
96908
+ //#endregion
96909
+ //#region src/cli/utils/retry-missing-project-scores.ts
96910
+ const retryMissingProjectScores = async (projectScans) => mapWithConcurrency(projectScans, 64, async (projectScan) => {
96911
+ if (!(projectScan.result.score === null && !projectScan.isScoreDisabled && !hasIncompleteScoreAnalysis(projectScan.result.skippedChecks))) return projectScan;
96912
+ const score = await calculateScore(filterScansForSurface([projectScan], "score"));
96913
+ recordCount(METRIC.scanScoreRetry, 1, { succeeded: score !== null });
96914
+ return score === null ? projectScan : {
96915
+ ...projectScan,
96916
+ result: {
96917
+ ...projectScan.result,
96918
+ score
96919
+ }
96920
+ };
96921
+ });
96922
+ //#endregion
96286
96923
  //#region src/cli/utils/resolve-project-relative-directory.ts
96287
96924
  /**
96288
96925
  * The scanned project's directory relative to the diff root, forward-slashed —
@@ -96343,6 +96980,16 @@ const resolveProjectChangedLineRanges = (rootDirectory, projectDirectory, ranges
96343
96980
  });
96344
96981
  };
96345
96982
  //#endregion
96983
+ //#region src/cli/utils/resolve-project-scan.ts
96984
+ const resolveProjectScan = async (rootScanTarget, projectDirectory) => {
96985
+ const projectScanTarget = projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96986
+ return {
96987
+ directory: projectScanTarget.resolvedDirectory,
96988
+ config: projectDirectory === rootScanTarget.resolvedDirectory ? rootScanTarget.userConfig : mergeReactDoctorConfigs(rootScanTarget.userConfig, projectScanTarget.userConfig ?? void 0),
96989
+ configSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? rootScanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
96990
+ };
96991
+ };
96992
+ //#endregion
96346
96993
  //#region src/cli/utils/build-diagnostic-issue-url.ts
96347
96994
  const formatRuleIdentifier = (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`;
96348
96995
  const buildDiagnosticIssueBody = (input) => {
@@ -96485,7 +97132,7 @@ const resolveExplainTargetDirectory = async (filePath, context) => {
96485
97132
  if (context.projectFlag) {
96486
97133
  const matchedDirectories = await selectProjects(context.resolvedDirectory, context.projectFlag, true);
96487
97134
  if (matchedDirectories.length === 0) return context.resolvedDirectory;
96488
- if (matchedDirectories.length > 1) throw new Error(`--explain takes a single project; --project resolved to ${matchedDirectories.length} projects.`);
97135
+ if (matchedDirectories.length > 1) throw new Error(`why takes a single project; --project resolved to ${matchedDirectories.length} projects.`);
96489
97136
  return matchedDirectories[0];
96490
97137
  }
96491
97138
  return findOwningProjectDirectory(context.resolvedDirectory, filePath);
@@ -96704,6 +97351,15 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96704
97351
  }
96705
97352
  }));
96706
97353
  };
97354
+ const reportSkippedProjects = (input) => {
97355
+ input.skippedProjects.sort((left, right) => left.directory.localeCompare(right.directory));
97356
+ if (input.skippedProjects.length === 0) return;
97357
+ recordCount(METRIC.scanProjectSkipped, input.skippedProjects.length, { reason: "max-duration" });
97358
+ if (!input.isQuiet) {
97359
+ cliLogger.warn(formatSkippedProjectsMessage(input.skippedProjects.length));
97360
+ cliLogger.break();
97361
+ }
97362
+ };
96707
97363
  /**
96708
97364
  * Post-scan finalization shared by the staged-arm and project-loop
96709
97365
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
@@ -96718,7 +97374,7 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
96718
97374
  */
96719
97375
  const finalizeScans = (input) => {
96720
97376
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
96721
- const baselineComputed = input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
97377
+ const baselineComputed = input.skippedProjects.length === 0 && input.completedScans.length > 0 && input.completedScans.every((scan) => scan.result.baselineDelta !== void 0);
96722
97378
  const baselineDegraded = input.baselineIntended && !baselineComputed;
96723
97379
  const mode = baselineDegraded ? "diff" : input.mode;
96724
97380
  const isReactDetected = input.completedScans.some((scan) => hasReactRuntime(scan.result.project));
@@ -96739,6 +97395,7 @@ const finalizeScans = (input) => {
96739
97395
  mode,
96740
97396
  diff: input.diff,
96741
97397
  scans: jsonCompletedScans,
97398
+ skippedProjects: input.skippedProjects,
96742
97399
  totalElapsedMilliseconds: performance$1.now() - input.startTime,
96743
97400
  baseline,
96744
97401
  baselineDegraded
@@ -96777,16 +97434,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96777
97434
  const requestedDirectory = Path.resolve(directory);
96778
97435
  const startTime = performance$1.now();
96779
97436
  let scanStartupSpinner = null;
96780
- if (isJsonMode) {
96781
- enableJsonMode({
96782
- compact: Boolean(flags.jsonCompact),
96783
- directory: requestedDirectory,
96784
- outputFile: flags.jsonOut
96785
- });
96786
- if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
96787
- }
96788
- recordCount(METRIC.cliInvoked, 1, { command: invocationCommand });
96789
97437
  try {
97438
+ if (isJsonMode) {
97439
+ enableJsonMode({
97440
+ compact: Boolean(flags.jsonCompact),
97441
+ directory: requestedDirectory,
97442
+ outputFile: flags.jsonOut
97443
+ });
97444
+ if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
97445
+ }
97446
+ recordCount(METRIC.cliInvoked, 1, { command: invocationCommand });
96790
97447
  validateModeFlags(flags);
96791
97448
  if (flags.staged) setJsonReportMode("staged");
96792
97449
  await maybeMigrateLegacyConfig(requestedDirectory, {
@@ -96859,16 +97516,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96859
97516
  return;
96860
97517
  }
96861
97518
  const buildProjectScanContext = async (projectDirectory) => {
96862
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
96863
- const scanDirectory = projectScanTarget.resolvedDirectory;
97519
+ const projectScan = await resolveProjectScan(scanTarget, projectDirectory);
97520
+ const scanDirectory = projectScan.directory;
96864
97521
  const treeRelativeDirectory = resolveProjectRelativeDirectory(resolvedDirectory, scanDirectory);
96865
97522
  if (treeRelativeDirectory === null) return null;
96866
97523
  return {
96867
97524
  projectDirectory,
96868
97525
  scanDirectory,
96869
97526
  treeRelativeDirectory,
96870
- projectConfig: projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0),
96871
- projectConfigSourceDirectory: projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory
97527
+ projectConfig: projectScan.config,
97528
+ projectConfigSourceDirectory: projectScan.configSourceDirectory
96872
97529
  };
96873
97530
  };
96874
97531
  const projectScanContexts = [];
@@ -96955,6 +97612,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96955
97612
  includePaths: resolveProjectSourceFilePaths(resolvedDirectory, projectScan.scanDirectory, projectScan.stagedFiles.filter((stagedFile) => materializedStagedFiles.has(stagedFile)))
96956
97613
  })).filter((projectRun) => projectRun.includePaths.length > 0);
96957
97614
  const isMultiProject = stagedProjectRuns.length > 1;
97615
+ const skippedProjects = [];
96958
97616
  if (stagedProjectRuns.length === 0) {
96959
97617
  snapshot.cleanup();
96960
97618
  reportNothingToScan({
@@ -96970,6 +97628,13 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
96970
97628
  }
96971
97629
  const scanStagedProject = async (projectRun) => {
96972
97630
  const { projectScan, includePaths } = projectRun;
97631
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97632
+ skippedProjects.push({
97633
+ directory: projectScan.scanDirectory,
97634
+ reason: "max-duration"
97635
+ });
97636
+ return null;
97637
+ }
96973
97638
  const projectTempDirectory = Path.join(snapshot.tempDirectory, projectScan.treeRelativeDirectory);
96974
97639
  const scanResult = await inspectProject(projectTempDirectory, {
96975
97640
  ...scanOptions,
@@ -97010,7 +97675,11 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97010
97675
  });
97011
97676
  const completedScans = stagedBatch.completedScans;
97012
97677
  try {
97013
- if (!isQuiet && isMultiProject) {
97678
+ reportSkippedProjects({
97679
+ skippedProjects,
97680
+ isQuiet
97681
+ });
97682
+ if (!isQuiet && isMultiProject && completedScans.length > 0) {
97014
97683
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97015
97684
  await runPromise(printMultiProjectSummary({
97016
97685
  completedScans,
@@ -97028,6 +97697,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97028
97697
  }
97029
97698
  finalizeScans({
97030
97699
  completedScans,
97700
+ skippedProjects,
97031
97701
  mode: "staged",
97032
97702
  diff: null,
97033
97703
  baselineIntended: false,
@@ -97103,12 +97773,28 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97103
97773
  }
97104
97774
  cliLogger.break();
97105
97775
  }
97106
- const isMultiProject = projectDirectories.length > 1;
97107
- const scanProject = async (projectDirectory) => {
97108
- const projectScanTarget = projectDirectory === resolvedDirectory ? scanTarget : await resolveScanTarget(projectDirectory, { allowAmbiguous: true });
97109
- const scanDirectory = projectScanTarget.resolvedDirectory;
97110
- const projectConfig = projectDirectory === resolvedDirectory ? userConfig : mergeReactDoctorConfigs(userConfig, projectScanTarget.userConfig ?? void 0);
97111
- const projectConfigSourceDirectory = projectScanTarget.userConfig?.plugins === void 0 ? scanTarget.configSourceDirectory : projectScanTarget.configSourceDirectory;
97776
+ const projectScans = deduplicateProjectScans(await Promise.all(projectDirectories.map((projectDirectory) => resolveProjectScan(scanTarget, projectDirectory))));
97777
+ const isMultiProject = projectScans.length > 1;
97778
+ const rootProjectScan = projectScans.find((projectScan) => Path.resolve(projectScan.directory) === Path.resolve(resolvedDirectory));
97779
+ const workspaceDeadCodeOwner = resolveWorkspaceDeadCodeOwner({
97780
+ rootDirectory: resolvedDirectory,
97781
+ projectDirectories: projectScans.map((projectScan) => projectScan.directory),
97782
+ isRootDeadCodeEnabled: scanOptions.deadCode ?? rootProjectScan?.config?.deadCode ?? true
97783
+ });
97784
+ if (workspaceDeadCodeOwner !== null) recordCount(METRIC.scanWorkspaceDeadCodeShared, 1, { projectCount: projectScans.length });
97785
+ const precomputedSourceFileCounts = isMultiProject && !isDiffMode ? await collectProjectSourceFileCounts(resolvedDirectory, projectScans.map((projectScan) => projectScan.directory)) : null;
97786
+ const skippedProjects = [];
97787
+ const scanProject = async (projectScan) => {
97788
+ if (scanDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(scanDeadlineEpochMs) === 0) {
97789
+ skippedProjects.push({
97790
+ directory: projectScan.directory,
97791
+ reason: "max-duration"
97792
+ });
97793
+ return null;
97794
+ }
97795
+ const scanDirectory = projectScan.directory;
97796
+ const projectConfig = projectScan.config;
97797
+ const ownsWorkspaceDeadCode = scanDirectory === workspaceDeadCodeOwner;
97112
97798
  const supplyChainEnabled = flags.supplyChain ?? projectConfig?.supplyChain?.enabled !== false;
97113
97799
  let includePaths;
97114
97800
  let supplyChainManifestChanged = false;
@@ -97132,12 +97818,16 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97132
97818
  if (!isQuiet && !isMultiProject) cliLogger.dim(" ");
97133
97819
  const scanResult = await inspectProject(scanDirectory, {
97134
97820
  ...scanOptions,
97821
+ deadCode: workspaceDeadCodeOwner === null ? scanOptions.deadCode : ownsWorkspaceDeadCode,
97822
+ precomputedSourceFileCount: precomputedSourceFileCounts?.get(scanDirectory),
97135
97823
  deadlineEpochMs: scanDeadlineEpochMs,
97136
97824
  includePaths,
97137
97825
  configOverride: projectConfig,
97138
- configSourceDirectory: projectConfigSourceDirectory ?? void 0,
97826
+ configSourceDirectory: projectScan.configSourceDirectory ?? void 0,
97139
97827
  suppressRendering: isMultiProject,
97140
97828
  concurrentScan: isMultiProject,
97829
+ excludedProjectDirectories: projectScans.filter((candidateProjectScan) => isPathInsideDirectory(candidateProjectScan.directory, scanDirectory)).map((candidateProjectScan) => candidateProjectScan.directory),
97830
+ retainExcludedProjectDeadCodeDiagnostics: ownsWorkspaceDeadCode,
97141
97831
  baseline: baselineRef !== null && projectBaselineBaseFiles !== null && projectBaselineHeadFiles !== null ? {
97142
97832
  ref: baselineRef,
97143
97833
  baseFiles: projectBaselineBaseFiles,
@@ -97154,12 +97844,19 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97154
97844
  };
97155
97845
  };
97156
97846
  const projectBatch = await runProjectScanBatch({
97157
- projects: projectDirectories,
97847
+ projects: projectScans,
97158
97848
  isQuiet,
97159
97849
  isSilent: scanOptions.silent === true,
97160
97850
  scanProject
97161
97851
  });
97162
- const completedScans = projectBatch.completedScans;
97852
+ const completedScans = await retryMissingProjectScores(projectBatch.completedScans.map((completedScan) => ({
97853
+ ...completedScan,
97854
+ isScoreDisabled: scanOptions.noScore ?? completedScan.config?.noScore ?? false
97855
+ })));
97856
+ reportSkippedProjects({
97857
+ skippedProjects,
97858
+ isQuiet
97859
+ });
97163
97860
  if (!isQuiet && isMultiProject && completedScans.length > 0) {
97164
97861
  const showShareLink = !isShareOptedOut(completedScans, scanOptions.noScore) && !scanOptions.isCi;
97165
97862
  await runPromise(printMultiProjectSummary({
@@ -97177,6 +97874,7 @@ const inspectAction = async (directory, flags, invocationCommand = "inspect") =>
97177
97874
  if (flags.outputDir && !didScansWriteDump) await runPromise(printDiagnosticsDump(selectedSurfaceDiagnostics, flags.outputDir, false, isQuiet ? "stderr" : "stdout"));
97178
97875
  finalizeScans({
97179
97876
  completedScans,
97877
+ skippedProjects,
97180
97878
  mode: baselineRef ? "baseline" : isDiffMode ? "diff" : "full",
97181
97879
  diff: isDiffMode ? diffInfo : null,
97182
97880
  baselineIntended: scope === "changed" && diffInfo !== null && !diffInfo.isCurrentChanges,
@@ -97261,6 +97959,56 @@ const installAction = async (options, command) => {
97261
97959
  }
97262
97960
  };
97263
97961
  //#endregion
97962
+ //#region src/cli/utils/should-use-tui.ts
97963
+ const shouldUseTui = (input) => {
97964
+ if (input.isNonInteractiveEnvironment || !input.stdinIsTty || !input.stdoutIsTty || !input.supportsRawMode || input.nodeMajorVersion < 22 || input.terminalName === "dumb") return false;
97965
+ const flags = input.flags;
97966
+ let requiresStableConfiguredScope = false;
97967
+ if (input.userConfig?.scope !== void 0) requiresStableConfiguredScope = input.userConfig.scope !== "full";
97968
+ else if (input.userConfig?.diff !== void 0) requiresStableConfiguredScope = input.userConfig.diff !== false && input.userConfig.diff !== "false";
97969
+ else if (input.userConfig?.base !== void 0) requiresStableConfiguredScope = true;
97970
+ return !(flags.verbose === true || flags.outputDir !== void 0 || flags.score === true || flags.json === true || flags.jsonCompact === true || flags.jsonOut !== void 0 || flags.staged === true || flags.scope !== void 0 || flags.base !== void 0 || flags.includeUntracked === true || flags.diff !== void 0 || flags.changedFilesFrom !== void 0 || flags.debug === true || flags.failOn !== void 0 || requiresStableConfiguredScope);
97971
+ };
97972
+ //#endregion
97973
+ //#region src/cli/commands/scan.ts
97974
+ const runScanCommand = async (input) => {
97975
+ if (input.flags.cache === false) process.env.REACT_DOCTOR_NO_CACHE = "1";
97976
+ const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
97977
+ const tuiEnvironment = {
97978
+ flags: input.flags,
97979
+ isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
97980
+ nodeMajorVersion,
97981
+ stdinIsTty: process.stdin.isTTY === true,
97982
+ stdoutIsTty: process.stdout.isTTY === true,
97983
+ supportsRawMode: typeof process.stdin.setRawMode === "function",
97984
+ terminalName: process.env.TERM
97985
+ };
97986
+ if (!shouldUseTui(tuiEnvironment)) {
97987
+ await inspectAction(input.directory, input.flags, input.invocationCommand);
97988
+ return;
97989
+ }
97990
+ await runProjectMigrations(path.resolve(input.directory));
97991
+ const scanTarget = await resolveScanTarget(input.directory, { allowAmbiguous: true });
97992
+ if (!shouldUseTui({
97993
+ ...tuiEnvironment,
97994
+ userConfig: scanTarget.userConfig
97995
+ })) {
97996
+ await inspectAction(input.directory, input.flags, input.invocationCommand);
97997
+ return;
97998
+ }
97999
+ recordCount(METRIC.cliInvoked, 1, { command: input.invocationCommand });
98000
+ const { runScanApp } = await import("./run-scan-app-C8iWVgvR.js");
98001
+ const { shouldFail } = await runScanApp({
98002
+ directory: input.directory,
98003
+ scanTarget,
98004
+ options: resolveCliInspectOptions(input.flags, null),
98005
+ projectFlag: input.flags.project,
98006
+ skipPrompts: input.flags.yes ?? false,
98007
+ blocking: input.flags.blocking
98008
+ });
98009
+ if (shouldFail) process.exitCode = 1;
98010
+ };
98011
+ //#endregion
97264
98012
  //#region src/cli/utils/rule-catalog.ts
97265
98013
  const buildRuleCatalog = () => REACT_DOCTOR_RULES.map((entry) => ({
97266
98014
  key: entry.key,
@@ -97901,13 +98649,39 @@ const ensureWindowsUtf8Console = () => {
97901
98649
  } catch {}
97902
98650
  };
97903
98651
  //#endregion
98652
+ //#region src/cli/utils/active-tui-renderer.ts
98653
+ let activeTuiRenderer = null;
98654
+ const registerActiveTuiRenderer = (renderer) => {
98655
+ activeTuiRenderer = renderer;
98656
+ return () => {
98657
+ if (activeTuiRenderer === renderer) activeTuiRenderer = null;
98658
+ };
98659
+ };
98660
+ const clearActiveTuiRenderer = () => {
98661
+ const renderer = activeTuiRenderer;
98662
+ activeTuiRenderer = null;
98663
+ renderer?.clear();
98664
+ };
98665
+ //#endregion
97904
98666
  //#region src/cli/utils/exit-gracefully.ts
98667
+ let didStartExiting = false;
97905
98668
  const exitGracefully = () => {
98669
+ if (didStartExiting) return process.exit(130);
98670
+ didStartExiting = true;
98671
+ activeScanAbortRegistry.abortAll();
98672
+ clearActiveTuiRenderer();
97906
98673
  try {
97907
98674
  if (isJsonModeActive()) writeJsonErrorReport(/* @__PURE__ */ new Error("Scan cancelled by user (SIGINT/SIGTERM)"));
97908
- else console.log("\nCancelled.\n");
98675
+ else console.log([
98676
+ "",
98677
+ "Cancelled.",
98678
+ "",
98679
+ buildSectionDivider(),
98680
+ "",
98681
+ ...buildFooterLinkLines({ shareUrl: null })
98682
+ ].join("\n"));
97909
98683
  } catch {}
97910
- process.exit(130);
98684
+ flushSentry().finally(() => process.exit(130));
97911
98685
  };
97912
98686
  //#endregion
97913
98687
  //#region src/cli/utils/guard-stdin.ts
@@ -98025,6 +98799,7 @@ const ROOT_FLAG_SPEC = {
98025
98799
  "--json-compact",
98026
98800
  "--lint",
98027
98801
  "--no-color",
98802
+ "--no-cache",
98028
98803
  "--no-dead-code",
98029
98804
  "--no-lint",
98030
98805
  "--no-parallel",
@@ -98148,13 +98923,18 @@ const COMMAND_FLAG_SPECS = new Map([
98148
98923
  longOptionsWithoutValues: new Set([
98149
98924
  "--color",
98150
98925
  "--help",
98926
+ "--no-cache",
98151
98927
  "--no-color",
98152
98928
  "--no-dead-code",
98153
98929
  "--no-score",
98154
98930
  "--no-supply-chain",
98155
98931
  "--yes"
98156
98932
  ]),
98157
- longOptionsWithRequiredValues: new Set(["--blocking", "--project"]),
98933
+ longOptionsWithRequiredValues: new Set([
98934
+ "--blocking",
98935
+ "--max-duration",
98936
+ "--project"
98937
+ ]),
98158
98938
  longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
98159
98939
  shortOptionsWithoutValues: new Set(["-h", "-y"]),
98160
98940
  shortOptionsWithRequiredValues: new Set(["-p"])
@@ -98296,6 +99076,7 @@ ${highlighter.dim("Scope:")}
98296
99076
  Dead-code, supply-chain, external lint-config, custom-plugin, and health-score passes are skipped.
98297
99077
  Standard scan flags such as ${highlighter.info("--scope")}, ${highlighter.info("--project")}, ${highlighter.info("--verbose")}, and ${highlighter.info("--json")} still work.
98298
99078
  `;
99079
+ const MAX_DURATION_OPTION_DESCRIPTION = "scan time budget for the whole run, shared across workspace projects: past it, queued projects, remaining lint batches, and dead-code are skipped and partial results are reported (skipped files and projects are listed in the JSON report)";
98299
99080
  const renderCiHelpEpilog = () => `
98300
99081
  ${highlighter.dim("Examples:")}
98301
99082
  ${formatExampleLines([
@@ -98315,8 +99096,12 @@ ${highlighter.dim("Learn more:")}
98315
99096
  ${highlighter.info(CI_URL)}
98316
99097
  `;
98317
99098
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
98318
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks (honors --project and config `projects`; exits 0 when nothing is staged)").option("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
98319
- program.action(inspectAction);
99099
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("--no-cache", "disable all scan caches for this run").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much supported JS/TS and inline HTML script source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks (honors --project and config `projects`; exits 0 when nothing is staged)").option("--max-duration <seconds>", MAX_DURATION_OPTION_DESCRIPTION).option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
99100
+ program.action((directory = ".", flags) => runScanCommand({
99101
+ directory,
99102
+ flags,
99103
+ invocationCommand: "inspect"
99104
+ }));
98320
99105
  program.command("design [directory]").description("Run only the focused UI design diagnostics").addHelpText("after", renderDesignHelpEpilog).action((directory, _options, command) => designAction(directory ?? ".", command.optsWithGlobals()));
98321
99106
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
98322
99107
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -98337,41 +99122,17 @@ rules.command("category <category> <severity>").description("Set the severity fo
98337
99122
  rules.command("ignore-tag <tag>").description("Skip a whole rule family by tag before linting (e.g. design)").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesIgnoreTagAction(tag, command.optsWithGlobals()));
98338
99123
  rules.command("unignore-tag <tag>").description("Stop ignoring a tag previously skipped via ignore-tag").option("-c, --cwd <cwd>", "working directory", process.cwd()).action((tag, _options, command) => rulesUnignoreTagAction(tag, command.optsWithGlobals()));
98339
99124
  program.command("experimental-lsp", { hidden: false }).description("[experimental] run the React Doctor language server over stdio (for editors)").allowUnknownOption().action(() => {});
98340
- program.command("experimental-tui [directory]", { hidden: true }).description("[experimental] interactive, scrollable scan report").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").option("--no-dead-code", "skip dead-code analysis").option("--no-supply-chain", "skip the dependency supply-chain scan").option("--no-score", "skip the score API, the share URL, and crash reporting").option("-p, --project <names>", "scan specific workspace projects (comma-separated, or *)").option("-y, --yes", "skip the project prompt and scan every discovered project").action(async (directory = ".", _localOptions, command) => {
98341
- const options = command.optsWithGlobals();
98342
- const deadCode = options.deadCode === false ? false : void 0;
98343
- const noScore = options.score === false ? true : void 0;
98344
- const supplyChain = options.supplyChain === false ? false : void 0;
98345
- const nodeMajorVersion = Number(process.versions.node.split(".")[0]);
98346
- if (process.stdout.isTTY !== true || process.stdin.isTTY !== true || isNonInteractiveEnvironment() || nodeMajorVersion < 22) {
98347
- await inspectAction(directory, {
98348
- deadCode,
98349
- score: options.score === false ? false : void 0,
98350
- supplyChain,
98351
- project: options.project,
98352
- yes: options.yes,
98353
- blocking: options.blocking
98354
- });
98355
- return;
98356
- }
98357
- recordCount(METRIC.cliInvoked, 1, { command: "experimental-tui" });
98358
- const { runScanApp } = await import("./run-scan-app-DHRvqfkX.js");
98359
- const { shouldFail } = await runScanApp({
98360
- directory,
98361
- options: {
98362
- deadCode,
98363
- noScore,
98364
- supplyChain
98365
- },
98366
- projectFlag: options.project,
98367
- skipPrompts: options.yes ?? false,
98368
- blocking: options.blocking
98369
- });
98370
- if (shouldFail) process.exitCode = 1;
98371
- });
99125
+ program.command("experimental-tui [directory]", { hidden: true }).description("[experimental] interactive, scrollable scan report").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").option("--no-dead-code", "skip dead-code analysis").option("--no-supply-chain", "skip the dependency supply-chain scan").option("--score", "only print the numeric score (for scripts and CI)").option("--no-score", "skip the score API, the share URL, and crash reporting").option("--no-cache", "disable all scan caches for this run").option("--max-duration <seconds>", MAX_DURATION_OPTION_DESCRIPTION).option("-p, --project <names>", "scan specific workspace projects (comma-separated, or *)").option("-y, --yes", "skip the project prompt and scan every discovered project").action((directory = ".", _localOptions, command) => runScanCommand({
99126
+ directory,
99127
+ flags: command.optsWithGlobals(),
99128
+ invocationCommand: "experimental-tui"
99129
+ }));
98372
99130
  process.stdout.on("error", (error) => {
98373
99131
  if (error.code === "EPIPE") process.exit(0);
98374
99132
  });
99133
+ process.stderr.on("error", (error) => {
99134
+ if (error.code === "EPIPE") process.exit(0);
99135
+ });
98375
99136
  const knownCommands = program.commands.flatMap((command) => [command.name(), ...command.aliases()]);
98376
99137
  const strippedArgv = stripUnknownCliFlags(process.argv);
98377
99138
  if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
@@ -98395,7 +99156,7 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
98395
99156
  handleError(error, { sentryEventId });
98396
99157
  });
98397
99158
  //#endregion
98398
- export { toForwardSlashes as A, getCategoryImpact as B, buildRulePriorityMap as C, resolveBlockingLevel as D, formatLearnMoreLine as E, TUI_REPORT_DETAIL_WIDTH_FRACTION as F, runPromise as G, mapWithConcurrency as H, isCiEnvironment as I, succeed$3 as J, sync as K, DIAGNOSTIC_CATEGORY_BUCKETS as L, isUnicodeSupported as M, METRIC as N, isReactDoctorWorkflowInstalled as O, TUI_REPORT_COMPACT_MAX_ROWS as P, Progress as R, hasLintHardFailure as S, formatFixRecipeLine as T, mergeReactDoctorConfigs as U, highlighter as V, resolveScanTarget as W, canAnimateOnboarding as _, resolveCliInspectOptions as a, buildCodeFrame as b, detectLaunchableAgents as c, launchCliAgent as d, isShareOptedOut as f, doctorFace as g, easeOutCubic as h, selectProjects as i, findNearestPackageDirectory as j, recordCount as k, CLI_AGENT_BINARIES as l, printFooter as m, shouldFailScanGate as n, countUniqueScannedFiles as o, createInvocationInspect as p, void_ as q, discoverWorkspacePackages as r, setUpGitHubActions as s, exitGracefully as t, copyToClipboard as u, computeProjectedScore as v, buildSortedRuleGroups as w, buildNoScoreMessage as x, formatElapsedTime as y, Reporter as z };
99159
+ export { TUI_REPORT_DETAIL_WIDTH_FRACTION as $, shouldRecordOnboarding as A, buildSortedRuleGroups as B, createInvocationInspect as C, markOnboardingComplete as D, doctorFace as E, buildEmptyReportMessage as F, recordCount as G, formatLearnMoreLine as H, hasIncompleteScoreAnalysis as I, findNearestPackageDirectory as J, recordDistribution as K, buildNoScoreMessage as L, computeProjectedScore as M, formatElapsedTime as N, canAnimateOnboarding as O, buildCodeFrame as P, TUI_REPORT_COMPACT_MAX_ROWS as Q, hasLintHardFailure as R, deduplicateProjectScans as S, easeOutCubic as T, resolveBlockingLevel as U, formatFixRecipeLine as V, isReactDoctorWorkflowInstalled as W, CI_TRUST_COMPANIES as X, isUnicodeSupported as Y, METRIC as Z, launchCliAgent as _, succeed$3 as _t, selectProjects as a, buildRuleDocsUrl as at, formatSkippedProjectsMessage as b, resolveWorkspaceDeadCodeOwner as c, highlighter as ct, countUniqueScannedFiles as d, remainingDeadlineBudgetMs as dt, isCiEnvironment as et, setUpGitHubActions as f, resolveScanTarget as ft, copyToClipboard as g, void_ as gt, CLI_AGENT_BINARIES as h, sync as ht, discoverWorkspacePackages as i, Reporter as it, filterDiagnosticsByCategories as j, isOnboardingForced as k, resolveCliInspectOptions as l, isPathInsideDirectory as lt, detectLaunchableAgents as m, runPromise as mt, registerActiveTuiRenderer as n, GITHUB_ACTIONS_SETUP_URL as nt, resolveProjectScan as o, getCategoryImpact as ot, openUrl as p, yieldToEventLoop as pt, toForwardSlashes as q, shouldFailScanGate as r, Progress as rt, retryMissingProjectScores as s, hasPublishedFixRecipe as st, exitGracefully as t, DIAGNOSTIC_CATEGORY_BUCKETS as tt, filterScansForSurface as u, mapWithConcurrency as ut, buildHandoffPayload as v, printFooter as w, collectProjectSourceFileCounts as x, isShareOptedOut as y, buildRulePriorityMap as z };
98399
99160
 
98400
99161
  //# sourceMappingURL=cli.js.map
98401
- //# debugId=707bd0ef-8490-5120-ba82-f65ad0b490e1
99162
+ //# debugId=e74e6a68-20e1-59fe-8db0-62b87e8fe049