react-doctor 0.5.8-dev.f853efd → 0.5.8-dev.fff9466

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/index.d.ts CHANGED
@@ -9726,7 +9726,7 @@ interface InspectResult {
9726
9726
  * Per-file lint cache outcome: files served from cache, and total files
9727
9727
  * considered. Both absent when the cache was off or bypassed (audit mode,
9728
9728
  * adopted `extends`, user plugins). The CLI projects these onto the Sentry
9729
- * wide event as `lintCacheHitRatio`.
9729
+ * wide event as `lint.cacheHitRatio`.
9730
9730
  */
9731
9731
  lintCacheHitFileCount?: number | null;
9732
9732
  lintCacheTotalFileCount?: number | null;
@@ -10123,7 +10123,7 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
10123
10123
  * untouched so the loader sees the same plain object either way.
10124
10124
  */
10125
10125
  declare const defineConfig: (config: ReactDoctorConfig) => ReactDoctorConfig; //#endregion
10126
- //#region src/utils/group-by.d.ts
10126
+ //#region src/utils/detect-ai-training-environment.d.ts
10127
10127
  //#endregion
10128
10128
  //#region ../api/dist/index.d.ts
10129
10129
  //#region src/diagnose.d.ts
package/dist/index.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]="62d09923-e78d-5501-8799-859098af2aaa")}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]="faa2d232-c423-58f8-97f4-ba8d41a199d7")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -5996,7 +5996,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
5996
5996
  * @since 2.0.0
5997
5997
  */
5998
5998
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
5999
- const setImmediate = "setImmediate" in globalThis ? (f) => {
5999
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
6000
6000
  const timer = globalThis.setImmediate(f);
6001
6001
  return () => globalThis.clearImmediate(timer);
6002
6002
  } : (f) => {
@@ -6040,7 +6040,7 @@ var PriorityBuckets = class {
6040
6040
  var MixedScheduler = class {
6041
6041
  executionMode;
6042
6042
  setImmediate;
6043
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
6043
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
6044
6044
  this.executionMode = executionMode;
6045
6045
  this.setImmediate = setImmediateFn;
6046
6046
  }
@@ -6065,7 +6065,7 @@ var MixedSchedulerDispatcher = class {
6065
6065
  tasks = /* @__PURE__ */ new PriorityBuckets();
6066
6066
  running = void 0;
6067
6067
  setImmediate;
6068
- constructor(setImmediateFn = setImmediate) {
6068
+ constructor(setImmediateFn = setImmediate$1) {
6069
6069
  this.setImmediate = setImmediateFn;
6070
6070
  }
6071
6071
  /**
@@ -33694,6 +33694,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33694
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33695
33695
  const FETCH_TIMEOUT_MS = 1e4;
33696
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33697
33698
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33698
33699
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33699
33700
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -33751,6 +33752,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
33751
33752
  const LINT_PHASE_TIMEOUT_MS = 3e5;
33752
33753
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
33753
33754
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
33755
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
33754
33756
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
33755
33757
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
33756
33758
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -35368,7 +35370,7 @@ const resolveScanConcurrency = (requested) => {
35368
35370
  if (!Number.isFinite(requested) || requested < 1) return 1;
35369
35371
  return Math.min(Math.floor(requested), 32);
35370
35372
  };
35371
- const readSystemFacts = () => ({
35373
+ const readSystemFacts$1 = () => ({
35372
35374
  availableCores: os.availableParallelism(),
35373
35375
  totalMemoryBytes: os.totalmem(),
35374
35376
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -35389,7 +35391,7 @@ const readSystemFacts = () => ({
35389
35391
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
35390
35392
  * limited, and ceiling cases without mocking `os` or the filesystem.
35391
35393
  */
35392
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
35394
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
35393
35395
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
35394
35396
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
35395
35397
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -36922,7 +36924,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
36922
36924
  }
36923
36925
  return true;
36924
36926
  };
36925
- const checkSecurityScan = (rootDirectory, options = {}) => {
36927
+ const yieldToEventLoop = () => new Promise((resolve) => {
36928
+ setImmediate(resolve);
36929
+ });
36930
+ const createSecurityScanSession = (rootDirectory, options) => {
36926
36931
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
36927
36932
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
36928
36933
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -36937,7 +36942,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36937
36942
  committedFilesOnly: rule.committedFilesOnly === true
36938
36943
  }];
36939
36944
  });
