react-doctor 0.6.3-dev.ce49250 → 0.6.3-dev.ced746f

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]="cb06d1e4-d50b-5715-af77-13eb97c43e07")}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]="1722becf-6e91-56ae-afc9-1e1410647bc3")}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";
@@ -36129,6 +36129,7 @@ const SOCKET_PACKAGE_PAGE_BASE = "https://socket.dev/npm/package";
36129
36129
  const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
36130
36130
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
36131
36131
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
36132
+ const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
36132
36133
  const CORE_PACKAGE_VERSION = "0.6.3";
36133
36134
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
36134
36135
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
@@ -36136,6 +36137,7 @@ const SUPPLY_CHAIN_PLUGIN = "socket";
36136
36137
  const SUPPLY_CHAIN_RULE = "low-supply-chain-score";
36137
36138
  const SUPPLY_CHAIN_CATEGORY = "Security";
36138
36139
  const SUPPLY_CHAIN_OVERLAP_TIMEOUT_MS = 9e4;
36140
+ const SUPPLY_CHAIN_CACHE_TTL_MS = 864e5;
36139
36141
  const SUPPLY_CHAIN_CACHE_SUBDIR = "supply-chain";
36140
36142
  const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
36141
36143
  const hasIgnoredPathSegment = (relativePath) => relativePath.split(/[/\\]/).slice(0, -1).some((segment) => IGNORED_DIRECTORIES.has(segment));
@@ -38817,6 +38819,29 @@ var PerFileLintCacheEnabled = class extends Reference("react-doctor/PerFileLintC
38817
38819
  return true;
38818
38820
  } }) {};
38819
38821
  /**
38822
+ * Whether the sidecar lint cache (`runners/oxlint/sidecar-lint-cache.ts`) is
38823
+ * active. Defaults ON — warm rescans replay the cross-file rules' cached
38824
+ * diagnostics for every file whose dependency fingerprint (the probe set the
38825
+ * plugin's collectors recorded) still matches the tree, and re-lint only the
38826
+ * rest. Only reachable when the per-file lint cache itself is on (the
38827
+ * sidecar covers its cache hits), so `REACT_DOCTOR_NO_CACHE` /
38828
+ * `REACT_DOCTOR_NO_FILE_CACHE` implicitly disable it too. Opt-OUT knobs:
38829
+ *
38830
+ * - `REACT_DOCTOR_NO_CACHE` — the global off-switch.
38831
+ * - `REACT_DOCTOR_NO_SIDECAR_CACHE` — granular rollback hatch: keep the
38832
+ * per-file cache but run the always-fresh sidecar over every hit
38833
+ * (the pre-cache behavior).
38834
+ *
38835
+ * Tests override via `Layer.succeed(SidecarLintCacheEnabled, false)`.
38836
+ */
38837
+ var SidecarLintCacheEnabled = class extends Reference("react-doctor/SidecarLintCacheEnabled", { defaultValue: () => {
38838
+ const noCache = process.env["REACT_DOCTOR_NO_CACHE"]?.toLowerCase() ?? "";
38839
+ const noSidecarCache = process.env["REACT_DOCTOR_NO_SIDECAR_CACHE"]?.toLowerCase() ?? "";
38840
+ if (CACHE_DISABLED_VALUES$1.has(noCache)) return false;
38841
+ if (CACHE_DISABLED_VALUES$1.has(noSidecarCache)) return false;
38842
+ return true;
38843
+ } }) {};
38844
+ /**
38820
38845
  * Whether the whole-project dead-code result cache
38821
38846
  * (`dead-code/dead-code-result-cache.ts`) is active. Defaults ON — a rescan
38822
38847
  * whose inputs (source tree, manifests, configs, analyzer version) are
@@ -40584,22 +40609,37 @@ const failOpenReadJson = (filePath, fallback) => {
40584
40609
  return fallback;
40585
40610
  }
40586
40611
  };
40612
+ const hashFileContents = (filePath) => {
40613
+ try {
40614
+ return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
40615
+ } catch {
40616
+ return null;
40617
+ }
40618
+ };
40587
40619
  const ANALYZED_FILE_EXTENSIONS = new Set(DEFAULT_EXTENSIONS);
40588
40620
  const CORE_SIDE_MANIFEST_NAMES = ["knip.json", "deno.lock"];
40589
40621
  const ANALYZED_MANIFEST_NAMES = new Set([...ANALYZED_MANIFEST_FILENAMES, ...CORE_SIDE_MANIFEST_NAMES]);
40590
40622
  const isTsConfigLikeFile = (fileName) => (fileName.startsWith("tsconfig") || fileName.startsWith("jsconfig")) && fileName.endsWith(".json");
40591
40623
  const isFingerprintedFile = (fileName) => ANALYZED_FILE_EXTENSIONS.has(Path.extname(fileName).toLowerCase()) || ANALYZED_MANIFEST_NAMES.has(fileName) || isTsConfigLikeFile(fileName);
40592
- const collectAnalyzedFileFingerprints = (rootDirectory) => {
40593
- const fingerprints = [];
40624
+ /**
40625
+ * Stat snapshot of every file the analysis reads, keyed by root-relative
40626
+ * `/`-separated path. Taken BEFORE the (long) analysis so a stored result is
40627
+ * verified against the tree it started from.
40628
+ */
40629
+ const collectAnalyzedFileStats = (rootDirectory) => {
40630
+ const statByRelativePath = /* @__PURE__ */ new Map();
40594
40631
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
40595
40632
  if (!isFingerprintedFile(name)) continue;
40596
40633
  try {
40597
40634
  const fileStat = NFS.statSync(absolutePath);
40598
40635
  const relativePath = Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/");
40599
- fingerprints.push(`${relativePath}:${fileStat.mtimeMs}:${fileStat.size}`);
40636
+ statByRelativePath.set(relativePath, {
40637
+ mtimeMs: fileStat.mtimeMs,
40638
+ size: fileStat.size
40639
+ });
40600
40640
  } catch {}
40601
40641
  }
40602
- return fingerprints.sort();
40642
+ return statByRelativePath;
40603
40643
  };
40604
40644
  const bundledRequire$2 = createRequire(import.meta.url);
40605
40645
  const resolveDeslopVersion = () => {
@@ -40611,34 +40651,85 @@ const resolveDeslopVersion = () => {
40611
40651
  }
40612
40652
  };
40613
40653
  const computeDeadCodeCacheKey = (input) => crypto.createHash("sha1").update(JSON.stringify({
40614
- schemaVersion: 1,
40654
+ schemaVersion: 2,
40615
40655
  coreVersion: input.coreVersion,
40616
40656
  deslopVersion: resolveDeslopVersion(),
40617
40657
  deslopJsModuleSpecifier: input.deslopJsModuleSpecifier,
40618
40658
  entryPatterns: input.entryPatterns,
40619
40659
  ignorePatterns: input.ignorePatterns,
40620
- tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/"),
40621
- files: collectAnalyzedFileFingerprints(input.rootDirectory)
40660
+ tsConfigFile: input.tsConfigPath === void 0 ? null : Path.relative(input.rootDirectory, input.tsConfigPath).replace(/\\/g, "/")
40622
40661
  })).digest("hex");
40623
- const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
40662
+ const validateDiagnostic$2 = decodeUnknownSync(Diagnostic);
40624
40663
  const decodeCachedDiagnostics = (raw) => {
40625
40664
  try {
40626
- for (const entry of raw) validateDiagnostic$1(entry);
40665
+ for (const entry of raw) validateDiagnostic$2(entry);
40627
40666
  return raw;
40628
40667
  } catch {
40629
40668
  return null;
40630
40669
  }
40631
40670
  };
40632
- const lookupDeadCodeResultCache = (cacheDirectory, cacheKey) => {
40633
- const persisted = failOpenReadJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), null);
40634
- if (persisted === null || !isRecord$1(persisted) || persisted.version !== 1 || persisted.key !== cacheKey || !Array.isArray(persisted.diagnostics)) return null;
40635
- return decodeCachedDiagnostics(persisted.diagnostics);
40671
+ const isPersistedFileIdentity = (value) => Array.isArray(value) && value.length === 3 && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "string";
40672
+ const lookupDeadCodeResultCache = (input) => {
40673
+ const cacheFilePath = Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME);
40674
+ const persisted = failOpenReadJson(cacheFilePath, null);
40675
+ if (persisted === null || !isRecord$1(persisted) || persisted.version !== 2 || persisted.key !== input.cacheKey || !isRecord$1(persisted.files) || !Array.isArray(persisted.diagnostics)) return null;
40676
+ const storedFileEntries = Object.entries(persisted.files);
40677
+ if (storedFileEntries.length !== input.currentFileStats.size) return null;
40678
+ const repairedFiles = {};
40679
+ let repairedCount = 0;
40680
+ for (const [relativePath, storedIdentity] of storedFileEntries) {
40681
+ if (!isPersistedFileIdentity(storedIdentity)) return null;
40682
+ const currentStat = input.currentFileStats.get(relativePath);
40683
+ if (currentStat === void 0) return null;
40684
+ const [storedMtimeMs, storedSize, storedContentHash] = storedIdentity;
40685
+ if (currentStat.mtimeMs === storedMtimeMs && currentStat.size === storedSize) {
40686
+ repairedFiles[relativePath] = storedIdentity;
40687
+ continue;
40688
+ }
40689
+ if (currentStat.size !== storedSize) return null;
40690
+ const currentContentHash = hashFileContents(Path.join(input.rootDirectory, relativePath));
40691
+ if (currentContentHash === null || currentContentHash !== storedContentHash) return null;
40692
+ repairedFiles[relativePath] = [
40693
+ currentStat.mtimeMs,
40694
+ currentStat.size,
40695
+ storedContentHash
40696
+ ];
40697
+ repairedCount += 1;
40698
+ }
40699
+ const diagnostics = decodeCachedDiagnostics(persisted.diagnostics);
40700
+ if (diagnostics === null) return null;
40701
+ if (repairedCount > 0) atomicWriteJson(cacheFilePath, {
40702
+ version: 2,
40703
+ key: input.cacheKey,
40704
+ files: repairedFiles,
40705
+ diagnostics: persisted.diagnostics
40706
+ });
40707
+ return diagnostics;
40636
40708
  };
