react-doctor 0.6.3-dev.8325343 → 0.6.3-dev.bdf8074

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]="2921b8ff-14cf-5604-8ae5-b65b4f1347fc")}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]="4d2b3e2c-ffb8-5da4-bf5d-833e608822bb")}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";
@@ -9,7 +9,7 @@ import * as NFS from "node:fs";
9
9
  import fs, { mkdtempSync, rmSync } from "node:fs";
10
10
  import process$1 from "node:process";
11
11
  import * as ts from "typescript";
12
- import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
12
+ import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
13
13
  import * as OS from "node:os";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { parseJSON5 } from "confbox";
@@ -18,6 +18,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
18
18
  import { createJiti } from "jiti";
19
19
  import * as Crypto from "node:crypto";
20
20
  import crypto, { createHash, randomUUID } from "node:crypto";
21
+ import { ANALYZED_MANIFEST_FILENAMES, DEFAULT_EXTENSIONS } from "deslop-js/analyzed-inputs";
21
22
  import { gzipSync } from "node:zlib";
22
23
  import * as Sentry from "@sentry/node";
23
24
  import { performance as performance$1 } from "node:perf_hooks";
@@ -36128,6 +36129,10 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36128
36129
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36129
36130
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36130
36131
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36133
+ const CORE_PACKAGE_VERSION = "0.6.3";
36134
+ const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36135
+ const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
36131
36136
  const SUPPLY_CHAIN_PLUGIN = "socket";
36132
36137
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36133
36138
  const SUPPLY_CHAIN_CATEGORY = "Security";
@@ -38812,6 +38817,48 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
38812
38817
  if (CACHE_DISABLED_VALUES$1.has(noFileCache)) return false;
38813
38818
  return true;
38814
38819
  } }) {};
38820
+ /**
38821
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
38822
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
38823
+ * diagnostics for every file whose dependency fingerprint (the probe set the
38824
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
38825
+ * rest. Only reachable when the per-file lint cache itself is on (the
38826
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
38827
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
38828
+ *
38829
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38830
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
38831
+ * per-file cache but run the always-fresh sidecar over every hit
38832
+ * (the pre-cache behavior).
38833
+ *
38834
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
38835
+ */
38836
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
38837
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38838
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
38839
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38840
+ if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
38841
+ return true;
38842
+ } }) {};
38843
+ /**
38844
+ * Whether the whole-project dead-code result cache
38845
+ * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
38846
+ * whose inputs (source tree, manifests, configs, analyzer version) are
38847
+ * unchanged replays the stored diagnostics instead of re-running the
38848
+ * analysis worker. Opt-OUT, two knobs (matching the per-file lint cache):
38849
+ *
38850
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38851
+ * - `REACT_DOCTOR_NO_DEAD_CODE_CACHE` — granular: bust only this cache.
38852
+ *
38853
+ * Tests override via `Layer.succeed(DeadCodeResultCacheEnabled, false)`.
38854
+ */
38855
+ var DeadCodeResultCacheEnabled = class extends Reference("react-doctor/DeadCodeResultCacheEnabled", { defaultValue: () => {
38856
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38857
+ const noDeadCodeCache = process.env["REACT_DOCTOR_NO_DEAD_CODE_CACHE"]?.toLowerCase() ?? "";
38858
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38859
+ if (CACHE_DISABLED_VALUES$1.has(noDeadCodeCache)) return false;
38860
+ return true;
38861
+ } }) {};
38815
38862
  const DIAGNOSTIC_SURFACES = [
38816
38863
  "cli",
38817
38864
  "prComment",
@@ -40544,6 +40591,80 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40544
40591
  return [...seen].filter((pattern) => pattern.length > 0);
40545
40592
  };
40546
40593
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40594
+ const atomicWriteJson = (filePath, value) => {
40595
+ try {
40596
+ NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
40597
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
40598
+ NFS.writeFileSync(temporaryPath, JSON.stringify(value));
40599
+ NFS.renameSync(temporaryPath, filePath);
40600
+ } catch {
40601
+ return;
40602
+ }
40603
+ };
40604
+ const failOpenReadJson = (filePath, fallback) => {
40605
+ try {
40606
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
40607
+ } catch {
40608
+ return fallback;
40609
+ }
40610
+ };
40611
+ const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40612
+ const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40613
+ const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40614
+ const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40615
+ const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40616
+ const collectAnalyzedFileFingerprints = (rootDirectory) => {
40617
+ const fingerprints = [];
40618
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40619
+ if (!isFingerprintedFile(name)) continue;
40620
+ try {
40621
+ const fileStat = NFS.statSync(absolutePath);
40622
+ const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
40623
+ fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
40624
+ } catch {}
40625
+ }
40626
+ return fingerprints.sort();
40627
+ };
40628
+ const bundledRequire$2 = createRequire(import.meta.url);
40629
+ const resolveDeslopVersion = () => {
40630
+ try {
40631
+ const packageJson = JSON.parse(NFS.readFileSync(bundledRequire$2.resolve("deslop-js/package.json"), "utf8"));
40632
+ return isRecord$1(packageJson) && typeof packageJson.version === "string" ? packageJson.version : "unknown";
40633
+ } catch {
40634
+ return "unknown";
40635
+ }
40636
+ };
40637
+ const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40638
+ schemaVersion: 1,
40639
+ coreVersion: input.coreVersion,
40640
+ deslopVersion: resolveDeslopVersion(),
40641
+ deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40642
+ entryPatterns: input.entryPatterns,
40643
+ ignorePatterns: input.ignorePatterns,
40644
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
40645
+ files: collectAnalyzedFileFingerprints(input.rootDirectory)
40646
+ })).digest("hex");
40647
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
40648
+ const decodeCachedDiagnostics = (raw) => {
40649
+ try {
40650
+ for (const entry of raw) validateDiagnostic$2(entry);
40651
+ return raw;
40652
+ } catch {
40653
+ return null;
40654
+ }
40655
+ };
40656
+ const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
40657
+ const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
40658
+ if (persisted === null || !isRecord$1(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
40659
+ return decodeCachedDiagnostics(persisted.diagnostics);
40660
+ };
40661
+ const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
40662
+ atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40663
+ version: 1,
40664
+ key: cacheKey,
40665
+ diagnostics
40666
+ });
40667
+ };
40547
40668
  const readSystemFacts = () => ({
40548
40669
  availableCores: os.availableParallelism(),
40549
40670
  totalMemoryBytes: os.totalmem(),
@@ -40612,6 +40733,21 @@ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40612
40733
  releaseSlot();
40613
40734
  }
40614
40735
  };