36940
- if (enabledScanRules.length === 0) return [];
36945
+ if (enabledScanRules.length === 0) return null;
36941
36946
  const diagnostics = [];
36942
36947
  const seen = /* @__PURE__ */ new Set();
36943
36948
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -36949,15 +36954,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
36949
36954
  }
36950
36955
  return status === true;
36951
36956
  };
36952
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36953
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
36954
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36955
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36956
- if (seen.has(key)) continue;
36957
- seen.add(key);
36958
- diagnostics.push(diagnostic);
36957
+ const scanFile = (file) => {
36958
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
36959
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
36960
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
36961
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
36962
+ if (seen.has(key)) continue;
36963
+ seen.add(key);
36964
+ diagnostics.push(diagnostic);
36965
+ }
36966
+ };
36967
+ return {
36968
+ scanFile,
36969
+ diagnostics
36970
+ };
36971
+ };
36972
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
36973
+ const session = createSecurityScanSession(rootDirectory, options);
36974
+ if (session === null) return [];
36975
+ let filesSinceYield = 0;
36976
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
36977
+ session.scanFile(file);
36978
+ filesSinceYield += 1;
36979
+ if (filesSinceYield >= 16) {
36980
+ filesSinceYield = 0;
36981
+ await yieldToEventLoop();
36982
+ }
36959
36983
  }
36960
- return diagnostics;
36984
+ return session.diagnostics;
36961
36985
  };
36962
36986
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
36963
36987
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -37174,6 +37198,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
37174
37198
  return [...seen].filter((pattern) => pattern.length > 0);
37175
37199
  };
37176
37200
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
37201
+ const readSystemFacts = () => ({
37202
+ availableCores: os.availableParallelism(),
37203
+ totalMemoryBytes: os.totalmem(),
37204
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
37205
+ });
37206
+ /**
37207
+ * How many real deslop dead-code child processes may run at once, across the
37208
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
37209
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
37210
+ * workers that fit in available memory, floored at 1.
37211
+ *
37212
+ * On a roomy dev box / CI runner this resolves high enough that every
37213
+ * concurrently-scanned project still spawns its own worker (no serialization vs
37214
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
37215
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
37216
+ * instead of oversubscribing memory with N simultaneous children — the global
37217
+ * cap the per-project spawn path lacked.
37218
+ *
37219
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
37220
+ * heavier dead-code worker. `facts` is injectable for tests.
37221
+ */
37222
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
37223
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
37224
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
37225
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
37226
+ };
37227
+ let availableSlots = -1;
37228
+ const waiters = [];
37229
+ const releaseSlot = () => {
37230
+ const nextWaiter = waiters.shift();
37231
+ if (nextWaiter !== void 0) nextWaiter();
37232
+ else availableSlots += 1;
37233
+ };
37234
+ /**
37235
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
37236
+ * task settles (success or failure). With a high cap (roomy machine) every
37237
+ * caller proceeds immediately; with a low cap (constrained runner) callers
37238
+ * queue and run as slots free.
37239
+ *
37240
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
37241
+ * this caller is queued, the call rejects without acquiring a slot or running
37242
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
37243
+ * then spawn a child only to tear it down. A queued caller that aborts removes
37244
+ * its own waiter so a later release never hands a slot to a dead request.
37245
+ */
37246
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
37247
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
37248
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
37249
+ if (availableSlots > 0) availableSlots -= 1;
37250
+ else await new Promise((resolve, reject) => {
37251
+ const waiter = () => {
37252
+ abortSignal?.removeEventListener("abort", onAbort);
37253
+ resolve();
37254
+ };
37255
+ const onAbort = () => {
37256
+ const queuedIndex = waiters.indexOf(waiter);
37257
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
37258
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
37259
+ };
37260
+ waiters.push(waiter);
37261
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
37262
+ });
37263
+ try {
37264
+ return await task();
37265
+ } finally {
37266
+ releaseSlot();
37267
+ }
37268
+ };
37177
37269
  /**
37178
37270
  * Resolves a path to its canonical, symlink-free form, falling back to
37179
37271
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -37198,6 +37290,11 @@ const toCanonicalPath = (filePath) => {
37198
37290
  };
37199
37291
  const DEAD_CODE_PLUGIN = "deslop";
37200
37292
  const DEAD_CODE_CATEGORY = "Maintainability";
37293
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
37294
+ "react-doctor",
37295
+ "eslint-plugin-react-doctor",
37296
+ "oxlint-plugin-react-doctor"
37297
+ ]);
37201
37298
  const DEAD_CODE_WORKER_SCRIPT = `
37202
37299
  const inputChunks = [];
37203
37300
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -37464,14 +37561,17 @@ const checkDeadCode = async (options) => {
37464
37561
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
37465
37562
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
37466
37563
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
37467
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37468
- rootDirectory,
37469
- entryPatterns,
37470
- tsConfigPath: resolveTsConfigPath(rootDirectory),
37471
- ignorePatterns,
37472
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37473
- parseConcurrency: options.parseConcurrency
37474
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
37564
+ const spawnAndRun = () => {
37565
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
37566
+ rootDirectory,
37567
+ entryPatterns,
37568
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
37569
+ ignorePatterns,
37570
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
37571
+ parseConcurrency: options.parseConcurrency
37572
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
37573
+ };
37574
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
37475
37575
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
37476
37576
  const diagnostics = [];
37477
37577
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -37500,6 +37600,7 @@ const checkDeadCode = async (options) => {
37500
37600
  });
37501
37601
  }
37502
37602
  for (const unusedDependency of result.unusedDependencies) {
37603
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
37503
37604
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
37504
37605
  diagnostics.push({
37505
37606
  filePath: "package.json",
@@ -37872,43 +37973,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37872
37973
  * reason: GitInvocationFailed })` so the rest of the codebase
37873
37974
  * sees a single failure channel.
37874
37975
  */