40637
- const storeDeadCodeResultCache = (cacheDirectory, cacheKey, diagnostics) => {
40638
- atomicWriteJson(Path.join(cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40639
- version: 1,
40640
- key: cacheKey,
40641
- diagnostics
40709
+ const storeDeadCodeResultCache = (input) => {
40710
+ const persistedFiles = {};
40711
+ for (const [relativePath, snapshotStat] of input.snapshotFileStats) {
40712
+ const absolutePath = Path.join(input.rootDirectory, relativePath);
40713
+ const contentHash = hashFileContents(absolutePath);
40714
+ if (contentHash === null) return;
40715
+ let currentStat;
40716
+ try {
40717
+ currentStat = NFS.statSync(absolutePath);
40718
+ } catch {
40719
+ return;
40720
+ }
40721
+ if (currentStat.mtimeMs !== snapshotStat.mtimeMs || currentStat.size !== snapshotStat.size) return;
40722
+ persistedFiles[relativePath] = [
40723
+ snapshotStat.mtimeMs,
40724
+ snapshotStat.size,
40725
+ contentHash
40726
+ ];
40727
+ }
40728
+ atomicWriteJson(Path.join(input.cacheDirectory, DEAD_CODE_CACHE_FILENAME), {
40729
+ version: 2,
40730
+ key: input.cacheKey,
40731
+ files: persistedFiles,
40732
+ diagnostics: input.diagnostics
40642
40733
  });
40643
40734
  };
40644
40735
  const readSystemFacts = () => ({
@@ -41048,16 +41139,22 @@ const checkDeadCode = async (options) => {
41048
41139
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
41049
41140
  const tsConfigPath = resolveTsConfigPath(rootDirectory);
41050
41141
  const deslopJsModuleSpecifier = options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js");
41051
- const cacheKey = options.cacheEnabled ? computeDeadCodeCacheKey({
41142
+ const fileStatsSnapshot = options.cacheEnabled ? collectAnalyzedFileStats(rootDirectory) : null;
41143
+ const cacheKey = fileStatsSnapshot === null ? null : computeDeadCodeCacheKey({
41052
41144
  rootDirectory,
41053
41145
  entryPatterns,
41054
41146
  ignorePatterns,
41055
41147
  tsConfigPath,
41056
41148
  deslopJsModuleSpecifier,
41057
41149
  coreVersion: CORE_PACKAGE_VERSION
41058
- }) : null;
41059
- if (cacheKey !== null) {
41060
- const cachedDiagnostics = lookupDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey);
41150
+ });
41151
+ if (cacheKey !== null && fileStatsSnapshot !== null) {
41152
+ const cachedDiagnostics = lookupDeadCodeResultCache({
41153
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41154
+ cacheKey,
41155
+ rootDirectory,
41156
+ currentFileStats: fileStatsSnapshot
41157
+ });
41061
41158
  options.onCacheOutcome?.(cachedDiagnostics !== null);
41062
41159
  if (cachedDiagnostics !== null) return [...cachedDiagnostics];
41063
41160
  }
@@ -41131,7 +41228,13 @@ const checkDeadCode = async (options) => {
41131
41228
  category: DEAD_CODE_CATEGORY
41132
41229
  });
41133
41230
  }
41134
- if (cacheKey !== null) storeDeadCodeResultCache(resolveReactDoctorCacheDir(rootDirectory), cacheKey, diagnostics);
41231
+ if (cacheKey !== null && fileStatsSnapshot !== null) storeDeadCodeResultCache({
41232
+ cacheDirectory: resolveReactDoctorCacheDir(rootDirectory),
41233
+ cacheKey,
41234
+ rootDirectory,
41235
+ snapshotFileStats: fileStatsSnapshot,
41236
+ diagnostics
41237
+ });
41135
41238
  return diagnostics;
41136
41239
  };
41137
41240
  const DEAD_CODE_RULE_KEY_PREFIX = `${DEAD_CODE_PLUGIN}/`;
@@ -42113,7 +42216,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
42113
42216
  }
42114
42217
  return enabled;
42115
42218
  };
42116
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection }) => {
42219
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
42117
42220
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
42118
42221
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
42119
42222
  const jsPlugins = [];
@@ -42125,6 +42228,7 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42125
42228
  if (!rule) continue;
42126
42229
  if (ruleSelection === "cacheable" && CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42127
42230
  if (ruleSelection === "sidecar" && !CROSS_FILE_RULE_IDS.has(registryEntry.id)) continue;
42231
+ if (ruleSelection === "sidecar" && sidecarRuleIdFilter !== void 0 && !sidecarRuleIdFilter.has(registryEntry.id)) continue;
42128
42232
  if (rule.scan !== void 0) continue;
42129
42233
  if (customRulesOnly && registryEntry.originallyExternal) continue;
42130
42234
  if (rule.framework !== "global" && !rule.requires) continue;
@@ -42169,23 +42273,23 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
42169
42273
  }
42170
42274
  };
42171
42275
  };
42172
- const validateDiagnostic = decodeUnknownSync(Diagnostic);
42276
+ const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
42173
42277
  const decodeFileDiagnostics = (raw) => {
42174
42278
  if (!Array.isArray(raw)) return null;
42175
42279
  try {
42176
- for (const entry of raw) validateDiagnostic(entry);
42280
+ for (const entry of raw) validateDiagnostic$1(entry);
42177
42281
  return raw;
42178
42282
  } catch {
42179
42283
  return null;
42180
42284
  }
42181
42285
  };