40736
+ const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
40737
+ const resolveReactDoctorCacheDir = (projectDirectory) => {
40738
+ const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
40739
+ if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
40740
+ const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
40741
+ if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
40742
+ return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
40743
+ };
40744
+ const resolveUserCacheScope = () => {
40745
+ try {
40746
+ return String(process.getuid?.() ?? os.userInfo().username);
40747
+ } catch {
40748
+ return "shared";
40749
+ }
40750
+ };
40615
40751
  /**
40616
40752
  * Resolves a path to its canonical, symlink-free form, falling back to
40617
40753
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40665,6 +40801,14 @@ process.stdin.on("end", () => {
40665
40801
  circularDependencies: result.circularDependencies.map((cycle) => ({
40666
40802
  files: cycle.files,
40667
40803
  })),
40804
+ ...(result.incrementalCacheStats
40805
+ ? {
40806
+ summaryCacheStats: {
40807
+ hits: result.incrementalCacheStats.summaryHits,
40808
+ misses: result.incrementalCacheStats.summaryMisses,
40809
+ },
40810
+ }
40811
+ : {}),
40668
40812
  });
40669
40813
 
40670
40814
  const serializeError = (error) =>
@@ -40688,6 +40832,9 @@ process.stdin.on("end", () => {
40688
40832
  ...(workerInput.ignorePatterns.length > 0
40689
40833
  ? { ignorePatterns: workerInput.ignorePatterns }
40690
40834
  : {}),
40835
+ ...(workerInput.incrementalCachePath
40836
+ ? { incrementalCachePath: workerInput.incrementalCachePath }
40837
+ : {}),
40691
40838
  // We consume only deslop's GRAPH-based findings (unusedFiles, unusedExports,
40692
40839
  // unusedDependencies, circularDependencies). Everything else deslop can compute
40693
40840
  // is pure wasted work for us, and it's the bulk of the runtime:
@@ -40698,12 +40845,18 @@ process.stdin.on("end", () => {
40698
40845
  // are the single most expensive pass — duplicate-block detection alone was
40699
40846
  // ~83s of a ~130s Sentry scan — so skipping them is an ~8.5x dead-code
40700
40847
  // speedup on a large repo.
40701
- // Both are provably safe: the consumed graph findings are computed by their own
40848
+ // - reportRedundancy: the DRY-pattern detectors (duplicate types/constants,
40849
+ // simplifiable functions, identity wrappers, …) — ~120 ms of discarded
40850
+ // output per scan, and skipping them lets the incremental cache drop the
40851
+ // DRY-pattern summary fields (the largest slice of the cache file).
40852
+ // All are provably safe: the consumed graph findings are computed by their own
40702
40853
  // detectors, independent of these passes (confirmed byte-identical on
40703
- // excalidraw + mui-material + sentry). tsConfigPath stays — the module resolver
40704
- // needs it for path-alias resolution in the import graph.
40854
+ // excalidraw + mui-material + sentry; re-verified on sentry after the
40855
+ // reportRedundancy flip). tsConfigPath stays — the module resolver needs it
40856
+ // for path-alias resolution in the import graph.
40705
40857
  semantic: { enabled: false },
40706
40858
  reportCodeQuality: false,
40859
+ reportRedundancy: false,
40707
40860
  };
40708
40861
  const result = await analyze(defineConfig(config));
40709
40862
  emit({ ok: true, result: normalizeResult(result) });
@@ -40787,13 +40940,23 @@ const parseCircularDependencies = (value) => {
40787
40940
  }
40788
40941
  return circularDependencies;
40789
40942
  };
40943
+ const parseSummaryCacheStats = (value) => {
40944
+ if (!isRecord$1(value)) return void 0;
40945
+ if (typeof value.hits !== "number" || typeof value.misses !== "number") return void 0;
40946
+ return {
40947
+ hits: value.hits,
40948
+ misses: value.misses
40949
+ };
40950
+ };
40790
40951
  const parseDeadCodeWorkerResult = (value) => {
40791
40952
  if (!isRecord$1(value)) throw new Error("Dead-code worker returned an invalid result.");
40953
+ const summaryCacheStats = parseSummaryCacheStats(value.summaryCacheStats);
40792
40954
  return {
40793
40955
  unusedFiles: parseUnusedFiles(value.unusedFiles),
40794
40956
  unusedExports: parseUnusedExports(value.unusedExports),
40795
40957
  unusedDependencies: parseUnusedDependencies(value.unusedDependencies),
40796
- circularDependencies: parseCircularDependencies(value.circularDependencies)
40958
+ circularDependencies: parseCircularDependencies(value.circularDependencies),
40959
+ ...summaryCacheStats ? { summaryCacheStats } : {}
40797
40960
  };
40798
40961
  };
40799
40962
  const parseDeadCodeWorkerError = (value) => {
@@ -40907,17 +41070,35 @@ const checkDeadCode = async (options) => {
40907
41070
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40908
41071
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40909
41072
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
41073
+ const tsConfigPath = resolveTsConfigPath(rootDirectory);
41074
+ const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
41075
+ const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
41076
+ rootDirectory,
41077
+ entryPatterns,
41078
+ ignorePatterns,
41079
+ tsConfigPath,
41080
+ deslopJsModuleSpecifier,
41081
+ coreVersion: CORE_PACKAGE_VERSION
41082
+ }) : null;
41083
+ if (cacheKey !== null) {
41084
+ const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
41085
+ options.onCacheOutcome?.(cachedDiagnostics !== null);
41086
+ if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41087
+ }
41088
+ const incrementalCachePath = options.cacheEnabled === true ? Path.join(resolveReactDoctorCacheDir(rootDirectory), DEAD_CODE_SUMMARY_CACHE_FILENAME) : void 0;
40910
41089
  const spawnAndRun = () => {
40911
41090
  return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40912
41091
  rootDirectory,
40913
41092
  entryPatterns,
40914
- tsConfigPath: resolveTsConfigPath(rootDirectory),
41093
+ tsConfigPath,
40915
41094
  ignorePatterns,
40916
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40917
- parseConcurrency: options.parseConcurrency
41095
+ deslopJsModuleSpecifier,
41096
+ parseConcurrency: options.parseConcurrency,
41097
+ incrementalCachePath
40918
41098
  }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40919
41099
  };
40920
41100
  const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
41101
+ if (result.summaryCacheStats !== void 0) options.onSummaryCacheStats?.(result.summaryCacheStats);
40921
41102
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40922
41103
  const diagnostics = [];
40923
41104
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -40974,6 +41155,7 @@ const checkDeadCode = async (options) => {
40974
41155
  category: DEAD_CODE_CATEGORY
40975
41156
  });
40976
41157
  }
41158
+ if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
40977
41159
  return diagnostics;
40978
41160
  };
40979
41161
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -41076,12 +41258,16 @@ var Config = class Config extends Service()("react-doctor/Config") {
41076
41258
  */
41077
41259
  var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
41078
41260
  static layerNode = succeed$3(DeadCode, DeadCode.of({ run: (input) => unwrap(fn("DeadCode.run")(function* () {
41261
+ const cacheEnabled = yield* DeadCodeResultCacheEnabled;
41079
41262
  return yield* tryPromise({
41080
41263
  try: (signal) => checkDeadCode({
41081
41264
  rootDirectory: input.rootDirectory,
41082
41265
  parseConcurrency: input.parseConcurrency,
41083
41266
  workerTimeoutMs: input.workerTimeoutMs,
41084
- abortSignal: signal
41267
+ abortSignal: signal,
41268
+ cacheEnabled,
41269
+ onCacheOutcome: input.onCacheOutcome,
41270
+ onSummaryCacheStats: input.onSummaryCacheStats
41085
41271
  }),
41086
41272
  catch: (cause) => new ReactDoctorError({ reason: new DeadCodeAnalysisFailed({ cause }) })
41087
41273
  }).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)));
@@ -41951,7 +42137,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
41951
42137
  }
41952
42138
  return enabled;
41953
42139
  };
41954
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
42140
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
41955
42141
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
41956
42142
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
41957
42143
  const jsPlugins = [];
@@ -41963,6 +42149,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
41963
42149
  if (!rule) continue;
41964
42150
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
41965
42151
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42152
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
41966
42153
  if (rule.scan !== void 0) continue;
41967
42154
  if (customRulesOnly && registryEntry.originallyExternal) continue;
41968
42155
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -42007,33 +42194,152 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42007
42194
  }
42008
42195
  };
42009
42196
  };