37875
- const runCommand = (input) => scoped(gen(function* () {
37876
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37877
- cwd: input.directory,
37878
- env: input.env,
37879
- extendEnv: true
37880
- }));
37881
- const maxStdoutBytes = input.maxStdoutBytes;
37882
- const stdoutByteCount = yield* make$13(0);
37883
- const [stdout, stderr, status] = yield* all([
37884
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37885
- args: [...input.args],
37886
- directory: input.directory,
37887
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37888
- }) })) : void_)))))),
37889
- mkString(decodeText(handle.stderr)),
37890
- handle.exitCode
37891
- ], { concurrency: 3 });
37892
- return {
37893
- status,
37894
- stdout,
37895
- stderr
37896
- };
37897
- })).pipe(catchTag$1("PlatformError", (cause) => {
37898
- if (input.command !== "git") return succeed$2({
37976
+ const runCommand = (input) => {
37977
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37899
37978
  status: 127,
37900
37979
  stdout: "",
37901
37980
  stderr: String(cause)
37902
- });
37903
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37981
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37904
37982
  args: [...input.args],
37905
37983
  directory: input.directory,
37906
37984
  cause
37907
- }) });
37908
- }), withSpan("git.exec", { attributes: {
37909
- "git.command": input.command,
37910
- "git.subcommand": input.args[0] ?? ""
37911
- } }));
37985
+ }) }));
37986
+ return scoped(gen(function* () {
37987
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37988
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37989
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37990
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37991
+ cwd: input.directory,
37992
+ env: input.env,
37993
+ extendEnv: true
37994
+ }));
37995
+ const maxStdoutBytes = input.maxStdoutBytes;
37996
+ const stdoutByteCount = yield* make$13(0);
37997
+ const [stdout, stderr, status] = yield* all([
37998
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37999
+ args: [...input.args],
38000
+ directory: input.directory,
38001
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
38002
+ }) })) : void_)))))),
38003
+ mkString(decodeText(handle.stderr)),
38004
+ handle.exitCode
38005
+ ], { concurrency: 3 });
38006
+ return {
38007
+ status,
38008
+ stdout,
38009
+ stderr
38010
+ };
38011
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
38012
+ "git.command": input.command,
38013
+ "git.subcommand": input.args[0] ?? ""
38014
+ } }));
38015
+ };
37912
38016
  const runGit = (directory, args) => runCommand({
37913
38017
  command: "git",
37914
38018
  args,
@@ -38155,7 +38259,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38155
38259
  ]);