42182
- const emptyCache = () => ({
42286
+ const emptyCache$1 = () => ({
42183
42287
  version: 1,
42184
42288
  rulesets: {}
42185
42289
  });
42186
- const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42290
+ const loadRulesetEntries$1 = (cacheFilePath, rulesetHash) => {
42187
42291
  const entries = /* @__PURE__ */ new Map();
42188
- const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42292
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache$1());
42189
42293
  if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42190
42294
  const bucket = persisted.rulesets[rulesetHash];
42191
42295
  if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
@@ -42197,7 +42301,7 @@ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42197
42301
  };
42198
42302
  const createFileLintCache = (cacheDirectory, rulesetHash) => {
42199
42303
  const cacheFilePath = Path.join(cacheDirectory, FILE_LINT_CACHE_FILENAME);
42200
- const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42304
+ const entries = loadRulesetEntries$1(cacheFilePath, rulesetHash);
42201
42305
  return {
42202
42306
  lookup: (fileKey) => entries.get(fileKey) ?? null,
42203
42307
  store: (fileKey, diagnostics) => {
@@ -42205,7 +42309,7 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42205
42309
  entries.set(fileKey, diagnostics);
42206
42310
  },
42207
42311
  persist: () => {
42208
- const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42312
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache$1());
42209
42313
  const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42210
42314
  const existingBucket = rulesets[rulesetHash];
42211
42315
  const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
@@ -42226,6 +42330,135 @@ const createFileLintCache = (cacheDirectory, rulesetHash) => {
42226
42330
  }
42227
42331
  };
42228
42332
  };
42333
+ const ABSENT_CONTENT_ANSWER = "absent";
42334
+ /**
42335
+ * Answers dependency probes for the sidecar lint cache — used both when
42336
+ * STORING an entry (turn the collector's probe paths into answers) and when
42337
+ * VERIFYING one (re-answer each stored probe against the current tree).
42338
+ * Store and verify must agree exactly, so both go through this one resolver.
42339
+ *
42340
+ * - `content` — SHA-1 of the file's bytes, `"absent"` when unreadable
42341
+ * (missing, a directory, permission failure). Source files reuse the
42342
+ * hashes the cache-partition loop already computed
42343
+ * (`contentHashByRelativePath`); everything else (package.json,
42344
+ * tsconfig, ignored files) is hashed on demand.
42345
+ * - `exists` — the existence class a module-resolution candidate probe
42346
+ * sees: `"file"`, `"dir"`, or `"none"` (missing or an exotic entry —
42347
+ * `statSync` follows symlinks, matching the resolvers).
42348
+ *
42349
+ * Answers are memoized per scan: the filesystem is frozen for the run, and
42350
+ * hot dependency paths (a barrel imported by hundreds of files, the shared
42351
+ * tsconfig chain) repeat across many files' probe sets.
42352
+ */
42353
+ const createSidecarProbeAnswerResolver = (input) => {
42354
+ const contentAnswerByPath = /* @__PURE__ */ new Map();
42355
+ const existsAnswerByPath = /* @__PURE__ */ new Map();
42356
+ const toRelativePath = (absolutePath) => Path.relative(input.rootDirectory, absolutePath).replaceAll("\\", "/");
42357
+ const contentAnswer = (relativePath) => {
42358
+ const memoized = contentAnswerByPath.get(relativePath);
42359
+ if (memoized !== void 0) return memoized;
42360
+ const answer = input.contentHashByRelativePath.get(relativePath) ?? hashFileContents(Path.resolve(input.rootDirectory, relativePath)) ?? ABSENT_CONTENT_ANSWER;
42361
+ contentAnswerByPath.set(relativePath, answer);
42362
+ return answer;
42363
+ };
42364
+ const existsAnswer = (relativePath) => {
42365
+ const memoized = existsAnswerByPath.get(relativePath);
42366
+ if (memoized !== void 0) return memoized;
42367
+ let answer;
42368
+ try {
42369
+ const stat = NFS.statSync(Path.resolve(input.rootDirectory, relativePath));
42370
+ answer = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "none";
42371
+ } catch {
42372
+ answer = "none";
42373
+ }
42374
+ existsAnswerByPath.set(relativePath, answer);
42375
+ return answer;
42376
+ };
42377
+ return {
42378
+ answerFor: (kind, relativePath) => kind === "content" ? contentAnswer(relativePath) : existsAnswer(relativePath),
42379
+ toRelativePath
42380
+ };
42381
+ };
42382
+ const validateDiagnostic = decodeUnknownSync(Diagnostic);
42383
+ const isProbeKind = (value) => value === "content" || value === "exists";
42384
+ const decodeSidecarFileEntry = (raw) => {
42385
+ if (!isRecord$1(raw) || !Array.isArray(raw.probes) || !Array.isArray(raw.diagnostics)) return null;
42386
+ const probes = [];
42387
+ for (const rawProbe of raw.probes) {
42388
+ if (!Array.isArray(rawProbe) || rawProbe.length !== 3) return null;
42389
+ const [kind, probePath, answer] = rawProbe;
42390
+ if (!isProbeKind(kind) || typeof probePath !== "string" || typeof answer !== "string") return null;
42391
+ probes.push({
42392
+ kind,
42393
+ path: probePath,
42394
+ answer
42395
+ });
42396
+ }
42397
+ try {
42398
+ for (const entry of raw.diagnostics) validateDiagnostic(entry);
42399
+ } catch {
42400
+ return null;
42401
+ }
42402
+ return {
42403
+ probes,
42404
+ diagnostics: raw.diagnostics
42405
+ };
42406
+ };
42407
+ const encodeSidecarFileEntry = (entry) => ({
42408
+ probes: entry.probes.map((probe) => [
42409
+ probe.kind,
42410
+ probe.path,
42411
+ probe.answer
42412
+ ]),
42413
+ diagnostics: entry.diagnostics
42414
+ });
42415
+ const emptyCache = () => ({
42416
+ version: 1,
42417
+ rulesets: {}
42418
+ });
42419
+ const loadRulesetEntries = (cacheFilePath, rulesetHash) => {
42420
+ const entries = /* @__PURE__ */ new Map();
42421
+ const persisted = failOpenReadJson(cacheFilePath, emptyCache());
42422
+ if (persisted.version !== 1 || !isRecord$1(persisted.rulesets)) return entries;
42423
+ const bucket = persisted.rulesets[rulesetHash];
42424
+ if (!isRecord$1(bucket) || !isRecord$1(bucket.files)) return entries;
42425
+ for (const [fileKey, rawEntry] of Object.entries(bucket.files)) {
42426
+ const decoded = decodeSidecarFileEntry(rawEntry);
42427
+ if (decoded !== null) entries.set(fileKey, decoded);
42428
+ }
42429
+ return entries;
42430
+ };
42431
+ const createSidecarLintCache = (cacheDirectory, rulesetHash) => {
42432
+ const cacheFilePath = Path.join(cacheDirectory, SIDECAR_LINT_CACHE_FILENAME);
42433
+ const entries = loadRulesetEntries(cacheFilePath, rulesetHash);
42434
+ return {
42435
+ lookup: (fileKey) => entries.get(fileKey) ?? null,
42436
+ store: (fileKey, entry) => {
42437
+ entries.delete(fileKey);
42438
+ entries.set(fileKey, entry);
42439
+ },
42440
+ persist: () => {
42441
+ const onDisk = failOpenReadJson(cacheFilePath, emptyCache());
42442
+ const rulesets = onDisk.version === 1 && isRecord$1(onDisk.rulesets) ? { ...onDisk.rulesets } : {};
42443
+ const existingBucket = rulesets[rulesetHash];
42444
+ const existingFiles = isRecord$1(existingBucket) && isRecord$1(existingBucket.files) ? existingBucket.files : {};
42445
+ const ourFiles = {};
42446
+ for (const [fileKey, entry] of entries) ourFiles[fileKey] = encodeSidecarFileEntry(entry);
42447
+ const cappedEntries = Object.entries({
42448
+ ...existingFiles,
42449
+ ...ourFiles
42450
+ }).slice(-FILE_LINT_CACHE_MAX_FILE_COUNT);
42451
+ rulesets[rulesetHash] = {
42452
+ updatedAtMs: Date.now(),
42453
+ files: Object.fromEntries(cappedEntries)
42454
+ };
42455
+ atomicWriteJson(cacheFilePath, {
42456
+ version: 1,
42457
+ 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))
42458
+ });
42459
+ }
42460
+ };
42461
+ };
42229
42462
  const bundledRequire$3 = createRequire(import.meta.url);
42230
42463
  const childNodeVersionByBinaryPath = /* @__PURE__ */ new Map();
42231
42464
  const resolveChildNodeVersion = (nodeBinaryPath) => {
@@ -43225,13 +43458,6 @@ const validateRuleRegistration = () => {
43225
43458
  ].filter((entry) => entry !== null).join("; ");
43226
43459
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
43227
43460
  };
43228
- const hashFileContents = (filePath) => {
43229
- try {
43230
- return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
43231
- } catch {
43232
- return null;
43233
- }
43234
- };
43235
43461
  /**
43236
43462
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
43237
43463
  * `batchIncludePaths` greedily fills 100-file batches and leaves a small
@@ -43313,6 +43539,106 @@ const writeOxlintConfig = (configPath, configToWrite) => {
43313
43539
  NFS.closeSync(fileHandle);
43314
43540
  }
43315
43541
  };
43542
+ /**
43543
+ * Attributes diagnostics back to the file that produced them by the
43544
+ * normalized path oxlint echoes. Returns `null` when ANY diagnostic can't be
43545
+ * attributed — the path forms don't align, so the caller must skip its cache
43546
+ * store rather than risk caching a wrong empty result for a file that
43547
+ * actually had diagnostics.
43548
+ */
43549
+ const attributeDiagnosticsToFiles = (diagnostics, files) => {
43550
+ const fileByNormalizedPath = /* @__PURE__ */ new Map();
43551
+ for (const file of files) fileByNormalizedPath.set(file.replaceAll("\\", "/"), file);
43552
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
43553
+ for (const diagnostic of diagnostics) {
43554
+ const file = fileByNormalizedPath.get(diagnostic.filePath);
43555
+ if (file === void 0) return null;
43556
+ const fileDiagnostics = diagnosticsByFile.get(file) ?? [];
43557
+ fileDiagnostics.push(diagnostic);
43558
+ diagnosticsByFile.set(file, fileDiagnostics);
43559
+ }
43560
+ return diagnosticsByFile;
43561
+ };
43562
+ /**
43563
+ * Collects each file's cross-file dependency probes (via the plugin's
43564
+ * collectors) with their current answers. Fail-open per file: an unreadable /
43565
+ * unparseable / collector-failing file maps to `null` (no entry stored — it
43566
+ * re-lints next scan). The filesystem is frozen for the scan, so this can run
43567
+ * CONCURRENTLY with an oxlint child-process await — the caller starts it
43568
+ * unawaited before a lint pass and awaits it at store time, hiding the
43569
+ * in-process parse + resolution cost under the subprocess wait. The loop
43570
+ * yields on the cooperative time budget so the child's stdout handling (and
43571
+ * sibling fibers) keep flowing; each per-file collection is synchronous, so
43572
+ * the module-level probe recorder never sees interleaved use. Never rejects.
43573
+ */
43574
+ const collectSidecarProbesForFiles = async (input) => {
43575
+ const buildProbes = (file) => {
43576
+ const absoluteFilePath = Path.resolve(input.rootDirectory, file);
43577
+ let trace;
43578
+ try {
43579
+ trace = collectCrossFileDependencyProbes({
43580
+ absoluteFilePath,
43581
+ sourceText: NFS.readFileSync(absoluteFilePath, "utf8"),
43582
+ ruleIds: input.boundedSidecarRuleIds
43583
+ });
43584
+ } catch {
43585
+ return null;
43586
+ }
43587
+ if (trace === null) return null;
43588
+ const probes = [];
43589
+ for (const contentPath of trace.contentPaths) {
43590
+ const relativePath = input.probeAnswers.toRelativePath(contentPath);
43591
+ probes.push({
43592
+ kind: "content",
43593
+ path: relativePath,
43594
+ answer: input.probeAnswers.answerFor("content", relativePath)
43595
+ });
43596
+ }
43597
+ for (const existencePath of trace.existencePaths) {
43598
+ const relativePath = input.probeAnswers.toRelativePath(existencePath);
43599
+ probes.push({
43600
+ kind: "exists",
43601
+ path: relativePath,
43602
+ answer: input.probeAnswers.answerFor("exists", relativePath)
43603
+ });
43604
+ }
43605
+ return probes;
43606
+ };
43607
+ const probesByFile = /* @__PURE__ */ new Map();
43608
+ let collectSliceStartedAt = performance.now();
43609
+ for (const file of input.files) {
43610
+ probesByFile.set(file, buildProbes(file));
43611
+ if (performance.now() - collectSliceStartedAt >= 12) {
43612
+ await yieldToEventLoop();
43613
+ collectSliceStartedAt = performance.now();
43614
+ }
43615
+ }
43616
+ return probesByFile;
43617
+ };
43618
+ /**
43619
+ * Stores each freshly-linted file's probe set + cross-file diagnostics so
43620
+ * the next scan can replay instead of re-linting.
43621
+ */
43622
+ const storeSidecarEntries = (input) => {
43623
+ let didStoreAnyEntry = false;
43624
+ for (const pass of input.storablePasses) {
43625
+ if (!pass.isTrusted) continue;
43626
+ const diagnosticsByFile = attributeDiagnosticsToFiles(pass.diagnostics, pass.files);
43627
+ if (diagnosticsByFile === null) continue;
43628
+ for (const file of pass.files) {
43629
+ const cacheKey = input.cacheKeyByFile.get(file);
43630
+ if (cacheKey === void 0) continue;
43631
+ const probes = pass.probesByFile.get(file);
43632
+ if (probes === void 0 || probes === null) continue;
43633
+ input.sidecarCache.store(cacheKey, {
43634
+ probes,
43635
+ diagnostics: diagnosticsByFile.get(file) ?? []
43636
+ });
43637
+ didStoreAnyEntry = true;
43638
+ }
43639
+ }
43640
+ if (didStoreAnyEntry) input.sidecarCache.persist();
43641
+ };
43316
43642
  const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