42010
- const atomicWriteJson = (filePath, value) => {
42197
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
42198
+ const decodeFileDiagnostics = (raw) => {
42199
+ if (!Array.isArray(raw)) return null;
42011
42200
  try {
42012
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
42013
- const temporaryPath = `${filePath}.${process.pid}.tmp`;
42014
- NFS.writeFileSync(temporaryPath, JSON.stringify(value));
42015
- NFS.renameSync(temporaryPath, filePath);
42201
+ for (const entry of raw) validateDiagnostic$1(entry);
42202
+ return raw;
42016
42203
  } catch {
42017
- return;
42204
+ return null;
42018
42205
  }
42019
42206
  };
42020
- const failOpenReadJson = (filePath, fallback) => {
42207
+ const emptyCache$1 = () => ({
42208
+ version: 1,
42209
+ rulesets: {}
42210
+ });
42211
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
42212
+ const entries = /* @__PURE__ */ new Map();
42213
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
42214
+ if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42215
+ const bucket = persisted.rulesets[rulesetHash];
42216
+ if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42217
+ for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
42218
+ const decoded = decodeFileDiagnostics(rawDiagnostics);
42219
+ if (decoded !== null) entries.set(fileKey, decoded);
42220
+ }
42221
+ return entries;
42222
+ };
42223
+ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42224
+ const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42225
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
42226
+ return {
42227
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
42228
+ store: (fileKey, diagnostics) => {
42229
+ entries.delete(fileKey);
42230
+ entries.set(fileKey, diagnostics);
42231
+ },
42232
+ persist: () => {
42233
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
42234
+ const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42235
+ const existingBucket = rulesets[rulesetHash];
42236
+ const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42237
+ const ourFiles = {};
42238
+ for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
42239
+ const cappedEntries = Object.entries({
42240
+ ...existingFiles,
42241
+ ...ourFiles
42242
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
42243
+ rulesets[rulesetHash] = {
42244
+ updatedAtMs: Date.now(),
42245
+ files: Object.fromEntries(cappedEntries)
42246
+ };
42247
+ atomicWriteJson(cacheFilePath, {
42248
+ version: 1,
42249
+ rulesets: Object.fromEntries(Object.entries(rulesets).filter(([, bucket]) => isRecord$1(bucket) && typeof bucket.updatedAtMs === "number").sort(([, first], [, second]) => second.updatedAtMs - first.updatedAtMs).slice(0, 8))
42250
+ });
42251
+ }
42252
+ };
42253
+ };
42254
+ const hashFileContents = (filePath) => {
42021
42255
  try {
42022
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
42256
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
42023
42257
  } catch {
42024
- return fallback;
42258
+ return null;
42025
42259
  }
42026
42260
  };
42261
+ const ABSENT_CONTENT_ANSWER = "absent";
42262
+ /**
42263
+ * Answers dependency probes for the sidecar lint cache — used both when
42264
+ * STORING an entry (turn the collector's probe paths into answers) and when
42265
+ * VERIFYING one (re-answer each stored probe against the current tree).
42266
+ * Store and verify must agree exactly, so both go through this one resolver.
42267
+ *
42268
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
42269
+ * (missing, a directory, permission failure). Source files reuse the
42270
+ * hashes the cache-partition loop already computed
42271
+ * (`contentHashByRelativePath`); everything else (package.json,
42272
+ * tsconfig, ignored files) is hashed on demand.
42273
+ * - `exists` — the existence class a module-resolution candidate probe
42274
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
42275
+ * `statSync` follows symlinks, matching the resolvers).
42276
+ *
42277
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
42278
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
42279
+ * tsconfig chain) repeat across many files' probe sets.
42280
+ */
42281
+ const createSidecarProbeAnswerResolver = (input) => {
42282
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
42283
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
42284
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
42285
+ const contentAnswer = (relativePath) => {
42286
+ const memoized = contentAnswerByPath.get(relativePath);
42287
+ if (memoized !== void 0) return memoized;
42288
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
42289
+ contentAnswerByPath.set(relativePath, answer);
42290
+ return answer;
42291
+ };
42292
+ const existsAnswer = (relativePath) => {
42293
+ const memoized = existsAnswerByPath.get(relativePath);
42294
+ if (memoized !== void 0) return memoized;
42295
+ let answer;
42296
+ try {
42297
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
42298
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
42299
+ } catch {
42300
+ answer = "none";
42301
+ }
42302
+ existsAnswerByPath.set(relativePath, answer);
42303
+ return answer;
42304
+ };
42305
+ return {
42306
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
42307
+ toRelativePath
42308
+ };
42309
+ };
42027
42310
  const validateDiagnostic = decodeUnknownSync(Diagnostic);
42028
- const decodeFileDiagnostics = (raw) => {
42029
- if (!Array.isArray(raw)) return null;
42311
+ const isProbeKind = (value) => value === "content" || value === "exists";
42312
+ const decodeSidecarFileEntry = (raw) => {
42313
+ if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
42314
+ const probes = [];
42315
+ for (const rawProbe of raw.probes) {
42316
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
42317
+ const [kind, probePath, answer] = rawProbe;
42318
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
42319
+ probes.push({
42320
+ kind,
42321
+ path: probePath,
42322
+ answer
42323
+ });
42324
+ }
42030
42325
  try {
42031
- for (const entry of raw) validateDiagnostic(entry);
42032
- return raw;
42326
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
42033
42327
  } catch {
42034
42328
  return null;
42035
42329
  }
42330
+ return {
42331
+ probes,
42332
+ diagnostics: raw.diagnostics
42333
+ };
42036
42334
  };
42335
+ const encodeSidecarFileEntry = (entry) => ({
42336
+ probes: entry.probes.map((probe) => [
42337
+ probe.kind,
42338
+ probe.path,
42339
+ probe.answer
42340
+ ]),
42341
+ diagnostics: entry.diagnostics
42342
+ });
42037
42343
  const emptyCache = () => ({
42038
42344
  version: 1,
42039
42345
  rulesets: {}
@@ -42044,20 +42350,20 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42044
42350
  if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42045
42351
  const bucket = persisted.rulesets[rulesetHash];
42046
42352
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42047
- for (const [fileKey, rawDiagnostics] of Object.entries(bucket.files)) {
42048
- const decoded = decodeFileDiagnostics(rawDiagnostics);
42353
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
42354
+ const decoded = decodeSidecarFileEntry(rawEntry);
42049
42355
  if (decoded !== null) entries.set(fileKey, decoded);
42050
42356
  }
42051
42357
  return entries;
42052
42358
  };
42053
- const createFileLintCache = (cacheDirectory, rulesetHash) => {
42054
- const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42359
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42360
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
42055
42361
  const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42056
42362
  return {
42057
42363
  lookup: (fileKey) => entries.get(fileKey) ?? null,
42058
- store: (fileKey, diagnostics) => {
42364
+ store: (fileKey, entry) => {
42059
42365
  entries.delete(fileKey);
42060
- entries.set(fileKey, diagnostics);
42366
+ entries.set(fileKey, entry);
42061
42367
  },
42062
42368
  persist: () => {
42063
42369
  const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
@@ -42065,7 +42371,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42065
42371
  const existingBucket = rulesets[rulesetHash];
42066
42372
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42067
42373
  const ourFiles = {};
42068
- for (const [fileKey, diagnostics] of entries) ourFiles[fileKey] = diagnostics;
42374
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
42069
42375
  const cappedEntries = Object.entries({
42070
42376
  ...existingFiles,
42071
42377
  ...ourFiles
@@ -42081,7 +42387,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42081
42387
  }
42082
42388
  };
42083
42389
  };
42084
- const bundledRequire$2 = createRequire(import.meta.url);
42390
+ const bundledRequire$3 = createRequire(import.meta.url);
42085
42391
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
42086
42392
  const resolveChildNodeVersion = (nodeBinaryPath) => {
42087
42393
  if (nodeBinaryPath === process.execPath) return process.version;
@@ -42107,7 +42413,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS$1 = [
42107
42413
  const resolveOxlintToolchainVersions = (nodeBinaryPath = process.execPath) => {
42108
42414
  const versions = [`node=${resolveChildNodeVersion(nodeBinaryPath)}`];
42109
42415
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS$1) try {
42110
- const packageJson = bundledRequire$2(specifier);
42416
+ const packageJson = bundledRequire$3(specifier);
42111
42417
  const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
42112
42418
  versions.push(`${specifier}=${version}`);
42113
42419
  } catch {
@@ -43080,13 +43386,6 @@ const validateRuleRegistration = () => {
43080
43386
  ].filter((entry) => entry !== null).join("; ");
43081
43387
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
43082
43388
  };
43083
- const hashFileContents = (filePath) => {
43084
- try {
43085
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
43086
- } catch {
43087
- return null;
43088
- }
43089
- };
43090
43389
  /**
43091
43390
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
43092
43391
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -43151,21 +43450,6 @@ const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
43151
43450
  }
43152
43451
  return batches.filter((batch) => batch.fileIndexes.length > 0).map((batch) => batch.fileIndexes.sort((leftIndex, rightIndex) => leftIndex - rightIndex).map((fileIndex) => files[fileIndex]));
43153
43452
  };
43154
- const projectCacheSubdir = (projectDirectory) => crypto.createHash("sha256").update(projectDirectory).digest("hex").slice(0, 16);
43155
- const resolveReactDoctorCacheDir = (projectDirectory) => {
43156
- const cacheDirOverride = process.env["REACT_DOCTOR_CACHE_DIR"]?.trim();
43157
- if (cacheDirOverride) return Path.join(cacheDirOverride, projectCacheSubdir(projectDirectory));
43158
- const nodeModulesDirectory = Path.join(projectDirectory, "node_modules");
43159
- if (NFS.existsSync(nodeModulesDirectory)) return Path.join(nodeModulesDirectory, ".cache", "react-doctor");
43160
- return Path.join(os.tmpdir(), `react-doctor-cache-${resolveUserCacheScope()}`, projectCacheSubdir(projectDirectory));
43161
- };
43162
- const resolveUserCacheScope = () => {
43163
- try {
43164
- return String(process.getuid?.() ?? os.userInfo().username);
43165
- } catch {
43166
- return "shared";
43167
- }
43168
- };
43169
43453
  /**
43170
43454
  * Atomically (re)writes the generated oxlintrc.json. Used twice in
43171
43455
  * the runner: once for the primary scan, once for the
@@ -43183,6 +43467,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
43183
43467
  NFS.closeSync(fileHandle);
43184
43468
  }
43185
43469
  };
43470
+ /**
43471
+ * Attributes diagnostics back to the file that produced them by the
43472
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
43473
+ * attributed — the path forms don't align, so the caller must skip its cache
43474
+ * store rather than risk caching a wrong empty result for a file that
43475
+ * actually had diagnostics.
43476
+ */
43477
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
43478
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
43479
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
43480
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
43481
+ for (const diagnostic of diagnostics) {
43482
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
43483
+ if (file === void 0) return null;
43484
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
43485
+ fileDiagnostics.push(diagnostic);
43486
+ diagnosticsByFile.set(file, fileDiagnostics);
43487
+ }
43488
+ return diagnosticsByFile;
43489
+ };
43490
+ /**
43491
+ * Collects each file's cross-file dependency probes (via the plugin's
43492
+ * collectors) with their current answers. Fail-open per file: an unreadable /
43493
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
43494
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
43495
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
43496
+ * unawaited before a lint pass and awaits it at store time, hiding the
43497
+ * in-process parse + resolution cost under the subprocess wait. The loop
43498
+ * yields on the cooperative time budget so the child's stdout handling (and
43499
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
43500
+ * the module-level probe recorder never sees interleaved use. Never rejects.
43501
+ */
43502
+ const collectSidecarProbesForFiles = async (input) => {
43503
+ const buildProbes = (file) => {
43504
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
43505
+ let trace;
43506
+ try {
43507
+ trace = collectCrossFileDependencyProbes({
43508
+ absoluteFilePath,
43509
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
43510
+ ruleIds: input.boundedSidecarRuleIds
43511
+ });
43512
+ } catch {
43513
+ return null;
43514
+ }
43515
+ if (trace === null) return null;
43516
+ const probes = [];
43517
+ for (const contentPath of trace.contentPaths) {
43518
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
43519
+ probes.push({
43520
+ kind: "content",
43521
+ path: relativePath,
43522
+ answer: input.probeAnswers.answerFor("content", relativePath)
43523
+ });
43524
+ }
43525
+ for (const existencePath of trace.existencePaths) {
43526
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
43527
+ probes.push({
43528
+ kind: "exists",
43529
+ path: relativePath,
43530
+ answer: input.probeAnswers.answerFor("exists", relativePath)
43531
+ });
43532
+ }
43533
+ return probes;
43534
+ };
43535
+ const probesByFile = /* @__PURE__ */ new Map();
43536
+ let collectSliceStartedAt = performance.now();
43537
+ for (const file of input.files) {
43538
+ probesByFile.set(file, buildProbes(file));
43539
+ if (performance.now() - collectSliceStartedAt >= 12) {
43540
+ await yieldToEventLoop();
43541
+ collectSliceStartedAt = performance.now();
43542
+ }
43543
+ }
43544
+ return probesByFile;
43545
+ };
43546
+ /**
43547
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
43548
+ * the next scan can replay instead of re-linting.
43549
+ */
43550
+ const storeSidecarEntries = (input) => {
43551
+ let didStoreAnyEntry = false;
43552
+ for (const pass of input.storablePasses) {
43553
+ if (!pass.isTrusted) continue;
43554
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
43555
+ if (diagnosticsByFile === null) continue;
43556
+ for (const file of pass.files) {
43557
+ const cacheKey = input.cacheKeyByFile.get(file);
43558
+ if (cacheKey === void 0) continue;
43559
+ const probes = pass.probesByFile.get(file);
43560
+ if (probes === void 0 || probes === null) continue;
43561
+ input.sidecarCache.store(cacheKey, {
43562
+ probes,
43563
+ diagnostics: diagnosticsByFile.get(file) ?? []
43564
+ });
43565
+ didStoreAnyEntry = true;
43566
+ }
43567
+ }
43568
+ if (didStoreAnyEntry) input.sidecarCache.persist();
43569
+ };
43186
43570
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
43187
43571
  /**
43188
43572
  * Detects an oxlint config-load crash caused by the optional
@@ -43224,7 +43608,7 @@ const reactHooksJsPluginDropNote = (error) => {
43224
43608
  * 6. always restore disable directives + clean up the temp dir
43225
43609
  */
43226
43610
  const runOxlint = async (options) => {
43227
- const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, onCacheStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
43611
+ const { rootDirectory, project, includePaths, nodeBinaryPath = process.execPath, customRulesOnly = false, respectInlineDisables = true, adoptExistingLintConfig = true, ignoredTags = /* @__PURE__ */ new Set(), userConfig, configSourceDirectory = rootDirectory, onPartialFailure, perFileLintCacheEnabled = false, sidecarLintCacheEnabled = false, onCacheStats, onSidecarStats, spawnTimeoutMs, outputMaxBytes, lintBatchOrdering = "cost" } = options;
43228
43612
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43229
43613
  const severityControls = buildRuleSeverityControls(userConfig);
43230
43614
  validateRuleRegistration();
@@ -43242,7 +43626,8 @@ const runOxlint = async (options) => {
43242
43626
  severityControls,
43243
43627
  userPlugins,
43244
43628
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
43245
- ruleSelection: overrides.ruleSelection
43629
+ ruleSelection: overrides.ruleSelection,
43630
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
43246
43631
  });
43247
43632
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
43248
43633
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -43345,15 +43730,18 @@ const runOxlint = async (options) => {
43345
43730
  });
43346
43731
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
43347
43732
  const cacheKeyByFile = /* @__PURE__ */ new Map();
43733
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
43348
43734
  const missFiles = [];
43349
43735
  const replayedDiagnostics = [];
43350
43736
  let hashSliceStartedAt = performance.now();
43351
43737
  for (const candidateFile of candidateFiles) {
43352
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
43738
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
43739
+ const contentHash = hashFileContents(absoluteCandidatePath);
43353
43740
  if (contentHash === null) {
43354
43741
  missFiles.push(candidateFile);
43355
43742
  continue;
43356
43743
  }
43744
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
43357
43745
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
43358
43746
  cacheKeyByFile.set(candidateFile, cacheKey);
43359
43747
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -43365,43 +43753,111 @@ const runOxlint = async (options) => {
43365
43753
  }
43366
43754
  }
43367
43755
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
43756
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
43757
+ extendsPaths: [],
43758
+ ruleSelection: "sidecar"
43759
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
43760
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43761
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
43762
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43763
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
43764
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
43765
+ config: buildConfig({
43766
+ extendsPaths: [],
43767
+ ruleSelection: "sidecar",
43768
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
43769
+ }),
43770
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
43771
+ ignorePatterns: combinedPatterns,
43772
+ tsconfigContent
43773
+ })) : null;
43774
+ const probeAnswers = createSidecarProbeAnswerResolver({
43775
+ rootDirectory,
43776
+ contentHashByRelativePath
43777
+ });
43778
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43779
+ files: missFiles,
43780
+ rootDirectory,
43781
+ boundedSidecarRuleIds,
43782
+ probeAnswers
43783
+ });
43368
43784
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
43369
43785
  extendsPaths: [],
43370
43786
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
43371
43787
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
43372
43788
  const missFileSet = new Set(missFiles);
43373
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
43789
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
43790
+ const sidecarReplayedDiagnostics = [];
43791
+ const sidecarLintFiles = [];
43792
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
43793
+ else {
43794
+ let verifySliceStartedAt = performance.now();
43795
+ for (const hitFile of hitFiles) {
43796
+ const cacheKey = cacheKeyByFile.get(hitFile);
43797
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
43798
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
43799
+ else sidecarLintFiles.push(hitFile);
43800
+ if (performance.now() - verifySliceStartedAt >= 12) {
43801
+ await yieldToEventLoop();
43802
+ verifySliceStartedAt = performance.now();
43803
+ }
43804
+ }
43805
+ }
43806
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43807
+ files: sidecarLintFiles,
43808
+ rootDirectory,
43809
+ boundedSidecarRuleIds,
43810
+ probeAnswers
43811
+ });
43812
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
43813
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
43814
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
43374
43815
  extendsPaths: [],