38156
38260
  if (result.status !== 0) return null;
38157
38261
  return parseChangedLineRanges(result.stdout);
38158
- }).pipe(withSpan("Git.changedLineRanges"))
38262
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38159
38263
  });
38160
38264
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38161
38265
  /**
@@ -40632,7 +40736,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40632
40736
  * diagnostics).
40633
40737
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
40634
40738
  * 3. environment checks (reduced-motion + pnpm hardening +
40635
- * expo/react-native + security scan), collected synchronously
40739
+ * expo/react-native), collected synchronously. The heavier
40740
+ * content-regex security scan is forked instead (like supply-chain
40741
+ * below) and joined before the concat, so its CPU overlaps lint
40742
+ * rather than blocking the event loop before it.
40636
40743
  * 4. The supply-chain check (Socket.dev) is forked onto a background
40637
40744
  * fiber so its ~100% network-bound time overlaps the ~100%
40638
40745
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -40652,7 +40759,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
40652
40759
  * order, so terminal output is identical either way; supply-chain
40653
40760
  * rides alongside without a spinner.
40654
40761
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
40655
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
40762
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
40656
40763
  * byte-identical regardless of which fiber settled first. The
40657
40764
  * viewer-permission fiber is joined later, during score-metadata
40658
40765
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -40713,12 +40820,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40713
40820
  ...checkPnpmHardening(scanDirectory),
40714
40821
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
40715
40822
  ...checkExpoProject(scanDirectory, project),
40716
- ...checkReactNativeProject(scanDirectory, project),
40717
- ...checkSecurityScan(scanDirectory, {
40718
- project,
40719
- ignoredTags: input.ignoredTags
40720
- })
40823
+ ...checkReactNativeProject(scanDirectory, project)
40721
40824
  ])));
40825
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
40826
+ project,
40827
+ ignoredTags: input.ignoredTags
40828
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
40722
40829
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
40723
40830
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
40724
40831
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -40854,9 +40961,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
40854
40961
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
40855
40962
  const supplyChainResult = yield* join(supplyChainFiber);
40856
40963
  const supplyChainCollected = supplyChainResult.diagnostics;
40964
+ const securityScanCollected = yield* join(securityScanFiber);
40857
40965
  yield* reporterService.finalize;
40858
40966
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
40859
40967
  ...envCollected,
40968
+ ...securityScanCollected,
40860
40969
  ...supplyChainCollected,
40861
40970
  ...lintCollected,
40862
40971
  ...deadCodeCollected
@@ -41308,6 +41417,61 @@ const clearAutoSuppressionCaches = () => {};
41308
41417
  * untouched so the loader sees the same plain object either way.
41309
41418
  */
41310
41419
  const defineConfig = (config) => config;