43317
43643
  /**
43318
43644
  * Detects an oxlint config-load crash caused by the optional
@@ -43354,7 +43680,7 @@ const reactHooksJsPluginDropNote = (error) => {
43354
43680
  * 6. always restore disable directives + clean up the temp dir
43355
43681
  */
43356
43682
  const runOxlint = async (options) => {
43357
- 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;
43683
+ 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;
43358
43684
  const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames) ? userConfig.serverAuthFunctionNames.filter((entry) => typeof entry === "string" && entry.length > 0) : void 0;
43359
43685
  const severityControls = buildRuleSeverityControls(userConfig);
43360
43686
  validateRuleRegistration();
@@ -43372,7 +43698,8 @@ const runOxlint = async (options) => {
43372
43698
  severityControls,
43373
43699
  userPlugins,
43374
43700
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
43375
- ruleSelection: overrides.ruleSelection
43701
+ ruleSelection: overrides.ruleSelection,
43702
+ sidecarRuleIdFilter: overrides.sidecarRuleIdFilter
43376
43703
  });
43377
43704
  const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
43378
43705
  const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
@@ -43475,15 +43802,18 @@ const runOxlint = async (options) => {
43475
43802
  });
43476
43803
  const cache = createFileLintCache(resolveReactDoctorCacheDir(rootDirectory), rulesetHash);
43477
43804
  const cacheKeyByFile = /* @__PURE__ */ new Map();
43805
+ const contentHashByRelativePath = /* @__PURE__ */ new Map();
43478
43806
  const missFiles = [];
43479
43807
  const replayedDiagnostics = [];
43480
43808
  let hashSliceStartedAt = performance.now();
43481
43809
  for (const candidateFile of candidateFiles) {
43482
- const contentHash = hashFileContents(Path.resolve(rootDirectory, candidateFile));
43810
+ const absoluteCandidatePath = Path.resolve(rootDirectory, candidateFile);
43811
+ const contentHash = hashFileContents(absoluteCandidatePath);
43483
43812
  if (contentHash === null) {
43484
43813
  missFiles.push(candidateFile);
43485
43814
  continue;
43486
43815
  }
43816
+ contentHashByRelativePath.set(Path.relative(rootDirectory, absoluteCandidatePath).replaceAll("\\", "/"), contentHash);
43487
43817
  const cacheKey = `${candidateFile.replaceAll("\\", "/")}\u0000${contentHash}`;
43488
43818
  cacheKeyByFile.set(candidateFile, cacheKey);
43489
43819
  const cachedDiagnostics = cache.lookup(cacheKey);
@@ -43495,43 +43825,111 @@ const runOxlint = async (options) => {
43495
43825
  }
43496
43826
  }
43497
43827
  const cacheHitFileCount = candidateFiles.length - missFiles.length;
43828
+ const enabledSidecarRuleIds = Object.keys(buildConfig({
43829
+ extendsPaths: [],
43830
+ ruleSelection: "sidecar"
43831
+ }).rules).map((ruleKey) => ruleKey.slice(ruleKey.lastIndexOf("/") + 1)).filter((ruleId) => CROSS_FILE_RULE_IDS.has(ruleId));
43832
+ const boundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43833
+ const boundedSidecarRuleIdSet = new Set(boundedSidecarRuleIds);
43834
+ const unboundedSidecarRuleIds = enabledSidecarRuleIds.filter((ruleId) => !CROSS_FILE_DEPENDENCY_COLLECTORS.has(ruleId));
43835
+ const useSidecarCache = sidecarLintCacheEnabled && boundedSidecarRuleIds.length > 0;
43836
+ const sidecarCache = useSidecarCache ? createSidecarLintCache(resolveReactDoctorCacheDir(rootDirectory), computeRulesetHash({
43837
+ config: buildConfig({
43838
+ extendsPaths: [],
43839
+ ruleSelection: "sidecar",
43840
+ sidecarRuleIdFilter: boundedSidecarRuleIdSet
43841
+ }),
43842
+ toolchainVersions: resolveOxlintToolchainVersions(nodeBinaryPath),
43843
+ ignorePatterns: combinedPatterns,
43844
+ tsconfigContent
43845
+ })) : null;
43846
+ const probeAnswers = createSidecarProbeAnswerResolver({
43847
+ rootDirectory,
43848
+ contentHashByRelativePath
43849
+ });
43850
+ const missProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43851
+ files: missFiles,
43852
+ rootDirectory,
43853
+ boundedSidecarRuleIds,
43854
+ probeAnswers
43855
+ });
43498
43856
  const fullResult = await runConfigOverFiles((overrides) => buildConfig({
43499
43857
  extendsPaths: [],
43500
43858
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
43501
43859
  }), "oxlintrc.full.json", missFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(scannedFileCount, candidateFiles.length)));
43502
43860
  const missFileSet = new Set(missFiles);
43503
- const sidecarResult = await runConfigOverFiles(() => buildConfig({
43861
+ const hitFiles = candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile));
43862
+ const sidecarReplayedDiagnostics = [];
43863
+ const sidecarLintFiles = [];
43864
+ if (sidecarCache === null) sidecarLintFiles.push(...hitFiles);
43865
+ else {
43866
+ let verifySliceStartedAt = performance.now();
43867
+ for (const hitFile of hitFiles) {
43868
+ const cacheKey = cacheKeyByFile.get(hitFile);
43869
+ const entry = cacheKey === void 0 ? null : sidecarCache.lookup(cacheKey);
43870
+ if (entry !== null && entry.probes.every((probe) => probeAnswers.answerFor(probe.kind, probe.path) === probe.answer)) sidecarReplayedDiagnostics.push(...entry.diagnostics);
43871
+ else sidecarLintFiles.push(hitFile);
43872
+ if (performance.now() - verifySliceStartedAt >= 12) {
43873
+ await yieldToEventLoop();
43874
+ verifySliceStartedAt = performance.now();
43875
+ }
43876
+ }
43877
+ }
43878
+ const sidecarProbesTask = sidecarCache === null ? null : collectSidecarProbesForFiles({
43879
+ files: sidecarLintFiles,
43880
+ rootDirectory,
43881
+ boundedSidecarRuleIds,
43882
+ probeAnswers
43883
+ });
43884
+ const sidecarReplayedFileCount = hitFiles.length - sidecarLintFiles.length;
43885
+ if (sidecarReplayedFileCount > 0) options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount, candidateFiles.length);
43886
+ const boundedSidecarResult = await runConfigOverFiles(() => buildConfig({
43504
43887
  extendsPaths: [],
43505
- ruleSelection: "sidecar"
43506
- }), "oxlintrc.sidecar.json", candidateFiles.filter((candidateFile) => !missFileSet.has(candidateFile)), options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + scannedFileCount, candidateFiles.length)));
43888
+ ruleSelection: "sidecar",
43889
+ ...useSidecarCache ? { sidecarRuleIdFilter: boundedSidecarRuleIdSet } : {}
43890
+ }), "oxlintrc.sidecar.json", sidecarLintFiles, options.onFileProgress && ((scannedFileCount) => options.onFileProgress?.(missFiles.length + sidecarReplayedFileCount + scannedFileCount, candidateFiles.length)));
43891
+ const unboundedSidecarResult = useSidecarCache && unboundedSidecarRuleIds.length > 0 ? await runConfigOverFiles(() => buildConfig({
43892
+ extendsPaths: [],
43893
+ ruleSelection: "sidecar",
43894
+ sidecarRuleIdFilter: new Set(unboundedSidecarRuleIds)
43895
+ }), "oxlintrc.sidecar-unbounded.json", hitFiles, void 0) : {
43896
+ diagnostics: [],
43897
+ didDropReactHooksJsPlugin: false,
43898
+ hadPartialFailure: false
43899
+ };
43507
43900
  const freshCacheableDiagnostics = [];
43508
43901
  for (const diagnostic of fullResult.diagnostics) if (!CROSS_FILE_RULE_IDS.has(diagnostic.rule)) freshCacheableDiagnostics.push(diagnostic);
43509
43902
  onCacheStats?.(cacheHitFileCount, candidateFiles.length);