43375
- ruleSelection: "sidecar"
43376
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
43816
+ ruleSelection: "sidecar",
43817
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
43818
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
43819
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
43820
+ extendsPaths: [],
43821
+ ruleSelection: "sidecar",
43822
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
43823
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
43824
+ diagnostics: [],
43825
+ didDropReactHooksJsPlugin: false,
43826
+ hadPartialFailure: false
43827
+ };
43377
43828
  const freshCacheableDiagnostics = [];
43378
43829
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
43379
43830
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
43380
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
43381
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
43382
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
43383
- let isAttributionSound = true;
43384
- for (const diagnostic of freshCacheableDiagnostics) {
43385
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
43386
- if (missFile === void 0) {
43387
- isAttributionSound = false;
43388
- break;
43389
- }
43390
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
43391
- fileDiagnostics.push(diagnostic);
43392
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
43393
- }
43394
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
43831
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
43832
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
43833
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
43395
43834
  for (const missFile of missFiles) {
43396
43835
  const cacheKey = cacheKeyByFile.get(missFile);
43397
43836
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
43398
43837
  }
43399
43838
  cache.persist();
43400
43839
  }
43840
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
43841
+ sidecarCache,
43842
+ cacheKeyByFile,
43843
+ storablePasses: [{
43844
+ files: sidecarLintFiles,
43845
+ diagnostics: boundedSidecarResult.diagnostics,
43846
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
43847
+ probesByFile: await sidecarProbesTask
43848
+ }, {
43849
+ files: missFiles,
43850
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
43851
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
43852
+ probesByFile: await missProbesTask
43853
+ }]
43854
+ });
43401
43855
  return dedupeDiagnostics([
43402
43856
  ...replayedDiagnostics,
43403
43857
  ...fullResult.diagnostics,
43404
- ...sidecarResult.diagnostics
43858
+ ...sidecarReplayedDiagnostics,
43859
+ ...boundedSidecarResult.diagnostics,
43860
+ ...unboundedSidecarResult.diagnostics
43405
43861
  ]);