41420
+ const AI_TRAINING_ENV_VARS = [
41421
+ ["HF_DATASETS_CACHE", "huggingface"],
41422
+ ["HF_HOME", "huggingface"],
41423
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
41424
+ ["SPACE_ID", "huggingface-spaces"],
41425
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
41426
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
41427
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
41428
+ ["TPU_NAME", "google-tpu"],
41429
+ ["WANDB_RUN_ID", "wandb"],
41430
+ ["WANDB_SWEEP_ID", "wandb"],
41431
+ ["MLFLOW_RUN_ID", "mlflow"],
41432
+ ["MLFLOW_TRACKING_URI", "mlflow"],
41433
+ ["COMET_EXPERIMENT_KEY", "comet"],
41434
+ ["NEPTUNE_RUN_ID", "neptune"],
41435
+ ["CLEARML_TASK_ID", "clearml"],
41436
+ ["DVC_STAGE", "dvc"],
41437
+ ["RAY_WORKER_PROCESS", "ray"],
41438
+ ["RAY_ADDRESS", "ray"],
41439
+ ["MUJOCO_GL", "mujoco"],
41440
+ ["MUJOCO_PATH", "mujoco"],
41441
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
41442
+ ["SM_TRAINING_ENV", "sagemaker"],
41443
+ ["TRAINING_JOB_ARN", "sagemaker"],
41444
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
41445
+ ["AZUREML_RUN_ID", "azure-ml"],
41446
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
41447
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
41448
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
41449
+ ["DET_MASTER", "determined-ai"],
41450
+ ["LIGHTNING_USER_ID", "lightning-ai"],
41451
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
41452
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
41453
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
41454
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
41455
+ ["COLAB_BACKEND_VERSION", "google-colab"],
41456
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
41457
+ ["DAYTONA_WS_ID", "daytona"],
41458
+ ["DAYTONA_WS_NAME", "daytona"],
41459
+ ["E2B_SANDBOX_ID", "e2b"],
41460
+ ["MODAL_FUNCTION_ID", "modal"],
41461
+ ["MODAL_TASK_ID", "modal"],
41462
+ ["RUNPOD_POD_ID", "runpod"],
41463
+ ["REPLICATE_USERNAME", "replicate"],
41464
+ ["VAST_CONTAINERLABEL", "vast-ai"],
41465
+ ["HARBOR_URL", "harbor"],
41466
+ ["HARBOR_HOSTNAME", "harbor"],
41467
+ ["SWE_BENCH_TASK", "swe-bench"],
41468
+ ["SWEBENCH_TASK", "swe-bench"],
41469
+ ["SWE_AGENT_MODEL", "swe-agent"]
41470
+ ];
41471
+ const detectAiTrainingEnvironment = () => {
41472
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
41473
+ return null;
41474
+ };
41311
41475
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
41312
41476
  const mergeIgnores = (baseIgnore, overrideIgnore) => {
41313
41477
  const mergedIgnore = {
@@ -41352,6 +41516,12 @@ const mergeReactDoctorConfigs = (baseConfig, overrideConfig) => {
41352
41516
  };
41353
41517
  //#endregion
41354
41518
  //#region ../api/dist/index.js
41519
+ let didWarnAiTraining = false;
41520
+ const warnIfAiTrainingEnvironment = () => {
41521
+ if (didWarnAiTraining || detectAiTrainingEnvironment() === null) return;
41522
+ didWarnAiTraining = true;
41523
+ console.warn("[react-doctor] Use in an AI or ML pipeline requires written permission under the react-doctor license. Contact founders@million.dev to request access.");
41524
+ };
41355
41525
  const buildDiagnoseLayer = (config, configOverrideTarget) => {
41356
41526
  const configLayer = configOverrideTarget === void 0 ? Config.layerNode : Config.layerOf({
41357
41527
  config,
@@ -41389,6 +41559,7 @@ const outputToDiagnoseResult = (output, elapsedMilliseconds) => {
41389
41559
  };
41390
41560
  };
41391
41561
  const diagnoseDirectory = async (directory, options) => {
41562
+ warnIfAiTrainingEnvironment();
41392
41563
  const startTime = globalThis.performance.now();
41393
41564
  const scanTarget = await resolveScanTarget(directory);
41394
41565
  return outputToDiagnoseResult(await runPromise(restoreLegacyThrow(buildInspectProgram(scanTarget, options).pipe(provide(buildDiagnoseLayer(scanTarget.userConfig)), provide(layerOtlp)))), globalThis.performance.now() - startTime);
@@ -41435,6 +41606,7 @@ const diagnoseProject = async (projectDefinition, baseOptions, batchConfig) => {
41435
41606
  }
41436
41607
  };
41437
41608
  const diagnoseProjectBatch = async (input) => {
41609
+ warnIfAiTrainingEnvironment();
41438
41610
  const startTime = globalThis.performance.now();
41439
41611
  const { projects, concurrency, config: batchConfig, ...baseOptions } = input;
41440
41612
  const projectResults = await mapWithConcurrency(projects, concurrency ?? 4, (projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig));
@@ -41479,4 +41651,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41479
41651
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41480
41652
 
41481
41653
  //# sourceMappingURL=index.js.map
41482
- //# debugId=62d09923-e78d-5501-8799-859098af2aaa
41654
+ //# debugId=faa2d232-c423-58f8-97f4-ba8d41a199d7