43510
- const missFileByNormalizedPath = /* @__PURE__ */ new Map();
43511
- for (const missFile of missFiles) missFileByNormalizedPath.set(missFile.replaceAll("\\", "/"), missFile);
43512
- const freshDiagnosticsByFile = /* @__PURE__ */ new Map();
43513
- let isAttributionSound = true;
43514
- for (const diagnostic of freshCacheableDiagnostics) {
43515
- const missFile = missFileByNormalizedPath.get(diagnostic.filePath);
43516
- if (missFile === void 0) {
43517
- isAttributionSound = false;
43518
- break;
43519
- }
43520
- const fileDiagnostics = freshDiagnosticsByFile.get(missFile) ?? [];
43521
- fileDiagnostics.push(diagnostic);
43522
- freshDiagnosticsByFile.set(missFile, fileDiagnostics);
43523
- }
43524
- if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && isAttributionSound) {
43903
+ if (sidecarCache !== null) onSidecarStats?.(hitFiles.length - sidecarLintFiles.length, hitFiles.length);
43904
+ const freshDiagnosticsByFile = attributeDiagnosticsToFiles(freshCacheableDiagnostics, missFiles);
43905
+ if (!fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure && freshDiagnosticsByFile !== null) {
43525
43906
  for (const missFile of missFiles) {
43526
43907
  const cacheKey = cacheKeyByFile.get(missFile);
43527
43908
  if (cacheKey !== void 0) cache.store(cacheKey, freshDiagnosticsByFile.get(missFile) ?? []);
43528
43909
  }
43529
43910
  cache.persist();
43530
43911
  }
43912
+ if (sidecarCache !== null && missProbesTask !== null && sidecarProbesTask !== null) storeSidecarEntries({
43913
+ sidecarCache,
43914
+ cacheKeyByFile,
43915
+ storablePasses: [{
43916
+ files: sidecarLintFiles,
43917
+ diagnostics: boundedSidecarResult.diagnostics,
43918
+ isTrusted: !boundedSidecarResult.hadPartialFailure,
43919
+ probesByFile: await sidecarProbesTask
43920
+ }, {
43921
+ files: missFiles,
43922
+ diagnostics: fullResult.diagnostics.filter((diagnostic) => boundedSidecarRuleIdSet.has(diagnostic.rule)),
43923
+ isTrusted: !fullResult.didDropReactHooksJsPlugin && !fullResult.hadPartialFailure,
43924
+ probesByFile: await missProbesTask
43925
+ }]
43926
+ });
43531
43927
  return dedupeDiagnostics([
43532
43928
  ...replayedDiagnostics,
43533
43929
  ...fullResult.diagnostics,
43534
- ...sidecarResult.diagnostics
43930
+ ...sidecarReplayedDiagnostics,
43931
+ ...boundedSidecarResult.diagnostics,
43932
+ ...unboundedSidecarResult.diagnostics
43535
43933
  ]);
43536
43934
  }
43537
43935
  const baseArgs = makeBaseArgs(configPath);
@@ -43629,6 +44027,7 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43629
44027
  const concurrency = yield* OxlintConcurrency;
43630
44028
  const lintBatchOrdering = yield* LintBatchOrdering;
43631
44029
  const perFileLintCacheEnabled = yield* PerFileLintCacheEnabled;
44030
+ const sidecarLintCacheEnabled = yield* SidecarLintCacheEnabled;
43632
44031
  const collectedFailures = [];
43633
44032
  const diagnostics = yield* tryPromise({
43634
44033
  try: (signal) => runOxlint({
@@ -43647,7 +44046,9 @@ var Linter = class Linter extends Service()("react-doctor/Linter") {
43647
44046
  },
43648
44047
  onFileProgress: input.onFileProgress,
43649
44048
  perFileLintCacheEnabled,
44049
+ sidecarLintCacheEnabled,
43650
44050
  onCacheStats: input.onCacheStats,
44051
+ onSidecarStats: input.onSidecarStats,
43651
44052
  spawnTimeoutMs,
43652
44053
  outputMaxBytes,
43653
44054
  concurrency,
@@ -44069,6 +44470,20 @@ const writeCachedSocketBody = (cacheFile, body) => {
44069
44470
  }));
44070
44471
  } catch {}
44071
44472
  };