43406
43862
  }
43407
43863
  const baseArgs = makeBaseArgs(configPath);
@@ -43499,6 +43955,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43499
43955
  const concurrency = yield* OxlintConcurrency;
43500
43956
  const lintBatchOrdering = yield* LintBatchOrdering;
43501
43957
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
43958
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
43502
43959
  const collectedFailures = [];
43503
43960
  const diagnostics = yield* tryPromise({
43504
43961
  try: (signal) => runOxlint({
@@ -43517,7 +43974,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43517
43974
  },
43518
43975
  onFileProgress: input.onFileProgress,
43519
43976
  perFileLintCacheEnabled,
43977
+ sidecarLintCacheEnabled,
43520
43978
  onCacheStats: input.onCacheStats,
43979
+ onSidecarStats: input.onSidecarStats,
43521
43980
  spawnTimeoutMs,
43522
43981
  outputMaxBytes,
43523
43982
  concurrency,
@@ -44292,7 +44751,14 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44292
44751
  const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
44293
44752
  rootDirectory: scanDirectory,
44294
44753
  parseConcurrency: deadCodeParseConcurrency,
44295
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs
44754
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
44755
+ onCacheOutcome: (didHitCache) => {
44756
+ deadCodeCacheHit = didHitCache;
44757
+ },
44758
+ onSummaryCacheStats: (stats) => {
44759
+ deadCodeSummaryCacheHits = stats.hits;
44760
+ deadCodeSummaryCacheMisses = stats.misses;
44761
+ }
44296
44762
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44297
44763
  yield* set(deadCodeFailure, {
44298
44764
  didFail: true,
@@ -44320,6 +44786,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44320
44786
  let lastReportedTotalFileCount = 0;
44321
44787
  let lintCacheHitFileCount = null;
44322
44788
  let lintCacheTotalFileCount = null;
44789
+ let lintSidecarReplayedFileCount = null;
44790
+ let lintSidecarTotalFileCount = null;
44791
+ let deadCodeCacheHit = null;
44792
+ let deadCodeSummaryCacheHits = null;
44793
+ let deadCodeSummaryCacheMisses = null;
44323
44794
  const baseLintStream = linterService.run({
44324
44795
  rootDirectory: scanDirectory,
44325
44796
  project,
@@ -44339,6 +44810,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44339
44810
  lintCacheHitFileCount = cacheHitFileCount;
44340
44811
  lintCacheTotalFileCount = totalConsideredFileCount;
44341
44812
  },
44813
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
44814
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
44815
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
44816
+ },
44342
44817
  deadlineEpochMs: input.deadlineEpochMs
44343
44818
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44344
44819
  yield* set(lintFailure, {
@@ -44449,6 +44924,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44449
44924
  securityScanFailed,
44450
44925
  lintCacheHitFileCount,
44451
44926
  lintCacheTotalFileCount,
44927
+ lintSidecarReplayedFileCount,
44928
+ lintSidecarTotalFileCount,
44929
+ deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
44930
+ deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
44931
+ deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
44452
44932
  suppressedRuleCounts: transform.summarizeSuppressions()
44453
44933
  };
44454
44934
  }).pipe(withSpan("runInspect", { attributes: {
@@ -45230,6 +45710,7 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
45230
45710
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
45231
45711
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
45232
45712
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
45713
+ const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
45233
45714
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
45234
45715
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
45235
45716
  const WELCOME_EXPLANATION_HOLD_MS = 1e3;
@@ -45320,7 +45801,7 @@ const makeNoopConsole = () => ({
45320
45801
  });
45321
45802
  //#endregion
45322
45803
  //#region src/cli/utils/version.ts
45323
- const VERSION = "0.6.3-dev.8325343";
45804
+ const VERSION = "0.6.3-dev.bdf8074";
45324
45805
  //#endregion
45325
45806
  //#region src/cli/utils/json-mode.ts
45326
45807
  let context = null;
@@ -45689,13 +46170,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45689
46170
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45690
46171
  * standard `SENTRY_RELEASE` override.
45691
46172
  */
45692
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.8325343`;
46173
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.bdf8074`;
45693
46174
  /**
45694
46175
  * Deployment environment shown in Sentry's environment filter. Defaults to
45695
46176
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45696
46177
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45697
46178
  */
45698
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.8325343") ? "development" : "production");
46179
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.bdf8074") ? "development" : "production");
45699
46180
  /**
45700
46181
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45701
46182
  * (set to `0` to disable tracing) and falls back to
@@ -45896,7 +46377,8 @@ const runGit = (projectRoot, args) => {
45896
46377
  "ignore",
45897
46378
  "pipe",
45898
46379
  "ignore"
45899
- ]
46380
+ ],
46381
+ maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
45900
46382
  }).trim();
45901
46383
  } catch {
45902
46384
  return null;
@@ -57772,6 +58254,284 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
57772
58254
  })).toSorted((left, right) => right.fileCount - left.fileCount);
57773
58255
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
57774
58256
  //#endregion
58257
+ //#region src/cli/utils/scan-result-cache.ts
58258
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
58259
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
58260
+ "oxlint",
58261
+ "oxlint/package.json",
58262
+ "oxlint-plugin-react-doctor",
58263
+ "deslop-js/package.json",
58264
+ "eslint-plugin-react-hooks/package.json"
58265
+ ];
58266
+ const bundledRequire = createRequire(import.meta.url);
58267
+ const normalizeForStableJson = (value) => {
58268
+ if (value === null) return null;
58269
+ if (value === void 0) return void 0;
58270
+ const valueType = typeof value;
58271
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
58272
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
58273
+ if (Array.isArray(value)) return value.map((item) => {
58274
+ const normalized = normalizeForStableJson(item);
58275
+ return normalized === void 0 ? null : normalized;
58276
+ });
58277
+ const prototype = Object.getPrototypeOf(value);
58278
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
58279
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
58280
+ const normalizedRecord = {};
58281
+ for (const key of Object.keys(value).sort()) {
58282
+ const normalized = normalizeForStableJson(value[key]);
58283
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
58284
+ }
58285
+ return normalizedRecord;
58286
+ };
58287
+ const stringifyStableJson = (value) => {
58288
+ try {
58289
+ return JSON.stringify(normalizeForStableJson(value));
58290
+ } catch {
58291
+ return null;
58292
+ }
58293
+ };
58294
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
58295
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
58296
+ const parseWorktreeStatusRecords = (statusOutput) => {
58297
+ const entries = [];
58298
+ const tokens = statusOutput.split("\0");
58299
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
58300
+ let record = tokens[tokenIndex];
58301
+ if (record.length === 0) continue;
58302
+ if (record.length < 3 || record[2] !== " ") {
58303
+ if (tokenIndex !== 0) return null;
58304
+ record = ` ${record}`;
58305
+ }
58306
+ if (record.length < 4 || record[2] !== " ") return null;
58307
+ const statusCode = record.slice(0, 2);
58308
+ const recordPath = record.slice(3);
58309
+ if (statusCode.includes("R") || statusCode.includes("C")) {
58310
+ tokenIndex += 1;
58311
+ const originPath = tokens[tokenIndex];
58312
+ if (originPath === void 0 || originPath.length === 0) return null;
58313
+ entries.push({
58314
+ statusCode,
58315
+ path: recordPath,
58316
+ originPath
58317
+ });
58318
+ continue;
58319
+ }
58320
+ entries.push({
58321
+ statusCode,
58322
+ path: recordPath
58323
+ });
58324
+ }
58325
+ return entries;
58326
+ };
58327
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
58328
+ let stats;
58329
+ try {
58330
+ stats = fs.lstatSync(absolutePath);
58331
+ } catch {
58332
+ return statusCode.includes("D") ? "deleted" : null;
58333
+ }
58334
+ if (stats.isSymbolicLink()) try {
58335
+ return `link:${fs.readlinkSync(absolutePath)}`;
58336
+ } catch {
58337
+ return null;
58338
+ }
58339
+ if (!stats.isFile()) return null;
58340
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
58341
+ return hashFileContents(absolutePath);
58342
+ };
58343
+ const buildWorktreeFingerprint = (projectDirectory) => {
58344
+ const statusOutput = runGit(projectDirectory, [
58345
+ "status",
58346
+ "--porcelain=v1",
58347
+ "-z",
58348
+ "--untracked-files=all"
58349
+ ]);
58350
+ if (statusOutput === null) return null;
58351
+ if (statusOutput.length === 0) return [];
58352
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
58353
+ if (statusEntries === null) return null;
58354
+ if (statusEntries.length > 300) return null;
58355
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
58356
+ if (repositoryRoot === null) return null;
58357
+ const dirtyEntries = [];
58358
+ for (const statusEntry of statusEntries) {
58359
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
58360
+ if (contentFingerprint === null) return null;
58361
+ dirtyEntries.push({
58362
+ ...statusEntry,
58363
+ contentFingerprint
58364
+ });
58365
+ }
58366
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
58367
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
58368
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
58369
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
58370
+ });
58371
+ };
58372
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
58373
+ const resolveDotenvFingerprint = (projectDirectory) => {
58374
+ try {
58375
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${fileFingerprint(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
58376
+ } catch {
58377
+ return [];
58378
+ }
58379
+ };
58380
+ const hasHiddenTrackedFileState = (projectDirectory) => {
58381
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
58382
+ if (output === null) return true;
58383
+ return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58384
+ };
58385
+ const resolveCacheFilePath = (projectDirectory) => {
58386
+ const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
58387
+ if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
58388
+ const projectHash = hashString(projectDirectory).slice(0, 16);
58389
+ return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
58390
+ };
58391
+ const readPersistedCache = (cacheFilePath) => {
58392
+ try {
58393
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58394
+ if (!isRecord(parsed) || parsed.version !== 3) return {
58395
+ version: 3,
58396
+ entries: []
58397
+ };
58398
+ if (!Array.isArray(parsed.entries)) return {
58399
+ version: 3,
58400
+ entries: []
58401
+ };
58402
+ const entries = [];
58403
+ for (const entry of parsed.entries) {
58404
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
58405
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
58406
+ entries.push(entry);
58407
+ }
58408
+ return {
58409
+ version: 3,
58410
+ entries
58411
+ };
58412
+ } catch {
58413
+ return {
58414
+ version: 3,
58415
+ entries: []
58416
+ };
58417
+ }
58418
+ };
58419
+ const writePersistedCache = (cacheFilePath, cache) => {
58420
+ try {
58421
+ fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
58422
+ const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
58423
+ fs.writeFileSync(tempPath, JSON.stringify(cache));
58424
+ fs.renameSync(tempPath, cacheFilePath);
58425
+ } catch {
58426
+ return;
58427
+ }
58428
+ };
58429
+ /**
58430
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
58431
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
58432
+ * sidecar, and dead-code caches (their `Context.Reference` defaults read the
58433
+ * same variable). Exported for the wide event's `cache.temperature`
58434
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
58435
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
58436
+ * not consulted — they leave the other subsystems live.
58437
+ */
58438
+ const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
58439
+ const resolveProjectIdentity = (projectDirectory) => {
58440
+ try {
58441
+ return fs.realpathSync.native(projectDirectory);
58442
+ } catch {
58443
+ return path.resolve(projectDirectory);
58444
+ }
58445
+ };
58446
+ const fileFingerprint = (filePath) => {
58447
+ try {
58448
+ const stat = fs.statSync(filePath);
58449
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
58450
+ } catch {
58451
+ return null;
58452
+ }
58453
+ };
58454
+ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58455
+ const fingerprints = [];
58456
+ if (nodeBinaryPath !== null) {
58457
+ const fingerprint = fileFingerprint(nodeBinaryPath);
58458
+ if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
58459
+ }
58460
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
58461
+ const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
58462
+ if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
58463
+ } catch {
58464
+ continue;
58465
+ }
58466
+ return fingerprints;
58467
+ };
58468
+ const buildScanResultCacheKey = (input) => {
58469
+ if (isCacheGloballyDisabled()) return null;
58470
+ if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58471
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58472
+ if (worktreeFingerprint === null) return null;
58473
+ const headSha = readHeadSha(input.projectDirectory);
58474
+ if (headSha === null) return null;
58475
+ if (stringifyStableJson(input.userConfig) === null) return null;
58476
+ const cacheKeyJson = stringifyStableJson({
58477
+ schemaVersion: 3,
58478
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
58479
+ headSha,
58480
+ worktreeFingerprint,
58481
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
58482
+ reactDoctorVersion: input.version,
58483
+ nodeVersion: process.version,
58484
+ toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
58485
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
58486
+ hasConfigOverride: input.hasConfigOverride,
58487
+ configSourceDirectory: input.configSourceDirectory,
58488
+ userConfig: input.userConfig,
58489
+ engineOptions: {
58490
+ lint: input.options.lint,
58491
+ deadCode: input.options.deadCode,
58492
+ includePaths: [...input.options.includePaths].sort(),
58493
+ customRulesOnly: input.options.customRulesOnly,
58494
+ respectInlineDisables: input.options.respectInlineDisables,
58495
+ warnings: input.options.warnings,
58496
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
58497
+ ignoredTags: [...input.options.ignoredTags].sort(),
58498
+ concurrency: input.options.concurrency,
58499
+ lintBatchOrdering: resolveLintBatchOrdering(),
58500
+ baselineRef: input.options.baseline?.ref,
58501
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
58502
+ noScore: input.options.noScore,
58503
+ isCi: input.options.isCi,
58504
+ suppressRendering: input.options.suppressRendering,
58505
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
58506
+ }
58507
+ });
58508
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
58509
+ };
58510
+ const createScanResultCache = (projectDirectory) => {
58511
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
58512
+ const persistedCache = readPersistedCache(cacheFilePath);
58513
+ const entries = /* @__PURE__ */ new Map();
58514
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58515
+ const persist = () => {
58516
+ writePersistedCache(cacheFilePath, {
58517
+ version: 3,
58518
+ entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58519
+ });
58520
+ };
58521
+ return {
58522
+ lookup: (key) => entries.get(key)?.payload ?? null,
58523
+ store: (key, payload) => {
58524
+ entries.set(key, {
58525
+ key,
58526
+ createdAtMs: Date.now(),
58527
+ payload
58528
+ });
58529
+ persist();
58530
+ }
58531
+ };
58532
+ };
58533
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
58534
+ //#endregion
57775
58535
  //#region src/cli/utils/should-block-ci.ts
57776
58536
  const shouldBlockCi = (diagnostics, blockingLevel) => {
57777
58537
  if (blockingLevel === "none") return false;
@@ -57813,6 +58573,63 @@ const readEnvBoolean = (name) => {
57813
58573
  if (value === void 0) return null;
57814
58574
  return value.toLowerCase() === "true" || value === "1";
57815
58575
  };
58576
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
58577
+ const resolveDeadCodeReuseRatio = (result) => {
58578
+ if (result.deadCodeCacheHit === true) return 1;
58579
+ const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
58580
+ if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
58581
+ return result.deadCodeCacheHit === false ? 0 : null;
58582
+ };
58583
+ /**
58584
+ * One queryable cache temperature per scan, derived from the whole stack:
58585
+ *
58586
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
58587
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
58588
+ * branch; no scan work ran).
58589
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
58590
+ * replays, a dead-code whole-result hit, or dead-code
58591
+ * summary-cache hits.
58592
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
58593
+ * off-switch is on. Granular per-cache opt-outs
58594
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
58595
+ * since the other subsystems stay live.
58596
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
58597
+ *
58598
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
58599
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
58600
+ * replay ratio, dead-code reuse), skipping subsystems that never consulted a
58601
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
58602
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
58603
+ * p50/p90-able summary. Emitted only on the success path.
58604
+ */
58605
+ const buildCacheAttributes = (input) => {
58606
+ if (input.result === void 0) return {};
58607
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
58608
+ wholeRepoHit: true,
58609
+ temperature: "turbo",
58610
+ warmth: 1
58611
+ });
58612
+ const result = input.result;
58613
+ const subsystemReuseRatios = [
58614
+ ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58615
+ ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
58616
+ resolveDeadCodeReuseRatio(result)
58617
+ ];
58618
+ let knownSubsystemCount = 0;
58619
+ let reuseRatioSum = 0;
58620
+ for (const reuseRatio of subsystemReuseRatios) {
58621
+ if (reuseRatio === null) continue;
58622
+ knownSubsystemCount += 1;
58623
+ reuseRatioSum += reuseRatio;
58624
+ }
58625
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
58626
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
58627
+ return withNamespace("cache", {
58628
+ wholeRepoHit: input.wholeRepoCacheHit ?? null,
58629
+ temperature,
58630
+ warmth
58631
+ });
58632
+ };
57816
58633
  const resolveVersionPin = (versionInput) => {
57817
58634
  if (versionInput === void 0 || versionInput.trim() === "") return null;
57818
58635
  if (versionInput === "latest") return "latest";
@@ -57918,11 +58735,17 @@ const buildOutcomeAttributes = (input) => {
57918
58735
  deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
57919
58736
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
57920
58737
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
57921
- cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
58738
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58739
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
58740
+ sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
58741
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
57922
58742
  }),
57923
58743
  ...withNamespace("deadCode", {
57924
58744
  failed: input.didDeadCodeFail ?? null,
57925
- overlapped: input.deadCodeOverlapped ?? null
58745
+ overlapped: input.deadCodeOverlapped ?? null,
58746
+ cacheHit: result.deadCodeCacheHit ?? null,
58747
+ summaryCacheHits: result.deadCodeSummaryCacheHits ?? null,
58748
+ summaryCacheMisses: result.deadCodeSummaryCacheMisses ?? null
57926
58749
  }),
57927
58750
  ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
57928
58751
  ...withNamespace("securityScan", { failed: input.securityScanFailed ?? null }),
@@ -57982,7 +58805,7 @@ const buildScanAttributes = (input) => {
57982
58805
  * Projects a scan into the namespaced attribute set for its root span — the
57983
58806
  * canonical per-scan "wide event". Every attribute carries a dotted namespace
57984
58807
  * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
57985
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
58808
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
57986
58809
  * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
57987
58810
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
57988
58811
  * in the Spans dataset. Pure and exported so the projection (outcome
@@ -57996,7 +58819,8 @@ const buildScanAttributes = (input) => {
57996
58819
  const buildRunEventAttributes = (input) => toSpanAttributes({
57997
58820
  ...buildScanAttributes(input),
57998
58821
  ...buildActionAttributes(),
57999
- ...buildOutcomeAttributes(input)
58822
+ ...buildOutcomeAttributes(input),
58823
+ ...buildCacheAttributes(input)
58000
58824
  });
58001
58825
  /**
58002
58826
  * Stamps the wide-event attributes onto the run's root span. A guarded no-op
@@ -59206,196 +60030,6 @@ const resolveCliCategories = (categoryFlag) => {
59206
60030
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
59207
60031
  };
59208
60032
  //#endregion
59209
- //#region src/cli/utils/scan-result-cache.ts
59210
- const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59211
- const TOOLCHAIN_PACKAGE_SPECIFIERS = [
59212
- "oxlint",
59213
- "oxlint/package.json",
59214
- "oxlint-plugin-react-doctor",
59215
- "deslop-js/package.json",
59216
- "eslint-plugin-react-hooks/package.json"
59217
- ];
59218
- const bundledRequire = createRequire(import.meta.url);
59219
- const normalizeForStableJson = (value) => {
59220
- if (value === null) return null;
59221
- if (value === void 0) return void 0;
59222
- const valueType = typeof value;
59223
- if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
59224
- if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
59225
- if (Array.isArray(value)) return value.map((item) => {
59226
- const normalized = normalizeForStableJson(item);
59227
- return normalized === void 0 ? null : normalized;
59228
- });
59229
- const prototype = Object.getPrototypeOf(value);
59230
- if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
59231
- if (!isRecord(value)) throw new Error("Unsupported cache key object");
59232
- const normalizedRecord = {};
59233
- for (const key of Object.keys(value).sort()) {
59234
- const normalized = normalizeForStableJson(value[key]);
59235
- if (normalized !== void 0) normalizedRecord[key] = normalized;
59236
- }
59237
- return normalizedRecord;
59238
- };
59239
- const stringifyStableJson = (value) => {
59240
- try {
59241
- return JSON.stringify(normalizeForStableJson(value));
59242
- } catch {
59243
- return null;
59244
- }
59245
- };
59246
- const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
59247
- const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
59248
- const isWorktreeClean = (projectDirectory) => {
59249
- const status = runGit(projectDirectory, [
59250
- "status",
59251
- "--porcelain=v1",
59252
- "--untracked-files=normal"
59253
- ]);
59254
- return status !== null && status.length === 0;
59255
- };
59256
- const hasHiddenTrackedFileState = (projectDirectory) => {
59257
- const output = runGit(projectDirectory, ["ls-files", "-v"]);
59258
- if (output === null) return true;
59259
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
59260
- };
59261
- const resolveCacheFilePath = (projectDirectory) => {
59262
- const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
59263
- if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
59264
- const projectHash = hashString(projectDirectory).slice(0, 16);
59265
- return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
59266
- };
59267
- const readPersistedCache = (cacheFilePath) => {
59268
- try {
59269
- const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
59270
- if (!isRecord(parsed) || parsed.version !== 3) return {
59271
- version: 3,
59272
- entries: []
59273
- };
59274
- if (!Array.isArray(parsed.entries)) return {
59275
- version: 3,
59276
- entries: []
59277
- };
59278
- const entries = [];
59279
- for (const entry of parsed.entries) {
59280
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
59281
- if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
59282
- entries.push(entry);
59283
- }
59284
- return {
59285
- version: 3,
59286
- entries
59287
- };
59288
- } catch {
59289
- return {
59290
- version: 3,
59291
- entries: []
59292
- };
59293
- }
59294
- };
59295
- const writePersistedCache = (cacheFilePath, cache) => {
59296
- try {
59297
- fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
59298
- const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
59299
- fs.writeFileSync(tempPath, JSON.stringify(cache));
59300
- fs.renameSync(tempPath, cacheFilePath);
59301
- } catch {
59302
- return;
59303
- }
59304
- };
59305
- const isScanResultCacheDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
59306
- const resolveProjectIdentity = (projectDirectory) => {
59307
- try {
59308
- return fs.realpathSync.native(projectDirectory);
59309
- } catch {
59310
- return path.resolve(projectDirectory);
59311
- }
59312
- };
59313
- const fileFingerprint = (filePath) => {
59314
- try {
59315
- const stat = fs.statSync(filePath);
59316
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
59317
- } catch {
59318
- return null;
59319
- }
59320
- };
59321
- const resolveToolchainFingerprint = (nodeBinaryPath) => {
59322
- const fingerprints = [];
59323
- if (nodeBinaryPath !== null) {
59324
- const fingerprint = fileFingerprint(nodeBinaryPath);
59325
- if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
59326
- }
59327
- for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
59328
- const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
59329
- if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
59330
- } catch {
59331
- continue;
59332
- }
59333
- return fingerprints;
59334
- };
59335
- const buildScanResultCacheKey = (input) => {
59336
- if (isScanResultCacheDisabled()) return null;
59337
- if (!isWorktreeClean(input.projectDirectory)) return null;
59338
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59339
- const headSha = readHeadSha(input.projectDirectory);
59340
- if (headSha === null) return null;
59341
- if (stringifyStableJson(input.userConfig) === null) return null;
59342
- const cacheKeyJson = stringifyStableJson({
59343
- schemaVersion: 3,
59344
- projectIdentity: resolveProjectIdentity(input.projectDirectory),
59345
- headSha,
59346
- reactDoctorVersion: input.version,
59347
- nodeVersion: process.version,
59348
- toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
59349
- configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
59350
- hasConfigOverride: input.hasConfigOverride,
59351
- configSourceDirectory: input.configSourceDirectory,
59352
- userConfig: input.userConfig,
59353
- engineOptions: {
59354
- lint: input.options.lint,
59355
- deadCode: input.options.deadCode,
59356
- includePaths: [...input.options.includePaths].sort(),
59357
- customRulesOnly: input.options.customRulesOnly,
59358
- respectInlineDisables: input.options.respectInlineDisables,
59359
- warnings: input.options.warnings,
59360
- adoptExistingLintConfig: input.options.adoptExistingLintConfig,
59361
- ignoredTags: [...input.options.ignoredTags].sort(),
59362
- concurrency: input.options.concurrency,
59363
- lintBatchOrdering: resolveLintBatchOrdering(),
59364
- baselineRef: input.options.baseline?.ref,
59365
- changedLineRanges: input.options.changedLineRanges ?? void 0,
59366
- noScore: input.options.noScore,
59367
- isCi: input.options.isCi,
59368
- suppressRendering: input.options.suppressRendering,
59369
- supplyChainManifestChanged: input.options.supplyChainManifestChanged
59370
- }
59371
- });
59372
- return cacheKeyJson === null ? null : hashString(cacheKeyJson);
59373
- };
59374
- const createScanResultCache = (projectDirectory) => {
59375
- const cacheFilePath = resolveCacheFilePath(projectDirectory);
59376
- const persistedCache = readPersistedCache(cacheFilePath);
59377
- const entries = /* @__PURE__ */ new Map();
59378
- for (const entry of persistedCache.entries) entries.set(entry.key, entry);
59379
- const persist = () => {
59380
- writePersistedCache(cacheFilePath, {
59381
- version: 3,
59382
- entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
59383
- });
59384
- };
59385
- return {
59386
- lookup: (key) => entries.get(key)?.payload ?? null,
59387
- store: (key, payload) => {
59388
- entries.set(key, {
59389
- key,
59390
- createdAtMs: Date.now(),
59391
- payload
59392
- });
59393
- persist();
59394
- }
59395
- };
59396
- };
59397
- const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
59398
- //#endregion
59399
60033
  //#region src/inspect.ts
59400
60034
  const silentConsole = makeNoopConsole();
59401
60035
  const runConsole = (effect) => {
@@ -59625,7 +60259,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59625
60259
  startTime,
59626
60260
  rootSentrySpan,
59627
60261
  scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59628
- baselineDegraded
60262
+ baselineDegraded,
60263
+ wholeRepoCacheHit: true
59629
60264
  });
59630
60265
  recordOnboardingCompletion(options);
59631
60266
  return result;
@@ -59734,8 +60369,14 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59734
60369
  rootSentrySpan,
59735
60370
  scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59736
60371
  baselineDegraded,
60372
+ wholeRepoCacheHit: false,
59737
60373
  lintCacheHitFileCount: output.lintCacheHitFileCount,
59738
- lintCacheTotalFileCount: output.lintCacheTotalFileCount
60374
+ lintCacheTotalFileCount: output.lintCacheTotalFileCount,
60375
+ lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
60376
+ lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
60377
+ deadCodeCacheHit: output.deadCodeCacheHit,
60378
+ deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
60379
+ deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
59739
60380
  });
59740
60381
  recordOnboardingCompletion(options);
59741
60382
  return result;
@@ -59770,6 +60411,11 @@ const renderAndRecordScan = async (input) => {
59770
60411
  scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
59771
60412
  lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
59772
60413
  lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
60414
+ lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
60415
+ lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
60416
+ deadCodeCacheHit: input.deadCodeCacheHit ?? null,
60417
+ deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
60418
+ deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
59773
60419
  baselineDelta: input.payload.baselineDelta
59774
60420
  }), input.options.silent));
59775
60421
  const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
@@ -59794,6 +60440,7 @@ const renderAndRecordScan = async (input) => {
59794
60440
  result,
59795
60441
  mode: input.scanMode,
59796
60442
  gateExempt: input.baselineDegraded,
60443
+ wholeRepoCacheHit: input.wholeRepoCacheHit,
59797
60444
  didLintFail: input.payload.didLintFail,
59798
60445
  lintFailureReasonKind: input.payload.lintFailureReasonKind,
59799
60446
  lintPartialFailureCount: input.payload.lintPartialFailures.length,
@@ -59808,7 +60455,7 @@ const renderAndRecordScan = async (input) => {
59808
60455
  return result;
59809
60456
  };
59810
60457
  const finalizeAndRender = (input) => gen(function* () {
59811
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, baselineDelta } = input;
60458
+ const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, lintSidecarReplayedFileCount, lintSidecarTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
59812
60459
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
59813
60460
  didLintFail,
59814
60461
  lintFailureReason,
@@ -59832,6 +60479,15 @@ const finalizeAndRender = (input) => gen(function* () {
59832
60479
  lintCacheHitFileCount,
59833
60480
  lintCacheTotalFileCount
59834
60481
  } : {},
60482
+ ...lintSidecarTotalFileCount !== null ? {
60483
+ lintSidecarReplayedFileCount,
60484
+ lintSidecarTotalFileCount
60485
+ } : {},
60486
+ ...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
60487
+ ...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
60488
+ deadCodeSummaryCacheHits,
60489
+ deadCodeSummaryCacheMisses
60490
+ } : {},
59835
60491
  ...baselineDelta ? { baselineDelta } : {}
59836
60492
  });
59837
60493
  if (options.suppressRendering) return buildResult();
@@ -63678,4 +64334,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63678
64334
  export {};
63679
64335
 
63680
64336
  //# sourceMappingURL=cli.js.map
63681
- //# debugId=2921b8ff-14cf-5604-8ae5-b65b4f1347fc
64337
+ //# debugId=4d2b3e2c-ffb8-5da4-bf5d-833e608822bb