44473
+ const pruneExpiredSocketCache = (cacheDirectory) => {
44474
+ try {
44475
+ const supplyChainCacheDirectory = Path.join(cacheDirectory, SUPPLY_CHAIN_CACHE_SUBDIR);
44476
+ const expiryThresholdMs = Date.now() - SUPPLY_CHAIN_CACHE_TTL_MS;
44477
+ for (const entryName of NFS.readdirSync(supplyChainCacheDirectory)) {
44478
+ const entryPath = Path.join(supplyChainCacheDirectory, entryName);
44479
+ try {
44480
+ if (NFS.statSync(entryPath).mtimeMs < expiryThresholdMs) NFS.rmSync(entryPath);
44481
+ } catch {
44482
+ continue;
44483
+ }
44484
+ }
44485
+ } catch {}
44486
+ };
44072
44487
  const fetchSocketArtifact = (dependency, cacheDirectory) => tryPromise(async (signal) => {
44073
44488
  const cacheFile = cacheDirectory === null ? null : supplyChainCacheFile(cacheDirectory, dependency);
44074
44489
  if (cacheFile !== null) {
@@ -44196,6 +44611,7 @@ const checkSupplyChain = (input) => gen(function* () {
44196
44611
  const dependencies = collectDependenciesToScore(readPackageJson$1(packageJsonPath), readPackageJsonText(packageJsonPath), options.includeDevDependencies);
44197
44612
  if (dependencies.length === 0) return [];
44198
44613
  const cacheDirectory = isSupplyChainCacheDisabled() ? null : resolveReactDoctorCacheDir(input.rootDirectory);
44614
+ if (cacheDirectory !== null) pruneExpiredSocketCache(cacheDirectory);
44199
44615
  const artifacts = yield* forEach$1(dependencies, (dependency) => fetchSocketArtifact(dependency, cacheDirectory), { concurrency: 8 }).pipe(timeoutOption(input.totalTimeoutMs ?? 9e4), map$3((maybeArtifacts) => getOrElse$1(maybeArtifacts, () => [])));
44200
44616
  const diagnostics = [];
44201
44617
  for (let index = 0; index < dependencies.length; index += 1) {
@@ -44457,6 +44873,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44457
44873
  let lastReportedTotalFileCount = 0;
44458
44874
  let lintCacheHitFileCount = null;
44459
44875
  let lintCacheTotalFileCount = null;
44876
+ let lintSidecarReplayedFileCount = null;
44877
+ let lintSidecarTotalFileCount = null;
44460
44878
  let deadCodeCacheHit = null;
44461
44879
  let deadCodeSummaryCacheHits = null;
44462
44880
  let deadCodeSummaryCacheMisses = null;
@@ -44479,6 +44897,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44479
44897
  lintCacheHitFileCount = cacheHitFileCount;
44480
44898
  lintCacheTotalFileCount = totalConsideredFileCount;
44481
44899
  },
44900
+ onSidecarStats: (sidecarReplayedFileCount, sidecarConsideredFileCount) => {
44901
+ lintSidecarReplayedFileCount = sidecarReplayedFileCount;
44902
+ lintSidecarTotalFileCount = sidecarConsideredFileCount;
44903
+ },
44482
44904
  deadlineEpochMs: input.deadlineEpochMs
44483
44905
  }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
44484
44906
  yield* set(lintFailure, {
@@ -44589,6 +45011,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44589
45011
  securityScanFailed,
44590
45012
  lintCacheHitFileCount,
44591
45013
  lintCacheTotalFileCount,
45014
+ lintSidecarReplayedFileCount,
45015
+ lintSidecarTotalFileCount,
44592
45016
  deadCodeCacheHit: lintFailureState.didFail ? null : deadCodeCacheHit,
44593
45017
  deadCodeSummaryCacheHits: lintFailureState.didFail ? null : deadCodeSummaryCacheHits,
44594
45018
  deadCodeSummaryCacheMisses: lintFailureState.didFail ? null : deadCodeSummaryCacheMisses,
@@ -45073,11 +45497,9 @@ const computeConfigFingerprint = (projectDirectory, version) => {
45073
45497
  const parts = [`v=${version}`];
45074
45498
  let directory = projectDirectory;
45075
45499
  for (;;) {
45076
- for (const filename of CONFIG_FINGERPRINT_FILENAMES) try {
45077
- const stat = fs.statSync(path.join(directory, filename));
45078
- parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`);
45079
- } catch {
45080
- continue;
45500
+ for (const filename of CONFIG_FINGERPRINT_FILENAMES) {
45501
+ const contentHash = hashFileContents(path.join(directory, filename));
45502
+ if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`);
45081
45503
  }
45082
45504
  const parent = path.dirname(directory);
45083
45505
  if (parent === directory) break;
@@ -45373,6 +45795,7 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
45373
45795
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
45374
45796
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
45375
45797
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
45798
+ const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
45376
45799
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
45377
45800
  const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
45378
45801
  const CI_TRUST_COMPANIES = "PayPal, Rippling, and Alibaba";
@@ -45464,7 +45887,7 @@ const makeNoopConsole = () => ({
45464
45887
  });
45465
45888
  //#endregion
45466
45889
  //#region src/cli/utils/version.ts
45467
- const VERSION = "0.6.3-dev.ce49250";
45890
+ const VERSION = "0.6.3-dev.ced746f";
45468
45891
  //#endregion
45469
45892
  //#region src/cli/utils/json-mode.ts
45470
45893
  let context = null;
@@ -45833,13 +46256,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45833
46256
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45834
46257
  * standard `SENTRY_RELEASE` override.
45835
46258
  */
45836
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.ce49250`;
46259
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.6.3-dev.ced746f`;
45837
46260
  /**
45838
46261
  * Deployment environment shown in Sentry's environment filter. Defaults to
45839
46262
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45840
46263
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45841
46264
  */
45842
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.ce49250") ? "development" : "production");
46265
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.6.3-dev.ced746f") ? "development" : "production");
45843
46266
  /**
45844
46267
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45845
46268
  * (set to `0` to disable tracing) and falls back to
@@ -57917,6 +58340,276 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
57917
58340
  })).toSorted((left, right) => right.fileCount - left.fileCount);
57918
58341
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
57919
58342
  //#endregion
58343
+ //#region src/cli/utils/scan-result-cache.ts
58344
+ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
58345
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
58346
+ "oxlint/package.json",
58347
+ "oxlint-plugin-react-doctor/package.json",
58348
+ "deslop-js/package.json",
58349
+ "eslint-plugin-react-hooks/package.json"
58350
+ ];
58351
+ const bundledRequire = createRequire(import.meta.url);
58352
+ const normalizeForStableJson = (value) => {
58353
+ if (value === null) return null;
58354
+ if (value === void 0) return void 0;
58355
+ const valueType = typeof value;
58356
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
58357
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
58358
+ if (Array.isArray(value)) return value.map((item) => {
58359
+ const normalized = normalizeForStableJson(item);
58360
+ return normalized === void 0 ? null : normalized;
58361
+ });
58362
+ const prototype = Object.getPrototypeOf(value);
58363
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
58364
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
58365
+ const normalizedRecord = {};
58366
+ for (const key of Object.keys(value).sort()) {
58367
+ const normalized = normalizeForStableJson(value[key]);
58368
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
58369
+ }
58370
+ return normalizedRecord;
58371
+ };
58372
+ const stringifyStableJson = (value) => {
58373
+ try {
58374
+ return JSON.stringify(normalizeForStableJson(value));
58375
+ } catch {
58376
+ return null;
58377
+ }
58378
+ };
58379
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
58380
+ const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
58381
+ const parseWorktreeStatusRecords = (statusOutput) => {
58382
+ const entries = [];
58383
+ const tokens = statusOutput.split("\0");
58384
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
58385
+ let record = tokens[tokenIndex];
58386
+ if (record.length === 0) continue;
58387
+ if (record.length < 3 || record[2] !== " ") {
58388
+ if (tokenIndex !== 0) return null;
58389
+ record = ` ${record}`;
58390
+ }
58391
+ if (record.length < 4 || record[2] !== " ") return null;
58392
+ const statusCode = record.slice(0, 2);
58393
+ const recordPath = record.slice(3);
58394
+ if (statusCode.includes("R") || statusCode.includes("C")) {
58395
+ tokenIndex += 1;
58396
+ const originPath = tokens[tokenIndex];
58397
+ if (originPath === void 0 || originPath.length === 0) return null;
58398
+ entries.push({
58399
+ statusCode,
58400
+ path: recordPath,
58401
+ originPath
58402
+ });
58403
+ continue;
58404
+ }
58405
+ entries.push({
58406
+ statusCode,
58407
+ path: recordPath
58408
+ });
58409
+ }
58410
+ return entries;
58411
+ };
58412
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
58413
+ let stats;
58414
+ try {
58415
+ stats = fs.lstatSync(absolutePath);
58416
+ } catch {
58417
+ return statusCode.includes("D") ? "deleted" : null;
58418
+ }
58419
+ if (stats.isSymbolicLink()) try {
58420
+ return `link:${fs.readlinkSync(absolutePath)}`;
58421
+ } catch {
58422
+ return null;
58423
+ }
58424
+ if (!stats.isFile()) return null;
58425
+ if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
58426
+ return hashFileContents(absolutePath);
58427
+ };
58428
+ const buildWorktreeFingerprint = (projectDirectory) => {
58429
+ const statusOutput = runGit(projectDirectory, [
58430
+ "status",
58431
+ "--porcelain=v1",
58432
+ "-z",
58433
+ "--untracked-files=all"
58434
+ ]);
58435
+ if (statusOutput === null) return null;
58436
+ if (statusOutput.length === 0) return [];
58437
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
58438
+ if (statusEntries === null) return null;
58439
+ if (statusEntries.length > 300) return null;
58440
+ const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
58441
+ if (repositoryRoot === null) return null;
58442
+ const dirtyEntries = [];
58443
+ for (const statusEntry of statusEntries) {
58444
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
58445
+ if (contentFingerprint === null) return null;
58446
+ dirtyEntries.push({
58447
+ ...statusEntry,
58448
+ contentFingerprint
58449
+ });
58450
+ }
58451
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
58452
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
58453
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
58454
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
58455
+ });
58456
+ };
58457
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
58458
+ const resolveDotenvFingerprint = (projectDirectory) => {
58459
+ try {
58460
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
58461
+ } catch {
58462
+ return [];
58463
+ }
58464
+ };
58465
+ const hasHiddenTrackedFileState = (projectDirectory) => {
58466
+ const output = runGit(projectDirectory, ["ls-files", "-v"]);
58467
+ if (output === null) return true;
58468
+ return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
58469
+ };
58470
+ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
58471
+ const readPersistedCache = (cacheFilePath) => {
58472
+ try {
58473
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
58474
+ if (!isRecord(parsed) || parsed.version !== 3) return {
58475
+ version: 3,
58476
+ entries: []
58477
+ };
58478
+ if (!Array.isArray(parsed.entries)) return {
58479
+ version: 3,
58480
+ entries: []
58481
+ };
58482
+ const entries = [];
58483
+ for (const entry of parsed.entries) {
58484
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
58485
+ if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
58486
+ entries.push(entry);
58487
+ }
58488
+ return {
58489
+ version: 3,
58490
+ entries
58491
+ };
58492
+ } catch {
58493
+ return {
58494
+ version: 3,
58495
+ entries: []
58496
+ };
58497
+ }
58498
+ };
58499
+ const writePersistedCache = (cacheFilePath, cache) => {
58500
+ try {
58501
+ fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
58502
+ const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
58503
+ fs.writeFileSync(tempPath, JSON.stringify(cache));
58504
+ fs.renameSync(tempPath, cacheFilePath);
58505
+ } catch {
58506
+ return;
58507
+ }
58508
+ };
58509
+ /**
58510
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
58511
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
58512
+ * sidecar, and dead-code caches (their `Context.Reference` defaults read the
58513
+ * same variable). Exported for the wide event's `cache.temperature`
58514
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
58515
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
58516
+ * not consulted — they leave the other subsystems live.
58517
+ */
58518
+ const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
58519
+ const resolveProjectIdentity = (projectDirectory) => {
58520
+ try {
58521
+ return fs.realpathSync.native(projectDirectory);
58522
+ } catch {
58523
+ return path.resolve(projectDirectory);
58524
+ }
58525
+ };
58526
+ const fileFingerprint = (filePath) => {
58527
+ try {
58528
+ const stat = fs.statSync(filePath);
58529
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
58530
+ } catch {
58531
+ return null;
58532
+ }
58533
+ };
58534
+ const resolveToolchainFingerprint = (nodeBinaryPath) => {
58535
+ const fingerprints = [];
58536
+ if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
58537
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
58538
+ const packageJson = bundledRequire(specifier);
58539
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
58540
+ fingerprints.push(`${specifier}=${version}`);
58541
+ } catch {
58542
+ fingerprints.push(`${specifier}=missing`);
58543
+ }
58544
+ return fingerprints;
58545
+ };
58546
+ const buildScanResultCacheKey = (input) => {
58547
+ if (isCacheGloballyDisabled()) return null;
58548
+ if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
58549
+ const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
58550
+ if (worktreeFingerprint === null) return null;
58551
+ const headSha = readHeadSha(input.projectDirectory);
58552
+ if (headSha === null) return null;
58553
+ if (stringifyStableJson(input.userConfig) === null) return null;
58554
+ const cacheKeyJson = stringifyStableJson({
58555
+ schemaVersion: 3,
58556
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
58557
+ headSha,
58558
+ worktreeFingerprint,
58559
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
58560
+ reactDoctorVersion: input.version,
58561
+ nodeVersion: process.version,
58562
+ toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
58563
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
58564
+ hasConfigOverride: input.hasConfigOverride,
58565
+ configSourceDirectory: input.configSourceDirectory,
58566
+ userConfig: input.userConfig,
58567
+ engineOptions: {
58568
+ lint: input.options.lint,
58569
+ deadCode: input.options.deadCode,
58570
+ includePaths: [...input.options.includePaths].sort(),
58571
+ customRulesOnly: input.options.customRulesOnly,
58572
+ respectInlineDisables: input.options.respectInlineDisables,
58573
+ warnings: input.options.warnings,
58574
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
58575
+ ignoredTags: [...input.options.ignoredTags].sort(),
58576
+ concurrency: input.options.concurrency,
58577
+ lintBatchOrdering: resolveLintBatchOrdering(),
58578
+ baselineRef: input.options.baseline?.ref,
58579
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
58580
+ noScore: input.options.noScore,
58581
+ isCi: input.options.isCi,
58582
+ suppressRendering: input.options.suppressRendering,
58583
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
58584
+ }
58585
+ });
58586
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
58587
+ };
58588
+ const createScanResultCache = (projectDirectory) => {
58589
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
58590
+ const persistedCache = readPersistedCache(cacheFilePath);
58591
+ const entries = /* @__PURE__ */ new Map();
58592
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
58593
+ const persist = () => {
58594
+ writePersistedCache(cacheFilePath, {
58595
+ version: 3,
58596
+ entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
58597
+ });
58598
+ };
58599
+ return {
58600
+ lookup: (key) => entries.get(key)?.payload ?? null,
58601
+ store: (key, payload) => {
58602
+ entries.set(key, {
58603
+ key,
58604
+ createdAtMs: Date.now(),
58605
+ payload
58606
+ });
58607
+ persist();
58608
+ }
58609
+ };
58610
+ };
58611
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
58612
+ //#endregion
57920
58613
  //#region src/cli/utils/should-block-ci.ts
57921
58614
  const shouldBlockCi = (diagnostics, blockingLevel) => {
57922
58615
  if (blockingLevel === "none") return false;
@@ -57958,6 +58651,63 @@ const readEnvBoolean = (name) => {
57958
58651
  if (value === void 0) return null;
57959
58652
  return value.toLowerCase() === "true" || value === "1";
57960
58653
  };
58654
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
58655
+ const resolveDeadCodeReuseRatio = (result) => {
58656
+ if (result.deadCodeCacheHit === true) return 1;
58657
+ const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
58658
+ if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
58659
+ return result.deadCodeCacheHit === false ? 0 : null;
58660
+ };
58661
+ /**
58662
+ * One queryable cache temperature per scan, derived from the whole stack:
58663
+ *
58664
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
58665
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
58666
+ * branch; no scan work ran).
58667
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
58668
+ * replays, a dead-code whole-result hit, or dead-code
58669
+ * summary-cache hits.
58670
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
58671
+ * off-switch is on. Granular per-cache opt-outs
58672
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
58673
+ * since the other subsystems stay live.
58674
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
58675
+ *
58676
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
58677
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
58678
+ * replay ratio, dead-code reuse), skipping subsystems that never consulted a
58679
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
58680
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
58681
+ * p50/p90-able summary. Emitted only on the success path.
58682
+ */
58683
+ const buildCacheAttributes = (input) => {
58684
+ if (input.result === void 0) return {};
58685
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
58686
+ wholeRepoHit: true,
58687
+ temperature: "turbo",
58688
+ warmth: 1
58689
+ });
58690
+ const result = input.result;
58691
+ const subsystemReuseRatios = [
58692
+ ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58693
+ ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
58694
+ resolveDeadCodeReuseRatio(result)
58695
+ ];
58696
+ let knownSubsystemCount = 0;
58697
+ let reuseRatioSum = 0;
58698
+ for (const reuseRatio of subsystemReuseRatios) {
58699
+ if (reuseRatio === null) continue;
58700
+ knownSubsystemCount += 1;
58701
+ reuseRatioSum += reuseRatio;
58702
+ }
58703
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
58704
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
58705
+ return withNamespace("cache", {
58706
+ wholeRepoHit: input.wholeRepoCacheHit ?? null,
58707
+ temperature,
58708
+ warmth
58709
+ });
58710
+ };
57961
58711
  const resolveVersionPin = (versionInput) => {
57962
58712
  if (versionInput === void 0 || versionInput.trim() === "") return null;
57963
58713
  if (versionInput === "latest") return "latest";
@@ -58063,7 +58813,10 @@ const buildOutcomeAttributes = (input) => {
58063
58813
  deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount ?? null,
58064
58814
  cacheHitFiles: result.lintCacheHitFileCount ?? null,
58065
58815
  cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
58066
- cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
58816
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
58817
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount ?? null,
58818
+ sidecarTotalFiles: result.lintSidecarTotalFileCount ?? null,
58819
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
58067
58820
  }),
58068
58821
  ...withNamespace("deadCode", {
58069
58822
  failed: input.didDeadCodeFail ?? null,
@@ -58130,7 +58883,7 @@ const buildScanAttributes = (input) => {
58130
58883
  * Projects a scan into the namespaced attribute set for its root span — the
58131
58884
  * canonical per-scan "wide event". Every attribute carries a dotted namespace
58132
58885
  * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
58133
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
58886
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
58134
58887
  * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
58135
58888
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
58136
58889
  * in the Spans dataset. Pure and exported so the projection (outcome
@@ -58144,7 +58897,8 @@ const buildScanAttributes = (input) => {
58144
58897
  const buildRunEventAttributes = (input) => toSpanAttributes({
58145
58898
  ...buildScanAttributes(input),
58146
58899
  ...buildActionAttributes(),
58147
- ...buildOutcomeAttributes(input)
58900
+ ...buildOutcomeAttributes(input),
58901
+ ...buildCacheAttributes(input)
58148
58902
  });
58149
58903
  /**
58150
58904
  * Stamps the wide-event attributes onto the run's root span. A guarded no-op
@@ -59354,275 +60108,6 @@ const resolveCliCategories = (categoryFlag) => {
59354
60108
  return resolvedCategories.length > 0 ? resolvedCategories : void 0;
59355
60109
  };
59356
60110
  //#endregion
59357
- //#region src/cli/utils/scan-result-cache.ts
59358
- const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59359
- const TOOLCHAIN_PACKAGE_SPECIFIERS = [
59360
- "oxlint",
59361
- "oxlint/package.json",
59362
- "oxlint-plugin-react-doctor",
59363
- "deslop-js/package.json",
59364
- "eslint-plugin-react-hooks/package.json"
59365
- ];
59366
- const bundledRequire = createRequire(import.meta.url);
59367
- const normalizeForStableJson = (value) => {
59368
- if (value === null) return null;
59369
- if (value === void 0) return void 0;
59370
- const valueType = typeof value;
59371
- if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
59372
- if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
59373
- if (Array.isArray(value)) return value.map((item) => {
59374
- const normalized = normalizeForStableJson(item);
59375
- return normalized === void 0 ? null : normalized;
59376
- });
59377
- const prototype = Object.getPrototypeOf(value);
59378
- if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
59379
- if (!isRecord(value)) throw new Error("Unsupported cache key object");
59380
- const normalizedRecord = {};
59381
- for (const key of Object.keys(value).sort()) {
59382
- const normalized = normalizeForStableJson(value[key]);
59383
- if (normalized !== void 0) normalizedRecord[key] = normalized;
59384
- }
59385
- return normalizedRecord;
59386
- };
59387
- const stringifyStableJson = (value) => {
59388
- try {
59389
- return JSON.stringify(normalizeForStableJson(value));
59390
- } catch {
59391
- return null;
59392
- }
59393
- };
59394
- const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
59395
- const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
59396
- const parseWorktreeStatusRecords = (statusOutput) => {
59397
- const entries = [];
59398
- const tokens = statusOutput.split("\0");
59399
- for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
59400
- let record = tokens[tokenIndex];
59401
- if (record.length === 0) continue;
59402
- if (record.length < 3 || record[2] !== " ") {
59403
- if (tokenIndex !== 0) return null;
59404
- record = ` ${record}`;
59405
- }
59406
- if (record.length < 4 || record[2] !== " ") return null;
59407
- const statusCode = record.slice(0, 2);
59408
- const recordPath = record.slice(3);
59409
- if (statusCode.includes("R") || statusCode.includes("C")) {
59410
- tokenIndex += 1;
59411
- const originPath = tokens[tokenIndex];
59412
- if (originPath === void 0 || originPath.length === 0) return null;
59413
- entries.push({
59414
- statusCode,
59415
- path: recordPath,
59416
- originPath
59417
- });
59418
- continue;
59419
- }
59420
- entries.push({
59421
- statusCode,
59422
- path: recordPath
59423
- });
59424
- }
59425
- return entries;
59426
- };
59427
- const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
59428
- let stats;
59429
- try {
59430
- stats = fs.lstatSync(absolutePath);
59431
- } catch {
59432
- return statusCode.includes("D") ? "deleted" : null;
59433
- }
59434
- if (stats.isSymbolicLink()) try {
59435
- return `link:${fs.readlinkSync(absolutePath)}`;
59436
- } catch {
59437
- return null;
59438
- }
59439
- if (!stats.isFile()) return null;
59440
- if (stats.size > 10485760) return `stat:${stats.mtimeMs}:${stats.size}`;
59441
- return hashFileContents(absolutePath);
59442
- };
59443
- const buildWorktreeFingerprint = (projectDirectory) => {
59444
- const statusOutput = runGit(projectDirectory, [
59445
- "status",
59446
- "--porcelain=v1",
59447
- "-z",
59448
- "--untracked-files=all"
59449
- ]);
59450
- if (statusOutput === null) return null;
59451
- if (statusOutput.length === 0) return [];
59452
- const statusEntries = parseWorktreeStatusRecords(statusOutput);
59453
- if (statusEntries === null) return null;
59454
- if (statusEntries.length > 300) return null;
59455
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
59456
- if (repositoryRoot === null) return null;
59457
- const dirtyEntries = [];
59458
- for (const statusEntry of statusEntries) {
59459
- const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
59460
- if (contentFingerprint === null) return null;
59461
- dirtyEntries.push({
59462
- ...statusEntry,
59463
- contentFingerprint
59464
- });
59465
- }
59466
- return dirtyEntries.sort((firstEntry, secondEntry) => {
59467
- const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
59468
- const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
59469
- return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
59470
- });
59471
- };
59472
- const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
59473
- const resolveDotenvFingerprint = (projectDirectory) => {
59474
- try {
59475
- return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${fileFingerprint(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
59476
- } catch {
59477
- return [];
59478
- }
59479
- };
59480
- const hasHiddenTrackedFileState = (projectDirectory) => {
59481
- const output = runGit(projectDirectory, ["ls-files", "-v"]);
59482
- if (output === null) return true;
59483
- return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
59484
- };
59485
- const resolveCacheFilePath = (projectDirectory) => {
59486
- const nodeModulesDirectory = path.join(projectDirectory, "node_modules");
59487
- if (fs.existsSync(nodeModulesDirectory)) return path.join(nodeModulesDirectory, ".cache", "react-doctor", "scan-cache.json");
59488
- const projectHash = hashString(projectDirectory).slice(0, 16);
59489
- return path.join(os.tmpdir(), "react-doctor-cache", `scan-${projectHash}.json`);
59490
- };
59491
- const readPersistedCache = (cacheFilePath) => {
59492
- try {
59493
- const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
59494
- if (!isRecord(parsed) || parsed.version !== 3) return {
59495
- version: 3,
59496
- entries: []
59497
- };
59498
- if (!Array.isArray(parsed.entries)) return {
59499
- version: 3,
59500
- entries: []
59501
- };
59502
- const entries = [];
59503
- for (const entry of parsed.entries) {
59504
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
59505
- if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
59506
- entries.push(entry);
59507
- }
59508
- return {
59509
- version: 3,
59510
- entries
59511
- };
59512
- } catch {
59513
- return {
59514
- version: 3,
59515
- entries: []
59516
- };
59517
- }
59518
- };
59519
- const writePersistedCache = (cacheFilePath, cache) => {
59520
- try {
59521
- fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
59522
- const tempPath = `${cacheFilePath}.${process.pid}.tmp`;
59523
- fs.writeFileSync(tempPath, JSON.stringify(cache));
59524
- fs.renameSync(tempPath, cacheFilePath);
59525
- } catch {
59526
- return;
59527
- }
59528
- };
59529
- const isScanResultCacheDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
59530
- const resolveProjectIdentity = (projectDirectory) => {
59531
- try {
59532
- return fs.realpathSync.native(projectDirectory);
59533
- } catch {
59534
- return path.resolve(projectDirectory);
59535
- }
59536
- };
59537
- const fileFingerprint = (filePath) => {
59538
- try {
59539
- const stat = fs.statSync(filePath);
59540
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
59541
- } catch {
59542
- return null;
59543
- }
59544
- };
59545
- const resolveToolchainFingerprint = (nodeBinaryPath) => {
59546
- const fingerprints = [];
59547
- if (nodeBinaryPath !== null) {
59548
- const fingerprint = fileFingerprint(nodeBinaryPath);
59549
- if (fingerprint !== null) fingerprints.push(`node=${fingerprint}`);
59550
- }
59551
- for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
59552
- const fingerprint = fileFingerprint(bundledRequire.resolve(specifier));
59553
- if (fingerprint !== null) fingerprints.push(`${specifier}=${fingerprint}`);
59554
- } catch {
59555
- continue;
59556
- }
59557
- return fingerprints;
59558
- };
59559
- const buildScanResultCacheKey = (input) => {
59560
- if (isScanResultCacheDisabled()) return null;
59561
- if (hasHiddenTrackedFileState(input.projectDirectory)) return null;
59562
- const worktreeFingerprint = buildWorktreeFingerprint(input.projectDirectory);
59563
- if (worktreeFingerprint === null) return null;
59564
- const headSha = readHeadSha(input.projectDirectory);
59565
- if (headSha === null) return null;
59566
- if (stringifyStableJson(input.userConfig) === null) return null;
59567
- const cacheKeyJson = stringifyStableJson({
59568
- schemaVersion: 3,
59569
- projectIdentity: resolveProjectIdentity(input.projectDirectory),
59570
- headSha,
59571
- worktreeFingerprint,
59572
- dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
59573
- reactDoctorVersion: input.version,
59574
- nodeVersion: process.version,
59575
- toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
59576
- configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
59577
- hasConfigOverride: input.hasConfigOverride,
59578
- configSourceDirectory: input.configSourceDirectory,
59579
- userConfig: input.userConfig,
59580
- engineOptions: {
59581
- lint: input.options.lint,
59582
- deadCode: input.options.deadCode,
59583
- includePaths: [...input.options.includePaths].sort(),
59584
- customRulesOnly: input.options.customRulesOnly,
59585
- respectInlineDisables: input.options.respectInlineDisables,
59586
- warnings: input.options.warnings,
59587
- adoptExistingLintConfig: input.options.adoptExistingLintConfig,
59588
- ignoredTags: [...input.options.ignoredTags].sort(),
59589
- concurrency: input.options.concurrency,
59590
- lintBatchOrdering: resolveLintBatchOrdering(),
59591
- baselineRef: input.options.baseline?.ref,
59592
- changedLineRanges: input.options.changedLineRanges ?? void 0,
59593
- noScore: input.options.noScore,
59594
- isCi: input.options.isCi,
59595
- suppressRendering: input.options.suppressRendering,
59596
- supplyChainManifestChanged: input.options.supplyChainManifestChanged
59597
- }
59598
- });
59599
- return cacheKeyJson === null ? null : hashString(cacheKeyJson);
59600
- };
59601
- const createScanResultCache = (projectDirectory) => {
59602
- const cacheFilePath = resolveCacheFilePath(projectDirectory);
59603
- const persistedCache = readPersistedCache(cacheFilePath);
59604
- const entries = /* @__PURE__ */ new Map();
59605
- for (const entry of persistedCache.entries) entries.set(entry.key, entry);
59606
- const persist = () => {
59607
- writePersistedCache(cacheFilePath, {
59608
- version: 3,
59609
- entries: [...entries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
59610
- });
59611
- };
59612
- return {
59613
- lookup: (key) => entries.get(key)?.payload ?? null,
59614
- store: (key, payload) => {
59615
- entries.set(key, {
59616
- key,
59617
- createdAtMs: Date.now(),
59618
- payload
59619
- });
59620
- persist();
59621
- }
59622
- };
59623
- };
59624
- const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
59625
- //#endregion
59626
60111
  //#region src/inspect.ts
59627
60112
  const silentConsole = makeNoopConsole();
59628
60113
  const runConsole = (effect) => {
@@ -59799,7 +60284,7 @@ const runBaselineComparison = async (params) => {
59799
60284
  suppressScanSummary: true,
59800
60285
  supplyChainManifestChanged: params.options.supplyChainManifestChanged,
59801
60286
  deadlineEpochMs: params.deadlineEpochMs ?? void 0
59802
- }, {}).pipe(provide(baseLayers), provideService$2(Console, silentConsole))));
60287
+ }, {}).pipe(provide(baseLayers), provideService$2(PerFileLintCacheEnabled, false), provideService$2(SidecarLintCacheEnabled, false), provideService$2(Console, silentConsole))));
59803
60288
  if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
59804
60289
  const delta = computeDiagnosticDelta({
59805
60290
  headDiagnostics: params.headDiagnostics,
@@ -59852,7 +60337,8 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59852
60337
  startTime,
59853
60338
  rootSentrySpan,
59854
60339
  scanMode: cachedPayload.baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59855
- baselineDegraded
60340
+ baselineDegraded,
60341
+ wholeRepoCacheHit: true
59856
60342
  });
59857
60343
  recordOnboardingCompletion(options);
59858
60344
  return result;
@@ -59961,8 +60447,11 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
59961
60447
  rootSentrySpan,
59962
60448
  scanMode: baselineDelta ? "baseline" : isDiffMode ? "diff" : "full",
59963
60449
  baselineDegraded,
60450
+ wholeRepoCacheHit: false,
59964
60451
  lintCacheHitFileCount: output.lintCacheHitFileCount,
59965
60452
  lintCacheTotalFileCount: output.lintCacheTotalFileCount,
60453
+ lintSidecarReplayedFileCount: output.lintSidecarReplayedFileCount,
60454
+ lintSidecarTotalFileCount: output.lintSidecarTotalFileCount,
59966
60455
  deadCodeCacheHit: output.deadCodeCacheHit,
59967
60456
  deadCodeSummaryCacheHits: output.deadCodeSummaryCacheHits,
59968
60457
  deadCodeSummaryCacheMisses: output.deadCodeSummaryCacheMisses
@@ -60000,6 +60489,8 @@ const renderAndRecordScan = async (input) => {
60000
60489
  scanElapsedMilliseconds: input.payload.scanElapsedMilliseconds,
60001
60490
  lintCacheHitFileCount: input.lintCacheHitFileCount ?? null,
60002
60491
  lintCacheTotalFileCount: input.lintCacheTotalFileCount ?? null,
60492
+ lintSidecarReplayedFileCount: input.lintSidecarReplayedFileCount ?? null,
60493
+ lintSidecarTotalFileCount: input.lintSidecarTotalFileCount ?? null,
60003
60494
  deadCodeCacheHit: input.deadCodeCacheHit ?? null,
60004
60495
  deadCodeSummaryCacheHits: input.deadCodeSummaryCacheHits ?? null,
60005
60496
  deadCodeSummaryCacheMisses: input.deadCodeSummaryCacheMisses ?? null,
@@ -60027,6 +60518,7 @@ const renderAndRecordScan = async (input) => {
60027
60518
  result,
60028
60519
  mode: input.scanMode,
60029
60520
  gateExempt: input.baselineDegraded,
60521
+ wholeRepoCacheHit: input.wholeRepoCacheHit,
60030
60522
  didLintFail: input.payload.didLintFail,
60031
60523
  lintFailureReasonKind: input.payload.lintFailureReasonKind,
60032
60524
  lintPartialFailureCount: input.payload.lintPartialFailures.length,
@@ -60041,7 +60533,7 @@ const renderAndRecordScan = async (input) => {
60041
60533
  return result;
60042
60534
  };
60043
60535
  const finalizeAndRender = (input) => gen(function* () {
60044
- const { options, elapsedMilliseconds, diagnostics, score, project, userConfig, didLintFail, lintFailureReason, lintPartialFailures, didDeadCodeFail, deadCodeFailureReason, directory, scannedFileCount, scannedFilePaths, scanElapsedMilliseconds, lintCacheHitFileCount, lintCacheTotalFileCount, deadCodeCacheHit, deadCodeSummaryCacheHits, deadCodeSummaryCacheMisses, baselineDelta } = input;
60536
+ 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;
60045
60537
  const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
60046
60538
  didLintFail,
60047
60539
  lintFailureReason,
@@ -60065,6 +60557,10 @@ const finalizeAndRender = (input) => gen(function* () {
60065
60557
  lintCacheHitFileCount,
60066
60558
  lintCacheTotalFileCount
60067
60559
  } : {},
60560
+ ...lintSidecarTotalFileCount !== null ? {
60561
+ lintSidecarReplayedFileCount,
60562
+ lintSidecarTotalFileCount
60563
+ } : {},
60068
60564
  ...deadCodeCacheHit !== null ? { deadCodeCacheHit } : {},
60069
60565
  ...deadCodeSummaryCacheHits !== null && deadCodeSummaryCacheMisses !== null ? {
60070
60566
  deadCodeSummaryCacheHits,
@@ -63916,4 +64412,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
63916
64412
  export {};
63917
64413
 
63918
64414
  //# sourceMappingURL=cli.js.map
63919
- //# debugId=cb06d1e4-d50b-5715-af77-13eb97c43e07
64415
+ //# debugId=1722becf-6e91-56ae-afc9-1e1410647bc3