react-doctor 0.9.14 → 0.9.15-dev.23d6160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +3 -0
  2. package/dist/_react-doctor_require-typescript-DXQDYeWK.js +16 -0
  3. package/dist/{active-run-trace-dlIZeEvb.js → active-run-trace-Dewycxda.js} +3 -3
  4. package/dist/{active-scan-abort-registry-B72ASBUh.js → active-scan-abort-registry-CCTgal0O.js} +3 -3
  5. package/dist/{build-section-divider-8DpL5BjN.js → build-section-divider-Bt7znRi8.js} +5 -4
  6. package/dist/{ci-DH_jkNS4.js → ci-vktx9bIK.js} +14 -13
  7. package/dist/cli.js +32 -29
  8. package/dist/constants-DJ3RtEMu.js +1035 -0
  9. package/dist/{constants-CKOk_Fa9.js → constants-DNh19qg9.js} +3 -3
  10. package/dist/{design-DR72z3tB.js → design-CH3L5hSZ.js} +4 -4
  11. package/dist/{devtools-DOdrAPkZ.js → devtools-D20sefkm.js} +3 -3
  12. package/dist/duplicate-jsx-worker.js +62 -62
  13. package/dist/{exit-gracefully-BP0lJPao.js → exit-gracefully-Y5XyTvxu.js} +7 -7
  14. package/dist/fs-utils-C88IbqFa.js +45 -0
  15. package/dist/{get-east-asian-width-CM5BOEC-.js → get-east-asian-width-DRPy7dqz.js} +4 -4
  16. package/dist/{handle-error-BO7AUOeO.js → handle-error-8NlzFVoW.js} +6 -5
  17. package/dist/index.d.ts +8511 -8509
  18. package/dist/index.js +12734 -12338
  19. package/dist/{inspect-CtwjUwiF.js → inspect-Cs2Ojloa.js} +28 -21
  20. package/dist/{install-DAwHDunN.js → install-C4JkqZoO.js} +10 -9
  21. package/dist/{install-doctor-script-DousNjwa.js → install-doctor-script-Dr4O5HID.js} +17 -8
  22. package/dist/{install-github-workflow-DNfyCbSd.js → install-github-workflow-Dtf07ISQ.js} +10 -10
  23. package/dist/{install-react-doctor-CofgPrzS.js → install-react-doctor-Bi_Dav6m.js} +15 -11
  24. package/dist/is-cache-globally-disabled-De19063F.js +10 -0
  25. package/dist/{jsx-runtime-Dv_yWxfg.js → jsx-runtime-CND_0px2.js} +11 -10
  26. package/dist/oxlint-worker.js +95 -5
  27. package/dist/{path-format-DnHpmJwG.js → path-format-By20mo8A.js} +3 -3
  28. package/dist/{print-branded-header-CFoO5ZHN.js → print-branded-header-DqaKyGKm.js} +4 -4
  29. package/dist/project-analysis-worker.js +459 -461
  30. package/dist/{prompt-runtime-scan-url-9tmVyRA2.js → prompt-runtime-scan-url-DiI3_zIx.js} +6 -6
  31. package/dist/react-compiler-detection-worker.d.ts +1 -0
  32. package/dist/react-compiler-detection-worker.js +1029 -0
  33. package/dist/{read-object-file-C8gGTrwo.js → read-object-file-DBJkVOGK.js} +4 -4
  34. package/dist/{record-metric-DMuxA69u.js → record-metric-C-ZZzzDc.js} +12589 -13129
  35. package/dist/{report-error-DynYEdrW.js → report-error-_C-eJJHZ.js} +7 -6
  36. package/dist/{report-workflow-result-C0nwk5Eq.js → report-workflow-result-BA_xTdWY.js} +24 -11
  37. package/dist/{resolve-scope-CtDh9FtN.js → resolve-scope-Cl4BiTkm.js} +7 -7
  38. package/dist/{rules-Dtp6I51W.js → rules-DAQXfBXb.js} +8 -7
  39. package/dist/{run-scan-app-DTjrGFyP.js → run-scan-app-CN5nwdB-.js} +12 -11
  40. package/dist/{runtime-scan-DylzMW6d.js → runtime-scan-DFdypOyE.js} +13 -12
  41. package/dist/{sanitize-runtime-url-CtosBgav.js → sanitize-runtime-url-yCL8ZAFd.js} +4 -4
  42. package/dist/{scan-i9quJrxG.js → scan-C6U_DFdI.js} +10 -9
  43. package/dist/{semver-DlmXks31.js → semver-BZIuy1gP.js} +3 -3
  44. package/dist/{should-fail-scan-gate-uW8_hnTc.js → should-fail-scan-gate-2mIY6wHj.js} +1848 -1825
  45. package/dist/{should-skip-prompts-fmi7PPp5.js → should-skip-prompts-CuyyYvRd.js} +4 -4
  46. package/dist/{should-use-tui-DaLPPhMi.js → should-use-tui-CbbIdX9b.js} +4 -4
  47. package/dist/start-scan-preamble-DVhXIPc0.d.ts +1 -0
  48. package/dist/start-scan-preamble-DZknzhHU.js +68 -0
  49. package/dist/start-scan-preamble.d.ts +1 -0
  50. package/dist/start-scan-preamble.js +4 -0
  51. package/dist/{telemetry-runtime-CxBC0gEj.js → telemetry-runtime-DLq3FDzI.js} +7 -6
  52. package/dist/{unref-stdin-SK3WfwxS.js → unref-stdin-D-NZBFpg.js} +3 -3
  53. package/dist/unwrap-typescript-expression-CrEPQUWz.js +14 -0
  54. package/dist/{version-Bti3M8BN.js → version-DO7dtT21.js} +5 -4
  55. package/dist/{why-dcQaGoJz.js → why-uxPDWtZO.js} +4 -4
  56. package/dist/{with-run-span-Lynvloyt.js → with-run-span-BCopjALe.js} +7 -6
  57. package/package.json +7 -5
  58. package/dist/_react-doctor_require-typescript-DnwIzXJn.js +0 -7
  59. package/dist/unwrap-typescript-expression-CbO_6kmC.js +0 -14
@@ -1,25 +1,27 @@
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]="e73476ca-0e71-51dd-a00a-b07e28f2bb36")}catch(e){}}();
3
- import { $ as LintPartialFailures, $n as ParentSpan, A as SCAN_RESULT_CACHE_FILENAME, An as decodeUnknownSync, At as createOxlintSpawnSlots, B as isNonInteractiveEnvironment, Bn as runPromise, Cn as warmOxlintWorkerPool, Cr as OXLINT_NODE_REQUIREMENT, Dn as log, Dt as countOptInProjectRuleSelections, Et as computeDiagnosticDelta, Fn as orElseSucceed, Ft as filterSourceFiles, G as isCiEnvironment, Gt as hasPublishedFixRecipe, Hn as sync, In as promise, J as Config, Jn as succeed, Jt as highlighter, K as isCiOrCodingAgentEnvironment, Kn as mergeAll, Ln as provide, Mn as gen, Mt as discoverSupportedSubprojects, Nt as filterDiagnosticsForSurface, On as warn, Ot as createInvocationCaches, Pn as map, Pt as filterPathsOutsideDirectories, Qt as isReactDoctorError, Rn as provideContext, S as BASELINE_FILES_TEMP_DIR_PREFIX, Sr as MILLISECONDS_PER_SECOND, St as classifyFileContext, T as METRIC, Tn as error, Tt as computeConfigFingerprint, U as ACTION_INPUT_ENVIRONMENT_VARIABLES, Un as try_, Vn as runSync, Vt as getDiagnosticRuleIdentity, W as detectRunnerOs, Wt as groupBy, X as Git, Xt as isPathInsideDirectory, Y as Files, Yt as isMonorepoRoot, Z as InvocationCaches, _ as VERSION, _t as buildRuleDocsUrl, at as PerFileLintCacheEnabled, bn as summarizeDiagnostics, br as JSX_FILE_PATTERN, bt as calculateScore, cn as mergeReactDoctorConfigs, ct as Reporter, d as isJsonModeActive, dn as resolveLintBatchOrdering, en as isScanComplete, er as yieldToEventLoop, et as Linter, fn as resolveReactDoctorCacheDir, ft as SupplyChain, gn as runInspect, gr as ENTERPRISE_CONTACT_URL, hn as restoreLegacyThrow, ht as atomicWriteJson, in as listWorkspacePackages, it as OxlintSpawnSlots, kn as Diagnostic, kt as createNodeReadFileLinesSync, l as getRunId, ln as remainingDeadlineBudgetMs, lt as Score, mn as resolveScanTarget, mr as DIAGNOSTIC_CATEGORY_BUCKETS, n as recordDistribution, nn as listSourceFiles, nt as NodeResolver, on as mapWithConcurrency, ot as Progress, pn as resolveScanConcurrency, qt as hashFileContents, r as toSpanAttributes, rn as listSourceFilesCooperative, rr as isFile, rt as OxlintConcurrency, sn as materializeSourceTree, st as Project, t as recordCount, tr as isDirectory, tt as Maintainability, un as resolveGithubActionsScoreMetadata, ur as CI_URL, ut as SidecarLintCacheEnabled, v as makeNoopConsole, vr as GIT_SHOW_MAX_BUFFER_BYTES, vt as buildRuleSeverityControls, wn as Console, wr as SHARE_BASE_URL, wt as collectSourceFilesByDirectory, xr as MAINTAINABILITY_SOURCE_FILE_PATTERN, xt as canonicalizeUserRuleKey, yn as shouldUseMaintainabilityLayer, yr as HTML_FILE_PATTERN, yt as buildSkippedChecks, z as CliInputError, zn as provideService } from "./record-metric-DMuxA69u.js";
4
- import { t as getTelemetryContext } from "./telemetry-runtime-CxBC0gEj.js";
5
- import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-B72ASBUh.js";
6
- import { n as buildFooterLinkLines, t as buildSectionDivider } from "./build-section-divider-8DpL5BjN.js";
7
- import { t as toForwardSlashes } from "./path-format-DnHpmJwG.js";
8
- import { t as _react_doctor_require_typescript_default } from "./_react-doctor_require-typescript-DnwIzXJn.js";
9
- import { C as isRecord, M as cliLogger, S as getPackageJsonPath, a as installReactDoctorScriptStep, c as spinner, k as runGit, n as findNearestPackageDirectory, o as isSpinnerSilent, s as setSpinnerSilent } from "./install-doctor-script-DousNjwa.js";
10
- import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-DNfyCbSd.js";
11
- import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-CtDh9FtN.js";
12
- import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-Lynvloyt.js";
13
- import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-C0nwk5Eq.js";
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]="7ed987a8-a8e5-5486-8e48-829258f26d80")}catch(e){}}();
3
+ import { E as TRACKED_FILE_FLAGS_GIT_ARGUMENTS, Ft as HTML_FILE_PATTERN, Ht as MAINTAINABILITY_SOURCE_FILE_PATTERN, Jt as OXLINT_NODE_REQUIREMENT, K as resolveReactDoctorCacheDir, Nt as GIT_SHOW_MAX_BUFFER_BYTES, Rt as JSX_FILE_PATTERN, St as ENTERPRISE_CONTACT_URL, Wt as MILLISECONDS_PER_SECOND, _t as DIAGNOSTIC_CATEGORY_BUCKETS, c as HEAD_SHA_GIT_ARGUMENTS, dn as SHARE_BASE_URL, h as REPOSITORY_ROOT_GIT_ARGUMENTS, i as BASELINE_FILES_TEMP_DIR_PREFIX, k as WORKTREE_STATUS_GIT_ARGUMENTS, q as resolveScanConcurrency, u as METRIC, ut as CI_URL, v as SCAN_RESULT_CACHE_FILENAME } from "./constants-DJ3RtEMu.js";
4
+ import { t as isCacheGloballyDisabled } from "./is-cache-globally-disabled-De19063F.js";
5
+ import { $ as buildSkippedChecks, A as Git, B as Progress, Bt as materializeSourceTree, C as ACTION_INPUT_ENVIRONMENT_VARIABLES, Dt as hashFileContents, E as isCiOrCodingAgentEnvironment, En as succeed, F as Maintainability, Ft as listSourceFiles, Gt as resolveScanTarget, H as Reporter, Ht as remainingDeadlineBudgetMs, I as NodeResolver, In as isDirectory, It as listSourceFilesCooperative, K as SupplyChain, Kt as restoreLegacyThrow, L as OxlintConcurrency, Lt as listWorkspacePackages, Mn as isMonorepoRoot, N as LintPartialFailures, Nn as yieldToEventLoop, Nt as isScanComplete, O as Config, Ot as highlighter, P as Linter, Q as buildRuleSeverityControls, R as OxlintSpawnSlots, Rn as isFile, T as isCiEnvironment, Tt as hasPublishedFixRecipe, U as Score, Ut as resolveGithubActionsScoreMetadata, V as Project, Vt as mergeReactDoctorConfigs, W as SidecarLintCacheEnabled, Wt as resolveLintBatchOrdering, Xt as shouldUseMaintainabilityLayer, Y as atomicWriteJson, Z as buildRuleDocsUrl, Zt as summarizeDiagnostics, _ as VERSION, _n as provideService, an as log, at as computeConfigFingerprint, b as isNonInteractiveEnvironment, bn as sync, cn as decodeUnknownSync, ct as createInvocationCaches, d as isJsonModeActive, en as warmDuplicateJsxWorker, et as calculateScore, fn as map, ft as discoverSupportedSubprojects, gn as provideContext, hn as provide, ht as filterSourceFiles, it as collectSourceFilesByDirectory, j as InvocationCaches, jn as ParentSpan, jt as isReactDoctorError, k as Files, kt as isPathInsideDirectory, l as getRunId, lt as createNodeReadFileLinesSync, mn as promise, mt as filterPathsOutsideDirectories, n as recordDistribution, nn as Console, nt as classifyFileContext, on as warn, ot as computeDiagnosticDelta, pn as orElseSucceed, pt as filterDiagnosticsForSurface, qt as runInspect, r as toSpanAttributes, rn as error, sn as Diagnostic, st as countOptInProjectRuleSelections, t as recordCount, tn as warmOxlintWorkerPool, tt as canonicalizeUserRuleKey, un as gen, ut as createOxlintSpawnSlots, v as makeNoopConsole, vn as runPromise, w as detectRunnerOs, wn as mergeAll, wt as groupBy, xn as try_, xt as getDiagnosticRuleIdentity, y as CliInputError, yn as runSync, z as PerFileLintCacheEnabled, zt as mapWithConcurrency } from "./record-metric-C-ZZzzDc.js";
6
+ import { t as getTelemetryContext } from "./telemetry-runtime-DLq3FDzI.js";
7
+ import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-CCTgal0O.js";
8
+ import { n as buildFooterLinkLines, t as buildSectionDivider } from "./build-section-divider-Bt7znRi8.js";
9
+ import { t as toForwardSlashes } from "./path-format-By20mo8A.js";
10
+ import { t as typescriptBinding } from "./_react-doctor_require-typescript-DXQDYeWK.js";
11
+ import { A as runGitAsync, C as isRecord, N as cliLogger, S as getPackageJsonPath, a as installReactDoctorScriptStep, c as spinner, n as findNearestPackageDirectory, o as isSpinnerSilent, s as setSpinnerSilent } from "./install-doctor-script-Dr4O5HID.js";
12
+ import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-Dtf07ISQ.js";
13
+ import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-Cl4BiTkm.js";
14
+ import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-BCopjALe.js";
15
+ import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-BA_xTdWY.js";
14
16
  import { createRequire } from "node:module";
15
- import { spawn } from "node:child_process";
16
- import * as path$1 from "node:path";
17
- import path from "node:path";
18
17
  import * as fs$2 from "node:fs";
19
18
  import fs, { mkdtempSync, rmSync } from "node:fs";
19
+ import * as path$1 from "node:path";
20
+ import path from "node:path";
21
+ import { spawn } from "node:child_process";
22
+ import { tmpdir } from "node:os";
20
23
  import crypto, { randomUUID } from "node:crypto";
21
24
  import * as fs$1 from "node:fs/promises";
22
- import { tmpdir } from "node:os";
23
25
  import { performance } from "node:perf_hooks";
24
26
  //#region src/cli/utils/apply-observability.ts
25
27
  /**
@@ -269,1936 +271,1942 @@ const resolveScoreUnavailableReason = (input) => {
269
271
  return input.isAnalysisIncomplete ? "analysis-incomplete" : "api-unavailable";
270
272
  };
271
273
  //#endregion
272
- //#region src/cli/utils/scan-result-cache-payload.ts
273
- const decodeDiagnostic = decodeUnknownSync(Diagnostic);
274
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
275
- const isNullableString = (value) => value === null || typeof value === "string";
276
- const isBoolean = (value) => typeof value === "boolean";
277
- const isNumber = (value) => typeof value === "number";
278
- const isString = (value) => typeof value === "string";
279
- const isDiagnosticArray = (value) => {
280
- if (!Array.isArray(value)) return false;
281
- try {
282
- for (const entry of value) decodeDiagnostic(entry);
283
- return true;
284
- } catch {
285
- return false;
286
- }
274
+ //#region src/cli/utils/should-block-ci.ts
275
+ const shouldBlockCi = (diagnostics, blockingLevel) => {
276
+ if (blockingLevel === "none") return false;
277
+ if (blockingLevel === "warning") return diagnostics.length > 0;
278
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
287
279
  };
288
- const isScoreResult = (value) => value === null || isRecord(value) && typeof value.score === "number" && typeof value.label === "string";
289
- const isProjectInfo = (value) => isRecord(value) && typeof value.rootDirectory === "string" && typeof value.projectName === "string" && typeof value.framework === "string" && typeof value.sourceFileCount === "number";
290
- const isBaselineDelta = (value) => isRecord(value) && typeof value.baseRef === "string" && typeof value.fixedCount === "number" && typeof value.baseTotalCount === "number" && (value.crossFileMatchCount === void 0 || typeof value.crossFileMatchCount === "number");
291
- const isSuppressedRuleCountArray = (value) => Array.isArray(value) && value.every((entry) => isRecord(entry) && typeof entry.rule === "string" && typeof entry.source === "string" && typeof entry.count === "number");
292
- const REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS = [
293
- {
294
- key: "diagnostics",
295
- isValid: isDiagnosticArray
296
- },
297
- {
298
- key: "score",
299
- isValid: isScoreResult
300
- },
301
- {
302
- key: "project",
303
- isValid: isProjectInfo
304
- },
305
- {
306
- key: "userConfig",
307
- isValid: (value) => value === null || isRecord(value)
308
- },
309
- {
310
- key: "didLintFail",
311
- isValid: isBoolean
312
- },
313
- {
314
- key: "lintFailureReason",
315
- isValid: isNullableString
316
- },
317
- {
318
- key: "lintPartialFailures",
319
- isValid: isStringArray
320
- },
321
- {
322
- key: "didDeadCodeFail",
323
- isValid: isBoolean
324
- },
325
- {
326
- key: "deadCodeFailureReason",
327
- isValid: isNullableString
328
- },
329
- {
330
- key: "deadCodeOverlapped",
331
- isValid: isBoolean
332
- },
333
- {
334
- key: "directory",
335
- isValid: isString
336
- },
337
- {
338
- key: "scannedFileCount",
339
- isValid: isNumber
340
- },
341
- {
342
- key: "scannedFilePaths",
343
- isValid: isStringArray
344
- },
345
- {
346
- key: "scanElapsedMilliseconds",
347
- isValid: isNumber
348
- },
349
- {
350
- key: "lintFailureReasonKind",
351
- isValid: isNullableString
352
- },
353
- {
354
- key: "supplyChainOverlapTimedOut",
355
- isValid: isBoolean
356
- }
357
- ];
358
- const OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS = [
359
- {
360
- key: "analyzedFiles",
361
- isValid: isStringArray
362
- },
363
- {
364
- key: "baselineDelta",
365
- isValid: isBaselineDelta
366
- },
367
- {
368
- key: "scanConcurrency",
369
- isValid: isNumber
370
- },
371
- {
372
- key: "securityScanFailed",
373
- isValid: isBoolean
374
- },
375
- {
376
- key: "securityScanFailureReason",
377
- isValid: isNullableString
378
- },
379
- {
380
- key: "suppressedRuleCounts",
381
- isValid: isSuppressedRuleCountArray
382
- },
383
- {
384
- key: "manifestContentHash",
385
- isValid: isNullableString
386
- }
387
- ];
388
- const hasValidRequiredCachedScanPayloadFields = (value) => REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => field.isValid(value[field.key]));
389
- const hasValidOptionalCachedScanPayloadFields = (value) => OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => value[field.key] === void 0 || field.isValid(value[field.key]));
390
- const isCachedScanPayload = (value) => {
391
- if (!isRecord(value)) return false;
392
- return hasValidRequiredCachedScanPayloadFields(value) && hasValidOptionalCachedScanPayloadFields(value);
280
+ //#endregion
281
+ //#region src/cli/utils/to-category-key.ts
282
+ /**
283
+ * Lowercase, key-safe form of a rule category for the `diag.category.*`
284
+ * telemetry attribute namespace (categories carry spaces / capitals, e.g.
285
+ * "Performance" → `performance`, "Dead Code" → `dead_code`).
286
+ */
287
+ const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
288
+ //#endregion
289
+ //#region src/cli/utils/with-namespace.ts
290
+ /**
291
+ * Prefixes every key in a flat attribute group with `namespace.`, so a logical
292
+ * group (scan config, diagnostics rollup, score, …) carries one dotted
293
+ * namespace applied in a single place rather than hand-spelled into each key —
294
+ * which is how the wide event drifted into a flat, half-namespaced soup. The
295
+ * dotted prefix is what makes the attributes tree up in Sentry's attribute
296
+ * browser and stay group-/filter-/aggregate-able in the Spans dataset.
297
+ *
298
+ * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
299
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
300
+ * so `toSpanAttributes` can drop absent signals rather than coerce them).
301
+ */
302
+ const withNamespace = (namespace, attributes) => {
303
+ const namespaced = {};
304
+ for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
305
+ return namespaced;
393
306
  };
394
- const decodeCachedScanPayload = (value) => isCachedScanPayload(value) ? value : null;
395
- const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
396
307
  //#endregion
397
- //#region src/cli/utils/scan-result-cache.ts
398
- const createScanResultCacheInvocationState = () => ({ repositoryIdentityByRoot: /* @__PURE__ */ new Map() });
399
- const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
400
- const TOOLCHAIN_PACKAGE_SPECIFIERS = [
401
- "oxlint/package.json",
402
- "oxlint-plugin-react-doctor/package.json",
403
- "eslint-plugin-react-hooks/package.json"
404
- ];
405
- const bundledRequire = createRequire(import.meta.url);
406
- const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
407
- try {
408
- return `oxlint-plugin-react-doctor#fingerprint=${hashFileContents(bundledRequire.resolve("oxlint-plugin-react-doctor")) ?? "unreadable"}`;
409
- } catch {
410
- return "oxlint-plugin-react-doctor#fingerprint=unresolved";
411
- }
412
- })();
413
- const normalizeForStableJson = (value) => {
414
- if (value === null) return null;
415
- if (value === void 0) return void 0;
416
- const valueType = typeof value;
417
- if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
418
- if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
419
- if (Array.isArray(value)) return value.map((item) => {
420
- const normalized = normalizeForStableJson(item);
421
- return normalized === void 0 ? null : normalized;
308
+ //#region src/cli/utils/build-run-event.ts
309
+ const readEnvBoolean = (name) => {
310
+ const value = process.env[name];
311
+ if (value === void 0) return null;
312
+ return value.toLowerCase() === "true" || value === "1";
313
+ };
314
+ const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
315
+ /**
316
+ * One queryable cache temperature per scan, derived from the whole stack:
317
+ *
318
+ * - `"turbo"` — whole-repo scan-result replay (the explicit
319
+ * `wholeRepoCacheHit` flag from the CLI's cachedPayload
320
+ * branch; no scan work ran).
321
+ * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
322
+ * replays.
323
+ * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
324
+ * off-switch is on. Granular per-cache opt-outs
325
+ * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
326
+ * since the other subsystems stay live.
327
+ * - `"cold"` — caches on, zero reuse (first scan / everything changed).
328
+ *
329
+ * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
330
+ * the subsystem reuse fractions known this run (lint hit ratio, sidecar
331
+ * replay ratio), skipping subsystems that never consulted a
332
+ * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
333
+ * unweighted — the per-subsystem dims stay the precise signal; warmth is the
334
+ * p50/p90-able summary. Emitted only on the success path.
335
+ */
336
+ const buildCacheAttributes = (input) => {
337
+ if (input.result === void 0) return {};
338
+ if (input.wholeRepoCacheHit) return withNamespace("cache", {
339
+ wholeRepoHit: true,
340
+ temperature: "turbo",
341
+ warmth: 1
422
342
  });
423
- const prototype = Object.getPrototypeOf(value);
424
- if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
425
- if (!isRecord(value)) throw new Error("Unsupported cache key object");
426
- const normalizedRecord = {};
427
- for (const key of Object.keys(value).sort()) {
428
- const normalized = normalizeForStableJson(value[key]);
429
- if (normalized !== void 0) normalizedRecord[key] = normalized;
343
+ const result = input.result;
344
+ const subsystemReuseRatios = [ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount), ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)];
345
+ let knownSubsystemCount = 0;
346
+ let reuseRatioSum = 0;
347
+ for (const reuseRatio of subsystemReuseRatios) {
348
+ if (reuseRatio === null) continue;
349
+ knownSubsystemCount += 1;
350
+ reuseRatioSum += reuseRatio;
430
351
  }
431
- return normalizedRecord;
352
+ const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
353
+ const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
354
+ return withNamespace("cache", {
355
+ wholeRepoHit: input.wholeRepoCacheHit,
356
+ temperature,
357
+ warmth
358
+ });
432
359
  };
433
- const stringifyStableJson = (value) => {
434
- try {
435
- return JSON.stringify(normalizeForStableJson(value));
436
- } catch {
437
- return null;
438
- }
360
+ const resolveVersionPin = (versionInput) => {
361
+ if (versionInput === void 0 || versionInput.trim() === "") return null;
362
+ if (versionInput === "latest") return "latest";
363
+ if (/^(\.\.?\/|\/)/.test(versionInput)) return "local";
364
+ return "pinned";
439
365
  };
440
- const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
441
- const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
442
- const parseWorktreeStatusRecords = (statusOutput) => {
443
- const entries = [];
444
- const tokens = statusOutput.split("\0");
445
- for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
446
- let record = tokens[tokenIndex];
447
- if (record.length === 0) continue;
448
- if (record.length < 3 || record[2] !== " ") {
449
- if (tokenIndex !== 0) return null;
450
- record = ` ${record}`;
451
- }
452
- if (record.length < 4 || record[2] !== " ") return null;
453
- const statusCode = record.slice(0, 2);
454
- const recordPath = record.slice(3);
455
- if (statusCode.includes("R") || statusCode.includes("C")) {
456
- tokenIndex += 1;
457
- const originPath = tokens[tokenIndex];
458
- if (originPath === void 0 || originPath.length === 0) return null;
459
- entries.push({
460
- statusCode,
461
- path: recordPath,
462
- originPath
463
- });
464
- continue;
465
- }
466
- entries.push({
467
- statusCode,
468
- path: recordPath
366
+ const resolveTelemetryBlocking = (userConfig) => {
367
+ const fromAction = process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.blocking];
368
+ if (fromAction !== void 0 && isValidBlockingLevel(fromAction)) return fromAction;
369
+ return userConfig?.blocking ?? userConfig?.failOn ?? "error";
370
+ };
371
+ const buildOutcomeAttributes = (input) => {
372
+ if (input.result === void 0) {
373
+ const error = input.error;
374
+ const known = isReactDoctorError(error);
375
+ return withNamespace("outcome", {
376
+ status: "error",
377
+ exitCode: 1,
378
+ complete: false,
379
+ knownError: known,
380
+ errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
469
381
  });
470
382
  }
471
- return entries;
472
- };
473
- const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
474
- let stats;
475
- try {
476
- stats = fs.lstatSync(absolutePath);
477
- } catch {
478
- return statusCode.includes("D") ? "deleted" : null;
383
+ const result = input.result;
384
+ const summary = summarizeDiagnostics(result.diagnostics);
385
+ const blockingLevel = resolveTelemetryBlocking(input.userConfig);
386
+ const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
387
+ const gateDiagnosticSet = new Set(gateDiagnostics);
388
+ const complete = isInspectResultComplete(result);
389
+ const didLintHardFail = hasLintHardFailure(result);
390
+ const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
391
+ const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
392
+ const isClean = result.diagnostics.length === 0 && complete;
393
+ const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
394
+ const firings = summarizeRuleFirings(result.diagnostics);
395
+ const countByRule = /* @__PURE__ */ new Map();
396
+ const countByCategory = /* @__PURE__ */ new Map();
397
+ for (const firing of firings) {
398
+ countByRule.set(firing.rule, (countByRule.get(firing.rule) ?? 0) + firing.count);
399
+ countByCategory.set(firing.category, (countByCategory.get(firing.category) ?? 0) + firing.count);
479
400
  }
480
- if (stats.isSymbolicLink()) try {
481
- return `link:${fs.readlinkSync(absolutePath)}`;
482
- } catch {
483
- return null;
401
+ let topRule = null;
402
+ let topRuleCount = 0;
403
+ for (const [rule, count] of countByRule) if (count > topRuleCount) {
404
+ topRule = rule;
405
+ topRuleCount = count;
484
406
  }
485
- if (!stats.isFile()) return null;
486
- if (stats.size > 10485760) return null;
487
- return hashFileContents(absolutePath);
488
- };
489
- const buildWorktreeFingerprint = (projectDirectory, repositoryRoot) => {
490
- const statusOutput = runGit(projectDirectory, [
491
- "status",
492
- "--porcelain=v1",
493
- "-z",
494
- "--untracked-files=all"
495
- ]);
496
- if (statusOutput === null) return null;
497
- if (statusOutput.length === 0) return [];
498
- const statusEntries = parseWorktreeStatusRecords(statusOutput);
499
- if (statusEntries === null) return null;
500
- if (statusEntries.length > 300) return null;
501
- const dirtyEntries = [];
502
- for (const statusEntry of statusEntries) {
503
- const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
504
- if (contentFingerprint === null) return null;
505
- dirtyEntries.push({
506
- ...statusEntry,
507
- contentFingerprint
508
- });
407
+ const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
408
+ let diagnosticsInTestFiles = 0;
409
+ let diagnosticsInStoryFiles = 0;
410
+ let nonProductionGateExcluded = 0;
411
+ const diagnosticSiteCounts = /* @__PURE__ */ new Map();
412
+ const findingsPerFixGroup = /* @__PURE__ */ new Map();
413
+ for (const diagnostic of result.diagnostics) {
414
+ if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
415
+ if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
416
+ if (diagnostic.fileContext !== void 0 && !gateDiagnosticSet.has(diagnostic)) nonProductionGateExcluded += 1;
417
+ const diagnosticSite = JSON.stringify([
418
+ diagnostic.filePath,
419
+ diagnostic.line,
420
+ diagnostic.column,
421
+ diagnostic.plugin,
422
+ diagnostic.rule
423
+ ]);
424
+ diagnosticSiteCounts.set(diagnosticSite, (diagnosticSiteCounts.get(diagnosticSite) ?? 0) + 1);
425
+ if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
509
426
  }
510
- return dirtyEntries.sort((firstEntry, secondEntry) => {
511
- const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
512
- const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
513
- return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
514
- });
515
- };
516
- const resolveRepositoryCacheIdentity = (projectDirectory, invocationState) => {
517
- const repositoryRoot = runGit(projectDirectory, ["rev-parse", "--show-toplevel"]);
518
- if (repositoryRoot === null) return null;
519
- const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
520
- if (cachedIdentity !== void 0) return cachedIdentity;
521
- const worktreeFingerprint = buildWorktreeFingerprint(projectDirectory, repositoryRoot);
522
- const headSha = readHeadSha(repositoryRoot);
523
- const identity = worktreeFingerprint === null || headSha === null ? null : {
524
- headSha,
525
- worktreeFingerprint
526
- };
527
- invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
528
- return identity;
529
- };
530
- const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
531
- const resolveDotenvFingerprint = (projectDirectory) => {
532
- try {
533
- return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
534
- } catch {
535
- return [];
427
+ let fixGroupedFindings = 0;
428
+ for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
429
+ let sameSiteOccurrences = 0;
430
+ for (const count of diagnosticSiteCounts.values()) sameSiteOccurrences += Math.max(0, count - 1);
431
+ const categoryRollup = {};
432
+ for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
433
+ const suppressionRollup = {};
434
+ if (input.suppressedRuleCounts) {
435
+ const countBySource = {
436
+ config: 0,
437
+ override: 0,
438
+ inline: 0,
439
+ "foreign-inline": 0
440
+ };
441
+ for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
442
+ suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
443
+ suppressionRollup.suppressedConfig = countBySource.config;
444
+ suppressionRollup.suppressedOverride = countBySource.override;
445
+ suppressionRollup.suppressedInline = countBySource.inline;
446
+ suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
536
447
  }
448
+ const attributes = {
449
+ ...withNamespace("outcome", {
450
+ status: outcome,
451
+ exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
452
+ wouldBlock,
453
+ blocking: blockingLevel,
454
+ clean: isClean,
455
+ complete,
456
+ skippedChecks: result.skippedChecks.length
457
+ }),
458
+ ...withNamespace("diag", {
459
+ total: summary.totalDiagnosticCount,
460
+ errors: summary.errorCount,
461
+ warnings: summary.warningCount,
462
+ affectedFiles: summary.affectedFileCount,
463
+ inTestFiles: diagnosticsInTestFiles,
464
+ inStoryFiles: diagnosticsInStoryFiles,
465
+ nonProductionGateExcluded,
466
+ distinctRules: countByRule.size,
467
+ topRule,
468
+ sameSiteOccurrences,
469
+ fixGroups: findingsPerFixGroup.size,
470
+ fixGroupedFindings,
471
+ ...categoryRollup,
472
+ ...suppressionRollup
473
+ }),
474
+ ...withNamespace("score", {
475
+ value: result.score ? result.score.score : null,
476
+ label: result.score ? result.score.label : null,
477
+ available: result.score !== null,
478
+ unavailableReason: result.score === null ? resolveScoreUnavailableReason({
479
+ isScoreDisabled: input.noScore,
480
+ isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
481
+ }) : null
482
+ }),
483
+ ...withNamespace("lint", {
484
+ failed: input.didLintFail,
485
+ failureReasonKind: input.lintFailureReasonKind,
486
+ partialFailureCount: input.lintPartialFailureCount,
487
+ droppedFileCount: input.lintDroppedFileCount,
488
+ deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
489
+ cacheHitFiles: result.lintCacheHitFileCount,
490
+ cacheTotalFiles: result.lintCacheTotalFileCount,
491
+ cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
492
+ sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
493
+ sidecarTotalFiles: result.lintSidecarTotalFileCount,
494
+ sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
495
+ }),
496
+ ...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
497
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
498
+ ...withNamespace("securityScan", { failed: input.securityScanFailed }),
499
+ ...withNamespace("timing", {
500
+ elapsedMs: result.elapsedMilliseconds,
501
+ scanMs: result.scanElapsedMilliseconds
502
+ }),
503
+ ...withNamespace("migration", {
504
+ largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
505
+ largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
506
+ largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
507
+ })
508
+ };
509
+ if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
510
+ new: summary.totalDiagnosticCount,
511
+ fixed: result.baselineDelta.fixedCount,
512
+ baseTotal: result.baselineDelta.baseTotalCount,
513
+ crossFileMatches: result.baselineDelta.crossFileMatchCount,
514
+ degraded: false
515
+ }));
516
+ else if (input.gateExempt) attributes["baseline.degraded"] = true;
517
+ return attributes;
537
518
  };
538
- const isGitIdentityTrustworthy = (projectDirectory) => {
539
- const output = runGit(projectDirectory, ["ls-files", "-v"]);
540
- if (output === null) return false;
541
- const entryLines = output.split("\n").filter((line) => line.length > 0);
542
- return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
519
+ const buildActionAttributes = () => {
520
+ const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
521
+ return withNamespace("action", {
522
+ actorAssociation: githubActorAssociation,
523
+ runnerOs: detectRunnerOs(),
524
+ comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
525
+ reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
526
+ versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
527
+ });
543
528
  };
544
- const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
545
- const readPersistedCache = (cacheFilePath) => {
546
- try {
547
- const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
548
- if (!isRecord(parsed) || parsed.version !== 7) return {
549
- version: 7,
550
- entries: []
551
- };
552
- if (!Array.isArray(parsed.entries)) return {
553
- version: 7,
554
- entries: []
555
- };
556
- const entries = [];
557
- for (const entry of parsed.entries) {
558
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
559
- const payload = decodeCachedScanPayload(entry.payload);
560
- if (payload === null) continue;
561
- entries.push({
562
- key: entry.key,
563
- createdAtMs: entry.createdAtMs,
564
- payload
565
- });
566
- }
567
- return {
568
- version: 7,
569
- entries
570
- };
571
- } catch {
572
- return {
573
- version: 7,
574
- entries: []
575
- };
576
- }
529
+ const buildScanAttributes = (input) => {
530
+ const ruleOverrides = input.userConfig?.rules ?? {};
531
+ const ruleKeys = Object.keys(ruleOverrides);
532
+ return withNamespace("scan", {
533
+ mode: input.mode,
534
+ scope: input.scope,
535
+ parallel: input.parallel,
536
+ workerCount: input.workerCount,
537
+ maxDurationMs: input.maxDurationMs,
538
+ lint: input.lint,
539
+ maintainability: input.deadCode,
540
+ supplyChain: input.supplyChain,
541
+ scoreOnly: input.scoreOnly,
542
+ noScore: input.noScore,
543
+ respectInlineDisables: input.respectInlineDisables,
544
+ showWarnings: input.showWarnings,
545
+ usedOutputDir: input.usedOutputDir,
546
+ ignoredTagCount: input.ignoredTagCount,
547
+ hasCustomConfig: input.hasCustomConfig,
548
+ rulesConfigured: ruleKeys.length,
549
+ rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
550
+ projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
551
+ fileCount: input.result?.scannedFileCount,
552
+ nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
553
+ htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
554
+ multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
555
+ });
577
556
  };
578
557
  /**
579
- * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
580
- * cache subsystem: this whole-repo scan cache plus core's per-file lint,
581
- * sidecar caches (their `Context.Reference` defaults read the
582
- * same variable). Exported for the wide event's `cache.temperature`
583
- * derivation, which reports `"disabled"` instead of `"cold"` when the switch
584
- * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
585
- * not consulted — they leave the other subsystems live.
558
+ * Projects a scan into the namespaced attribute set for its root span — the
559
+ * canonical per-scan "wide event". Every attribute carries a dotted namespace
560
+ * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
561
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `maintainability.*`, `cache.*`,
562
+ * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
563
+ * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
564
+ * in the Spans dataset. Pure and exported so the projection (outcome
565
+ * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
566
+ * without a live Sentry client. `null`/`undefined` values are dropped so absent
567
+ * signals never become misleading `"null"`/`"undefined"` attributes. The run +
568
+ * project base context (version, command, ci/provider, framework, …) is already
569
+ * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
570
+ * adds only what those don't carry.
586
571
  */
587
- const isCacheGloballyDisabled = () => CACHE_DISABLED_VALUES.has(process.env.REACT_DOCTOR_NO_CACHE?.toLowerCase() ?? "");
588
- const resolveProjectIdentity = (projectDirectory) => {
589
- try {
590
- return fs.realpathSync.native(projectDirectory);
591
- } catch {
592
- return path.resolve(projectDirectory);
593
- }
594
- };
595
- const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
596
- const fileFingerprint = (filePath) => {
572
+ const buildRunEventAttributes = (input) => toSpanAttributes({
573
+ ...buildScanAttributes(input),
574
+ ...buildActionAttributes(),
575
+ ...buildOutcomeAttributes(input),
576
+ ...buildCacheAttributes(input)
577
+ });
578
+ /**
579
+ * Stamps the wide-event attributes onto the run's root span. A guarded no-op
580
+ * when tracing is off (no `rootSpan`) and swallow-on-throw, so telemetry can
581
+ * never break the run.
582
+ */
583
+ const recordRunEvent = (rootSpan, input) => {
584
+ if (!rootSpan) return;
597
585
  try {
598
- const stat = fs.statSync(filePath);
599
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
600
- } catch {
601
- return null;
602
- }
603
- };
604
- const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
605
- const fingerprints = [];
606
- if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
607
- for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
608
- const packageJson = bundledRequire(specifier);
609
- const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
610
- fingerprints.push(`${specifier}=${version}`);
611
- } catch {
612
- fingerprints.push(`${specifier}=missing`);
613
- }
614
- fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
615
- return fingerprints;
616
- };
617
- const buildScanResultCacheKey = (input) => {
618
- if (isCacheGloballyDisabled()) return null;
619
- if (!isGitIdentityTrustworthy(input.projectDirectory)) return null;
620
- const repositoryIdentity = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState);
621
- if (repositoryIdentity === null) return null;
622
- if (stringifyStableJson(input.userConfig) === null) return null;
623
- const cacheKeyJson = stringifyStableJson({
624
- schemaVersion: 7,
625
- projectIdentity: resolveProjectIdentity(input.projectDirectory),
626
- headSha: repositoryIdentity.headSha,
627
- worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
628
- dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
629
- reactDoctorVersion: input.version,
630
- nodeVersion: process.version,
631
- toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
632
- configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
633
- hasConfigOverride: input.hasConfigOverride,
634
- configSourceDirectory: input.configSourceDirectory,
635
- userConfig: input.userConfig,
636
- engineOptions: {
637
- lint: input.options.lint,
638
- deadCode: input.options.deadCode,
639
- supplyChain: input.options.supplyChain,
640
- includePaths: [...input.options.includePaths].sort(),
641
- excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
642
- retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
643
- customRulesOnly: input.options.customRulesOnly,
644
- respectInlineDisables: input.options.respectInlineDisables,
645
- warnings: input.options.warnings,
646
- adoptExistingLintConfig: input.options.adoptExistingLintConfig,
647
- ignoredTags: [...input.options.ignoredTags].sort(),
648
- includedTags: [...input.options.includedTags].sort(),
649
- includeTagDefaults: input.options.includeTagDefaults,
650
- concurrency: input.options.concurrency,
651
- lintBatchOrdering: resolveLintBatchOrdering(),
652
- baselineRef: input.options.baseline?.ref,
653
- changedLineRanges: input.options.changedLineRanges ?? void 0,
654
- noScore: input.options.noScore,
655
- isCi: input.options.isCi,
656
- suppressRendering: input.options.suppressRendering,
657
- supplyChainManifestChanged: input.options.supplyChainManifestChanged
658
- }
659
- });
660
- return cacheKeyJson === null ? null : hashString(cacheKeyJson);
661
- };
662
- const createScanResultCache = (projectDirectory) => {
663
- const cacheFilePath = resolveCacheFilePath(projectDirectory);
664
- const persistedCache = readPersistedCache(cacheFilePath);
665
- const entries = /* @__PURE__ */ new Map();
666
- for (const entry of persistedCache.entries) entries.set(entry.key, entry);
667
- const persist = () => {
668
- const mergedEntries = /* @__PURE__ */ new Map();
669
- for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
670
- for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
671
- atomicWriteJson(cacheFilePath, {
672
- version: 7,
673
- entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
674
- });
675
- };
676
- return {
677
- lookup: (key) => {
678
- const payload = entries.get(key)?.payload;
679
- if (payload === void 0) return null;
680
- return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
681
- },
682
- store: (key, payload) => {
683
- entries.set(key, {
684
- key,
685
- createdAtMs: Date.now(),
686
- payload: {
687
- ...payload,
688
- manifestContentHash: resolveManifestContentHash(projectDirectory)
689
- }
690
- });
691
- persist();
692
- }
693
- };
586
+ for (const [key, value] of Object.entries(buildRunEventAttributes(input))) rootSpan.attribute(key, value);
587
+ } catch {}
694
588
  };
695
589
  //#endregion
696
- //#region src/cli/utils/should-block-ci.ts
697
- const shouldBlockCi = (diagnostics, blockingLevel) => {
698
- if (blockingLevel === "none") return false;
699
- if (blockingLevel === "warning") return diagnostics.length > 0;
700
- return diagnostics.some((diagnostic) => diagnostic.severity === "error");
590
+ //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
591
+ const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
592
+ const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
593
+ return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
701
594
  };
702
595
  //#endregion
703
- //#region src/cli/utils/to-category-key.ts
704
- /**
705
- * Lowercase, key-safe form of a rule category for the `diag.category.*`
706
- * telemetry attribute namespace (categories carry spaces / capitals, e.g.
707
- * "Performance" → `performance`, "Dead Code" → `dead_code`).
708
- */
709
- const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
710
- //#endregion
711
- //#region src/cli/utils/with-namespace.ts
712
- /**
713
- * Prefixes every key in a flat attribute group with `namespace.`, so a logical
714
- * group (scan config, diagnostics rollup, score, …) carries one dotted
715
- * namespace applied in a single place rather than hand-spelled into each key —
716
- * which is how the wide event drifted into a flat, half-namespaced soup. The
717
- * dotted prefix is what makes the attributes tree up in Sentry's attribute
718
- * browser and stay group-/filter-/aggregate-able in the Spans dataset.
719
- *
720
- * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
721
- * numeric attribute and `p75(...)`/`avg(...)` work; `null`/`undefined` are kept
722
- * so `toSpanAttributes` can drop absent signals rather than coerce them).
723
- */
724
- const withNamespace = (namespace, attributes) => {
725
- const namespaced = {};
726
- for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
727
- return namespaced;
596
+ //#region src/cli/utils/filter-diagnostics-by-changed-lines.ts
597
+ const filterDiagnosticsByChangedLines = (input) => {
598
+ const rangesByFile = /* @__PURE__ */ new Map();
599
+ for (const entry of input.changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
600
+ return input.diagnostics.filter((diagnostic) => {
601
+ const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(input.directory, diagnostic.filePath) : diagnostic.filePath);
602
+ const ranges = rangesByFile.get(relativePath);
603
+ return ranges !== void 0 && diagnosticIntersectsLineRanges(diagnostic, ranges);
604
+ });
728
605
  };
729
606
  //#endregion
730
- //#region src/cli/utils/build-run-event.ts
731
- const readEnvBoolean = (name) => {
732
- const value = process.env[name];
733
- if (value === void 0) return null;
734
- return value.toLowerCase() === "true" || value === "1";
735
- };
736
- const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
607
+ //#region src/cli/utils/resolve-invocation-oxlint-concurrency.ts
737
608
  /**
738
- * One queryable cache temperature per scan, derived from the whole stack:
739
- *
740
- * - `"turbo"` — whole-repo scan-result replay (the explicit
741
- * `wholeRepoCacheHit` flag from the CLI's cachedPayload
742
- * branch; no scan work ran).
743
- * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
744
- * replays.
745
- * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
746
- * off-switch is on. Granular per-cache opt-outs
747
- * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
748
- * since the other subsystems stay live.
749
- * - `"cold"` — caches on, zero reuse (first scan / everything changed).
750
- *
751
- * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
752
- * the subsystem reuse fractions known this run (lint hit ratio, sidecar
753
- * replay ratio), skipping subsystems that never consulted a
754
- * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
755
- * unweighted — the per-subsystem dims stay the precise signal; warmth is the
756
- * p50/p90-able summary. Emitted only on the success path.
609
+ * The invocation-wide oxlint worker count: an explicit `--concurrency` pin
610
+ * when given, else the env-seeded / auto-budgeted `OxlintConcurrency`, clamped
611
+ * to the spawn ceiling. Every project of a workspace scan shares this one
612
+ * pool, so it also sizes how many projects may be in flight at once.
757
613
  */
758
- const buildCacheAttributes = (input) => {
759
- if (input.result === void 0) return {};
760
- if (input.wholeRepoCacheHit) return withNamespace("cache", {
761
- wholeRepoHit: true,
762
- temperature: "turbo",
763
- warmth: 1
764
- });
765
- const result = input.result;
766
- const subsystemReuseRatios = [ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount), ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)];
767
- let knownSubsystemCount = 0;
768
- let reuseRatioSum = 0;
769
- for (const reuseRatio of subsystemReuseRatios) {
770
- if (reuseRatio === null) continue;
771
- knownSubsystemCount += 1;
772
- reuseRatioSum += reuseRatio;
773
- }
774
- const warmth = knownSubsystemCount > 0 ? reuseRatioSum / knownSubsystemCount : null;
775
- const temperature = warmth !== null && warmth > 0 ? "warm" : isCacheGloballyDisabled() ? "disabled" : "cold";
776
- return withNamespace("cache", {
777
- wholeRepoHit: input.wholeRepoCacheHit,
778
- temperature,
779
- warmth
614
+ const resolveInvocationOxlintConcurrency = (requestedConcurrency) => resolveScanConcurrency(requestedConcurrency ?? runSync(OxlintConcurrency));
615
+ //#endregion
616
+ //#region src/cli/utils/resolve-oxlint-node.ts
617
+ const consoleWarn = (message) => warn(highlighter.warn(message));
618
+ const consoleBreak = log("");
619
+ const consoleDim = (message) => log(highlighter.gray(message));
620
+ const consoleSuccess = (message) => log(highlighter.success(message));
621
+ const promptShouldInstallNode = () => promise(async () => {
622
+ const { shouldInstallNode } = await prompts({
623
+ type: "confirm",
624
+ name: "shouldInstallNode",
625
+ message: `Install Node 24 via nvm to enable lint checks?`,
626
+ initial: true
780
627
  });
628
+ return Boolean(shouldInstallNode);
629
+ });
630
+ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => gen(function* () {
631
+ if (!isLintEnabled) return null;
632
+ const resolver = yield* NodeResolver;
633
+ const initial = yield* resolver.resolve();
634
+ if (initial !== null) {
635
+ if (!initial.isCurrentNode && !isQuiet) {
636
+ yield* consoleWarn(`Node ${process.version} is unsupported by oxlint. Using Node ${initial.version} from nvm.`);
637
+ yield* consoleBreak;
638
+ }
639
+ return initial.binaryPath;
640
+ }
641
+ if (isQuiet) return null;
642
+ yield* consoleWarn(`Node ${process.version} is not compatible with oxlint (requires ${OXLINT_NODE_REQUIREMENT}). Lint checks will be skipped.`);
643
+ const isNvmInstalled = yield* resolver.isNvmInstalled();
644
+ if (isNvmInstalled && process.stdin.isTTY) {
645
+ if (yield* promptShouldInstallNode()) {
646
+ yield* consoleBreak;
647
+ const fresh = (yield* resolver.installViaNvm()) ? yield* resolver.resolve() : null;
648
+ if (fresh) {
649
+ yield* consoleBreak;
650
+ yield* consoleSuccess(`Node ${fresh.version} installed. Using it for lint checks.`);
651
+ yield* consoleBreak;
652
+ return fresh.binaryPath;
653
+ }
654
+ yield* consoleBreak;
655
+ yield* consoleWarn("Failed to install Node via nvm. Skipping lint checks.");
656
+ yield* consoleBreak;
657
+ return null;
658
+ }
659
+ } else if (isNvmInstalled) yield* consoleDim(` Run: nvm install 24`);
660
+ else yield* consoleDim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
661
+ yield* consoleBreak;
662
+ return null;
663
+ });
664
+ const resolveOxlintNode = (isLintEnabled, isQuiet) => runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(provide(NodeResolver.layerNode)));
665
+ //#endregion
666
+ //#region src/cli/utils/resolve-cli-categories.ts
667
+ const categoryByLowercase = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category) => [category.toLowerCase(), category]));
668
+ const knownCategoryList = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
669
+ const splitCategoryFlagValue = (value) => value.split(",").map((category) => category.trim()).filter((category) => category.length > 0);
670
+ const resolveCliCategories = (categoryFlag) => {
671
+ const rawCategoryValues = categoryFlag === void 0 ? [] : Array.isArray(categoryFlag) ? categoryFlag : [categoryFlag];
672
+ const resolvedCategories = [];
673
+ const seenCategories = /* @__PURE__ */ new Set();
674
+ for (const rawCategoryValue of rawCategoryValues) for (const categoryQuery of splitCategoryFlagValue(rawCategoryValue)) {
675
+ const matchedCategory = categoryByLowercase.get(categoryQuery.toLowerCase());
676
+ if (matchedCategory === void 0) throw new CliInputError(`Unknown category "${categoryQuery}". Expected one of: ${knownCategoryList}.`);
677
+ if (!seenCategories.has(matchedCategory)) {
678
+ seenCategories.add(matchedCategory);
679
+ resolvedCategories.push(matchedCategory);
680
+ }
681
+ }
682
+ if (rawCategoryValues.length > 0 && resolvedCategories.length === 0) throw new CliInputError(`--category requires one of: ${knownCategoryList}.`);
683
+ return resolvedCategories.length > 0 ? resolvedCategories : void 0;
781
684
  };
782
- const resolveVersionPin = (versionInput) => {
783
- if (versionInput === void 0 || versionInput.trim() === "") return null;
784
- if (versionInput === "latest") return "latest";
785
- if (/^(\.\.?\/|\/)/.test(versionInput)) return "local";
786
- return "pinned";
685
+ //#endregion
686
+ //#region src/cli/utils/resolve-inspect-options.ts
687
+ const resolveIgnoredTags = (userConfig, includedTags) => {
688
+ const ignoredTags = new Set(userConfig?.ignore?.tags ?? []);
689
+ for (const includedTag of includedTags) ignoredTags.delete(includedTag);
690
+ return ignoredTags;
787
691
  };
788
- const resolveTelemetryBlocking = (userConfig) => {
789
- const fromAction = process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.blocking];
790
- if (fromAction !== void 0 && isValidBlockingLevel(fromAction)) return fromAction;
791
- return userConfig?.blocking ?? userConfig?.failOn ?? "error";
692
+ const resolveInspectOptions = (inputOptions, userConfig) => {
693
+ const includedTags = inputOptions.includedTags ?? /* @__PURE__ */ new Set();
694
+ const hasIncludedTags = includedTags.size > 0;
695
+ return {
696
+ lint: inputOptions.lint ?? userConfig?.lint ?? true,
697
+ deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
698
+ supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
699
+ verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
700
+ outputDirectory: inputOptions.outputDirectory || null,
701
+ scoreOnly: inputOptions.scoreOnly ?? false,
702
+ noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
703
+ isCi: inputOptions.isCi ?? false,
704
+ isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
705
+ isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
706
+ silent: inputOptions.silent ?? false,
707
+ includePaths: inputOptions.includePaths ?? [],
708
+ customRulesOnly: hasIncludedTags ? false : userConfig?.customRulesOnly ?? false,
709
+ share: userConfig?.share ?? true,
710
+ respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
711
+ warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
712
+ categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
713
+ adoptExistingLintConfig: hasIncludedTags ? false : userConfig?.adoptExistingLintConfig ?? true,
714
+ ignoredTags: resolveIgnoredTags(userConfig, includedTags),
715
+ includedTags,
716
+ includeTagDefaults: inputOptions.includeTagDefaults ?? false,
717
+ scoreDisabledMessage: inputOptions.scoreDisabledMessage,
718
+ outputSurface: inputOptions.outputSurface ?? "cli",
719
+ suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
720
+ uiLayers: inputOptions.uiLayers ?? null,
721
+ concurrentScan: inputOptions.concurrentScan ?? false,
722
+ concurrency: inputOptions.concurrency,
723
+ maxDurationMs: inputOptions.maxDurationMs ?? null,
724
+ baseline: inputOptions.baseline ?? null,
725
+ changedLineRanges: inputOptions.changedLineRanges ?? null,
726
+ supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
727
+ excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
728
+ retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
729
+ precomputedSourceFileCount: inputOptions.precomputedSourceFileCount,
730
+ precomputedSourceFiles: inputOptions.precomputedSourceFiles
731
+ };
792
732
  };
793
- const buildOutcomeAttributes = (input) => {
794
- if (input.result === void 0) {
795
- const error = input.error;
796
- const known = isReactDoctorError(error);
797
- return withNamespace("outcome", {
798
- status: "error",
799
- exitCode: 1,
800
- complete: false,
801
- knownError: known,
802
- errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
803
- });
804
- }
805
- const result = input.result;
806
- const summary = summarizeDiagnostics(result.diagnostics);
807
- const blockingLevel = resolveTelemetryBlocking(input.userConfig);
808
- const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
809
- const gateDiagnosticSet = new Set(gateDiagnostics);
810
- const complete = isInspectResultComplete(result);
811
- const didLintHardFail = hasLintHardFailure(result);
812
- const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
813
- const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
814
- const isClean = result.diagnostics.length === 0 && complete;
815
- const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
816
- const firings = summarizeRuleFirings(result.diagnostics);
817
- const countByRule = /* @__PURE__ */ new Map();
818
- const countByCategory = /* @__PURE__ */ new Map();
819
- for (const firing of firings) {
820
- countByRule.set(firing.rule, (countByRule.get(firing.rule) ?? 0) + firing.count);
821
- countByCategory.set(firing.category, (countByCategory.get(firing.category) ?? 0) + firing.count);
733
+ //#endregion
734
+ //#region src/cli/utils/sum-file-counts-matching.ts
735
+ const sumFileCountsMatching = (messages, countPattern) => messages.reduce((total, message) => {
736
+ const match = countPattern.exec(message);
737
+ return match ? total + Number(match[1]) : total;
738
+ }, 0);
739
+ //#endregion
740
+ //#region src/cli/utils/count-deadline-skipped-files.ts
741
+ const DEADLINE_SKIPPED_MESSAGE_PATTERN = /^(\d+) file\(s\) skipped — max scan duration reached/;
742
+ const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DEADLINE_SKIPPED_MESSAGE_PATTERN);
743
+ //#endregion
744
+ //#region src/cli/utils/count-dropped-lint-files.ts
745
+ const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
746
+ const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
747
+ //#endregion
748
+ //#region src/cli/utils/build-empty-report-message.ts
749
+ const buildEmptyReportMessage = (input) => {
750
+ const categoryFilters = [...input.categoryFilters];
751
+ if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
752
+ if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
753
+ return "No issues found!";
754
+ };
755
+ //#endregion
756
+ //#region src/cli/utils/build-inspect-result.ts
757
+ const buildInspectResult = (input) => {
758
+ const { payload, cacheStats } = input;
759
+ const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
760
+ didLintFail: payload.didLintFail,
761
+ lintFailureReason: payload.lintFailureReason,
762
+ lintPartialFailures: payload.lintPartialFailures,
763
+ didDeadCodeFail: payload.didDeadCodeFail,
764
+ deadCodeFailureReason: payload.deadCodeFailureReason,
765
+ supplyChainOverlapTimedOut: payload.supplyChainOverlapTimedOut,
766
+ securityScanFailed: payload.securityScanFailed ?? false,
767
+ securityScanFailureReason: payload.securityScanFailureReason ?? null
768
+ });
769
+ return {
770
+ diagnostics: [...payload.diagnostics],
771
+ score: payload.score,
772
+ skippedChecks,
773
+ ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
774
+ project: payload.project,
775
+ elapsedMilliseconds: input.elapsedMilliseconds,
776
+ scannedFileCount: payload.scannedFileCount,
777
+ scannedFilePaths: payload.scannedFilePaths,
778
+ analyzedFiles: payload.analyzedFiles ?? [],
779
+ scanElapsedMilliseconds: payload.scanElapsedMilliseconds,
780
+ ...cacheStats.lintCacheTotalFileCount === null ? {} : {
781
+ lintCacheHitFileCount: cacheStats.lintCacheHitFileCount,
782
+ lintCacheTotalFileCount: cacheStats.lintCacheTotalFileCount
783
+ },
784
+ ...cacheStats.lintSidecarTotalFileCount === null ? {} : {
785
+ lintSidecarReplayedFileCount: cacheStats.lintSidecarReplayedFileCount,
786
+ lintSidecarTotalFileCount: cacheStats.lintSidecarTotalFileCount
787
+ },
788
+ ...cacheStats.deadCodeCacheHit === null ? {} : { deadCodeCacheHit: cacheStats.deadCodeCacheHit },
789
+ ...cacheStats.deadCodeSummaryCacheHits === null || cacheStats.deadCodeSummaryCacheMisses === null ? {} : {
790
+ deadCodeSummaryCacheHits: cacheStats.deadCodeSummaryCacheHits,
791
+ deadCodeSummaryCacheMisses: cacheStats.deadCodeSummaryCacheMisses
792
+ },
793
+ ...payload.baselineDelta ? { baselineDelta: payload.baselineDelta } : {}
794
+ };
795
+ };
796
+ //#endregion
797
+ //#region src/cli/utils/build-no-score-message.ts
798
+ const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
799
+ const buildNoScoreMessage = (input) => {
800
+ const unavailableReason = resolveScoreUnavailableReason({
801
+ isScoreDisabled: input.isScoreDisabled,
802
+ isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
803
+ });
804
+ let reason;
805
+ switch (unavailableReason) {
806
+ case "disabled":
807
+ reason = input.disabledMessage ?? "Score disabled by --no-score.";
808
+ break;
809
+ case "analysis-incomplete":
810
+ reason = "Score not shown because lint or maintainability analysis could not complete.";
811
+ break;
812
+ case "api-unavailable":
813
+ reason = "Score unavailable (could not reach the score API).";
814
+ break;
822
815
  }
823
- let topRule = null;
824
- let topRuleCount = 0;
825
- for (const [rule, count] of countByRule) if (count > topRuleCount) {
826
- topRule = rule;
827
- topRuleCount = count;
816
+ return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
817
+ };
818
+ //#endregion
819
+ //#region src/cli/utils/filter-diagnostics-by-categories.ts
820
+ const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
821
+ //#endregion
822
+ //#region src/cli/utils/has-incomplete-score-analysis.ts
823
+ const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
824
+ //#endregion
825
+ //#region src/cli/utils/format-diagnostic-site.ts
826
+ const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
827
+ //#endregion
828
+ //#region src/cli/utils/format-rule-summary.ts
829
+ const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
830
+ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
831
+ const firstDiagnostic = ruleDiagnostics[0];
832
+ if (firstDiagnostic === void 0) return "";
833
+ const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
834
+ const sections = [
835
+ `Rule: ${ruleKey}`,
836
+ `Severity: ${firstDiagnostic.severity}`,
837
+ `Category: ${firstDiagnostic.category}`,
838
+ `Count: ${ruleDiagnostics.length}`,
839
+ "",
840
+ distinctMessages.join("\n\n")
841
+ ];
842
+ if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
843
+ if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
844
+ const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
845
+ if (fixRecipeLine) sections.push("", fixRecipeLine);
846
+ sections.push("", "Files:");
847
+ for (const diagnostic of ruleDiagnostics) {
848
+ sections.push(` ${formatDiagnosticSite(diagnostic)}${formatFileContextTag(diagnostic)}`);
849
+ if (diagnostic.suppressionHint) sections.push(` ${diagnostic.suppressionHint}`);
828
850
  }
829
- const largestRuleBucket = buildRuleBlastRadii(result.diagnostics)[0] ?? null;
830
- let diagnosticsInTestFiles = 0;
831
- let diagnosticsInStoryFiles = 0;
832
- let nonProductionGateExcluded = 0;
833
- const diagnosticSiteCounts = /* @__PURE__ */ new Map();
834
- const findingsPerFixGroup = /* @__PURE__ */ new Map();
835
- for (const diagnostic of result.diagnostics) {
836
- if (diagnostic.fileContext === "test") diagnosticsInTestFiles += 1;
837
- if (diagnostic.fileContext === "story") diagnosticsInStoryFiles += 1;
838
- if (diagnostic.fileContext !== void 0 && !gateDiagnosticSet.has(diagnostic)) nonProductionGateExcluded += 1;
839
- const diagnosticSite = JSON.stringify([
840
- diagnostic.filePath,
841
- diagnostic.line,
842
- diagnostic.column,
843
- diagnostic.plugin,
844
- diagnostic.rule
845
- ]);
846
- diagnosticSiteCounts.set(diagnosticSite, (diagnosticSiteCounts.get(diagnosticSite) ?? 0) + 1);
847
- if (diagnostic.fixGroupId) findingsPerFixGroup.set(diagnostic.fixGroupId, (findingsPerFixGroup.get(diagnostic.fixGroupId) ?? 0) + 1);
851
+ return `${sections.join("\n")}\n`;
852
+ };
853
+ //#endregion
854
+ //#region src/cli/utils/write-diagnostics-directory.ts
855
+ const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
856
+ const readPreviousRuleDumpFileNames = (directory) => {
857
+ try {
858
+ const previous = JSON.parse(fs$2.readFileSync(path$1.join(directory, "diagnostics.json"), "utf8"));
859
+ return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
860
+ } catch {
861
+ return /* @__PURE__ */ new Set();
848
862
  }
849
- let fixGroupedFindings = 0;
850
- for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
851
- let sameSiteOccurrences = 0;
852
- for (const count of diagnosticSiteCounts.values()) sameSiteOccurrences += Math.max(0, count - 1);
853
- const categoryRollup = {};
854
- for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
855
- const suppressionRollup = {};
856
- if (input.suppressedRuleCounts) {
857
- const countBySource = {
858
- config: 0,
859
- override: 0,
860
- inline: 0,
861
- "foreign-inline": 0
862
- };
863
- for (const suppression of input.suppressedRuleCounts) countBySource[suppression.source] += suppression.count;
864
- suppressionRollup.suppressed = countBySource.config + countBySource.override + countBySource.inline + countBySource["foreign-inline"];
865
- suppressionRollup.suppressedConfig = countBySource.config;
866
- suppressionRollup.suppressedOverride = countBySource.override;
867
- suppressionRollup.suppressedInline = countBySource.inline;
868
- suppressionRollup.suppressedForeignInline = countBySource["foreign-inline"];
863
+ };
864
+ const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
865
+ const directory = outputDirectory ? path$1.resolve(outputDirectory) : path$1.join(tmpdir(), `react-doctor-${randomUUID()}`);
866
+ if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) fs$2.rmSync(path$1.join(directory, fileName), { force: true });
867
+ fs$2.mkdirSync(directory, { recursive: true });
868
+ for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) fs$2.writeFileSync(path$1.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
869
+ fs$2.writeFileSync(path$1.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
870
+ return directory;
871
+ };
872
+ //#endregion
873
+ //#region src/cli/utils/print-diagnostics-dump.ts
874
+ const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
875
+ const writtenDirectory = yield* try_({
876
+ try: () => writeDiagnosticsDirectory(diagnostics, outputDirectory),
877
+ catch: (cause) => cause
878
+ }).pipe(orElseSucceed(() => null));
879
+ if (writtenDirectory === null || !verbose && !outputDirectory) return;
880
+ const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
881
+ yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
882
+ });
883
+ //#endregion
884
+ //#region src/cli/utils/collect-affected-files.ts
885
+ const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
886
+ //#endregion
887
+ //#region src/cli/utils/print-footer.ts
888
+ const buildShareUrl = (input) => {
889
+ const errorCount = input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
890
+ const warningCount = input.diagnostics.length - errorCount;
891
+ const affectedFileCount = collectAffectedFiles(input.diagnostics).size;
892
+ const parameters = new URLSearchParams({ p: input.projectName });
893
+ if (input.scoreResult) parameters.set("s", String(input.scoreResult.score));
894
+ if (errorCount > 0) parameters.set("e", String(errorCount));
895
+ if (warningCount > 0) parameters.set("w", String(warningCount));
896
+ if (affectedFileCount > 0) parameters.set("f", String(affectedFileCount));
897
+ return `${SHARE_BASE_URL}?${parameters.toString()}`;
898
+ };
899
+ const printFooter = (input) => gen(function* () {
900
+ yield* log("");
901
+ yield* log(buildSectionDivider());
902
+ yield* log("");
903
+ const shareUrl = input.isOffline ? null : buildShareUrl(input);
904
+ for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
905
+ });
906
+ //#endregion
907
+ //#region src/cli/utils/format-elapsed-time.ts
908
+ const formatElapsedTime = (elapsedMilliseconds) => {
909
+ if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
910
+ return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
911
+ };
912
+ //#endregion
913
+ //#region src/cli/utils/format-skipped-check-label.ts
914
+ const formatSkippedCheckLabel = (skippedCheck) => skippedCheck === "dead-code" ? "maintainability" : skippedCheck;
915
+ //#endregion
916
+ //#region src/cli/utils/pluralize.ts
917
+ const pluralize = (count, singularNoun) => `${count} ${singularNoun}${count === 1 ? "" : "s"}`;
918
+ //#endregion
919
+ //#region src/cli/utils/print-headless-report.ts
920
+ const formatContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
921
+ const printHeadlessReport = (input) => gen(function* () {
922
+ yield* log(`${highlighter.success("✔")} Scanned ${pluralize(input.scannedFileCount, "file")} in ${formatElapsedTime(input.elapsedMilliseconds)}`);
923
+ yield* log("");
924
+ yield* log(highlighter.bold(`React Doctor — ${input.projectName}`));
925
+ if (input.scoreResult) yield* log(`Score: ${input.scoreResult.score} / 100 ${input.scoreResult.label}`);
926
+ else yield* log(highlighter.gray(input.noScoreMessage));
927
+ if (input.diagnostics.length === 0) {
928
+ yield* log("");
929
+ yield* log(highlighter.success(`✔ ${input.emptyStateMessage}`));
930
+ } else {
931
+ yield* log("");
932
+ yield* log(highlighter.bold(pluralize(input.diagnostics.length, "issue")));
933
+ const categoryCounts = /* @__PURE__ */ new Map();
934
+ for (const diagnostic of input.diagnostics) {
935
+ const counts = categoryCounts.get(diagnostic.category) ?? {
936
+ error: 0,
937
+ warning: 0
938
+ };
939
+ categoryCounts.set(diagnostic.category, {
940
+ error: counts.error + Number(diagnostic.severity === "error"),
941
+ warning: counts.warning + Number(diagnostic.severity === "warning")
942
+ });
943
+ }
944
+ for (const [category, counts] of categoryCounts) {
945
+ const countParts = [];
946
+ if (counts.error > 0) countParts.push(pluralize(counts.error, "error"));
947
+ if (counts.warning > 0) countParts.push(pluralize(counts.warning, "warning"));
948
+ yield* log(`${category}: ${countParts.join(", ")}`);
949
+ }
950
+ yield* log("");
951
+ for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(input.diagnostics)) {
952
+ const representativeDiagnostic = ruleDiagnostics[0];
953
+ if (representativeDiagnostic === void 0) continue;
954
+ const severityIcon = representativeDiagnostic.severity === "error" ? "✖" : "⚠";
955
+ const colorize = representativeDiagnostic.severity === "error" ? highlighter.error : highlighter.warn;
956
+ const occurrenceSuffix = ruleDiagnostics.length > 1 ? ` ×${ruleDiagnostics.length}` : "";
957
+ yield* log(colorize(`${severityIcon} ${representativeDiagnostic.title ?? ruleKey}${occurrenceSuffix}`));
958
+ yield* log(highlighter.gray(` ${ruleKey}`));
959
+ for (const diagnostic of ruleDiagnostics) yield* log(` ${formatDiagnosticSite(diagnostic)}${formatContextTag(diagnostic)}`);
960
+ }
869
961
  }
870
- const attributes = {
871
- ...withNamespace("outcome", {
872
- status: outcome,
873
- exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
874
- wouldBlock,
875
- blocking: blockingLevel,
876
- clean: isClean,
877
- complete,
878
- skippedChecks: result.skippedChecks.length
879
- }),
880
- ...withNamespace("diag", {
881
- total: summary.totalDiagnosticCount,
882
- errors: summary.errorCount,
883
- warnings: summary.warningCount,
884
- affectedFiles: summary.affectedFileCount,
885
- inTestFiles: diagnosticsInTestFiles,
886
- inStoryFiles: diagnosticsInStoryFiles,
887
- nonProductionGateExcluded,
888
- distinctRules: countByRule.size,
889
- topRule,
890
- sameSiteOccurrences,
891
- fixGroups: findingsPerFixGroup.size,
892
- fixGroupedFindings,
893
- ...categoryRollup,
894
- ...suppressionRollup
895
- }),
896
- ...withNamespace("score", {
897
- value: result.score ? result.score.score : null,
898
- label: result.score ? result.score.label : null,
899
- available: result.score !== null,
900
- unavailableReason: result.score === null ? resolveScoreUnavailableReason({
901
- isScoreDisabled: input.noScore,
902
- isAnalysisIncomplete: input.didLintFail === true || input.didDeadCodeFail === true
903
- }) : null
904
- }),
905
- ...withNamespace("lint", {
906
- failed: input.didLintFail,
907
- failureReasonKind: input.lintFailureReasonKind,
908
- partialFailureCount: input.lintPartialFailureCount,
909
- droppedFileCount: input.lintDroppedFileCount,
910
- deadlineSkippedFileCount: input.lintDeadlineSkippedFileCount,
911
- cacheHitFiles: result.lintCacheHitFileCount,
912
- cacheTotalFiles: result.lintCacheTotalFileCount,
913
- cacheHitRatio: ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
914
- sidecarReplayedFiles: result.lintSidecarReplayedFileCount,
915
- sidecarTotalFiles: result.lintSidecarTotalFileCount,
916
- sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
917
- }),
918
- ...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
919
- ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
920
- ...withNamespace("securityScan", { failed: input.securityScanFailed }),
921
- ...withNamespace("timing", {
922
- elapsedMs: result.elapsedMilliseconds,
923
- scanMs: result.scanElapsedMilliseconds
924
- }),
925
- ...withNamespace("migration", {
926
- largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
927
- largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
928
- largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
929
- })
962
+ if (input.skippedChecks.length > 0) {
963
+ yield* log("");
964
+ yield* warn(highlighter.warn(`Results are incomplete: ${input.skippedChecks.map(formatSkippedCheckLabel).join(" and ")} checks failed.`));
965
+ }
966
+ });
967
+ //#endregion
968
+ //#region src/cli/utils/render-agent-guidance.ts
969
+ const AGENT_GUIDANCE_LINES = [
970
+ "Treat React Doctor diagnostics as starting hypotheses. Read the relevant code before confirming or suppressing each finding.",
971
+ "For each group, decide true positive, false positive, or needs-human-review, then assign high/medium/low confidence.",
972
+ "Do not suppress a finding without evidence from the file in question. Confidence requires code context.",
973
+ "Understand the root cause before editing. Fix the underlying code instead of changing react-doctor config or suppressing rules unless explicitly asked.",
974
+ "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
975
+ "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
976
+ "Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
977
+ "Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
978
+ "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
979
+ "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
980
+ "When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
981
+ "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
982
+ "If a fix needs an API, UX, or architecture decision, stop and ask before editing."
983
+ ];
984
+ const printAgentGuidance = () => gen(function* () {
985
+ yield* log(`${highlighter.bold("Agent guidance")}`);
986
+ for (const line of AGENT_GUIDANCE_LINES) yield* log(highlighter.gray(` - ${line}`));
987
+ yield* log("");
988
+ });
989
+ //#endregion
990
+ //#region src/cli/utils/finalize-inspect-result.ts
991
+ const buildInspectPresentation = ({ options, payload }) => {
992
+ const surfaceDiagnostics = filterDiagnosticsForSurface([...payload.diagnostics], options.outputSurface, payload.userConfig);
993
+ return {
994
+ diagnostics: filterDiagnosticsByCategories(surfaceDiagnostics, options.categoryFilters),
995
+ demotedDiagnosticCount: payload.diagnostics.length - surfaceDiagnostics.length
930
996
  };
931
- if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
932
- new: summary.totalDiagnosticCount,
933
- fixed: result.baselineDelta.fixedCount,
934
- baseTotal: result.baselineDelta.baseTotalCount,
935
- crossFileMatches: result.baselineDelta.crossFileMatchCount,
936
- degraded: false
937
- }));
938
- else if (input.gateExempt) attributes["baseline.degraded"] = true;
939
- return attributes;
940
997
  };
941
- const buildActionAttributes = () => {
942
- const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
943
- return withNamespace("action", {
944
- actorAssociation: githubActorAssociation,
945
- runnerOs: detectRunnerOs(),
946
- comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
947
- reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
948
- versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
998
+ const printScoreOnlyInspectResult = ({ options, payload, diagnostics, noScoreMessage }) => gen(function* () {
999
+ if (options.outputDirectory !== null) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, false, "stderr");
1000
+ if (payload.score) {
1001
+ yield* log(`${payload.score.score}`);
1002
+ return;
1003
+ }
1004
+ yield* error(highlighter.gray(noScoreMessage));
1005
+ });
1006
+ const printFullInspectResultDetails = ({ options, payload, diagnostics, demotedDiagnosticCount }) => gen(function* () {
1007
+ if (options.outputDirectory !== null || options.verbose) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, options.verbose);
1008
+ if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
1009
+ yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
1010
+ yield* log("");
1011
+ }
1012
+ yield* printFooter({
1013
+ diagnostics,
1014
+ scoreResult: payload.score,
1015
+ projectName: payload.project.projectName,
1016
+ isOffline: options.isCi || !options.share || payload.score === null
949
1017
  });
950
- };
951
- const buildScanAttributes = (input) => {
952
- const ruleOverrides = input.userConfig?.rules ?? {};
953
- const ruleKeys = Object.keys(ruleOverrides);
954
- return withNamespace("scan", {
955
- mode: input.mode,
956
- scope: input.scope,
957
- parallel: input.parallel,
958
- workerCount: input.workerCount,
959
- maxDurationMs: input.maxDurationMs,
960
- lint: input.lint,
961
- maintainability: input.deadCode,
962
- supplyChain: input.supplyChain,
963
- scoreOnly: input.scoreOnly,
964
- noScore: input.noScore,
965
- respectInlineDisables: input.respectInlineDisables,
966
- showWarnings: input.showWarnings,
967
- usedOutputDir: input.usedOutputDir,
968
- ignoredTagCount: input.ignoredTagCount,
969
- hasCustomConfig: input.hasCustomConfig,
970
- rulesConfigured: ruleKeys.length,
971
- rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
972
- projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
973
- fileCount: input.result?.scannedFileCount,
974
- nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
975
- htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
976
- multilineDiagnosticCount: input.result?.diagnostics.filter((diagnostic) => (diagnostic.endLine ?? diagnostic.line) > diagnostic.line).length
1018
+ });
1019
+ const printFullInspectResult = ({ options, elapsedMilliseconds, payload, result, diagnostics, demotedDiagnosticCount, noScoreMessage }) => gen(function* () {
1020
+ if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
1021
+ yield* printHeadlessReport({
1022
+ diagnostics,
1023
+ elapsedMilliseconds,
1024
+ emptyStateMessage: buildEmptyReportMessage({
1025
+ categoryFilters: options.categoryFilters,
1026
+ demotedDiagnosticCount,
1027
+ outputSurface: options.outputSurface
1028
+ }),
1029
+ noScoreMessage,
1030
+ projectName: payload.project.projectName,
1031
+ scannedFileCount: payload.scannedFileCount,
1032
+ scoreResult: result.skippedChecks.length > 0 ? null : payload.score,
1033
+ skippedChecks: result.skippedChecks
977
1034
  });
978
- };
979
- /**
980
- * Projects a scan into the namespaced attribute set for its root span — the
981
- * canonical per-scan "wide event". Every attribute carries a dotted namespace
982
- * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
983
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `maintainability.*`, `cache.*`,
984
- * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
985
- * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
986
- * in the Spans dataset. Pure and exported so the projection (outcome
987
- * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
988
- * without a live Sentry client. `null`/`undefined` values are dropped so absent
989
- * signals never become misleading `"null"`/`"undefined"` attributes. The run +
990
- * project base context (version, command, ci/provider, framework, …) is already
991
- * on the span from `withSentryRunSpan` / `recordSentryProjectContext`, so this
992
- * adds only what those don't carry.
993
- */
994
- const buildRunEventAttributes = (input) => toSpanAttributes({
995
- ...buildScanAttributes(input),
996
- ...buildActionAttributes(),
997
- ...buildOutcomeAttributes(input),
998
- ...buildCacheAttributes(input)
1035
+ yield* printFullInspectResultDetails({
1036
+ options,
1037
+ payload,
1038
+ diagnostics,
1039
+ demotedDiagnosticCount
1040
+ });
1041
+ });
1042
+ const finalizeInspectResult = (input) => gen(function* () {
1043
+ const { payload } = input;
1044
+ const result = buildInspectResult(input);
1045
+ const noScoreMessage = buildNoScoreMessage({
1046
+ isScoreDisabled: input.options.noScore,
1047
+ isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
1048
+ disabledMessage: input.options.scoreDisabledMessage
1049
+ });
1050
+ if (input.options.suppressRendering) return result;
1051
+ const presentation = buildInspectPresentation(input);
1052
+ if (input.options.scoreOnly) {
1053
+ yield* printScoreOnlyInspectResult({
1054
+ options: input.options,
1055
+ payload,
1056
+ diagnostics: presentation.diagnostics,
1057
+ noScoreMessage
1058
+ });
1059
+ return result;
1060
+ }
1061
+ yield* printFullInspectResult({
1062
+ ...input,
1063
+ ...presentation,
1064
+ result,
1065
+ noScoreMessage
1066
+ });
1067
+ return result;
999
1068
  });
1069
+ //#endregion
1070
+ //#region src/cli/utils/resolve-worker-telemetry.ts
1000
1071
  /**
1001
- * Stamps the wide-event attributes onto the run's root span. A guarded no-op
1002
- * when tracing is off (no `rootSpan`) and swallow-on-throw, so telemetry can
1003
- * never break the run.
1072
+ * Projects the resolved lint worker count into the `(workerCount, parallel)`
1073
+ * telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
1074
+ * out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
1075
+ * `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
1076
+ * count (the pre-scan failure path, or a cache entry persisted before the
1077
+ * resolved count was tracked). `parallel` is derived from the count — NOT from
1078
+ * whether a count was pinned — so the common auto path (no pin) still reports
1079
+ * parallelism correctly instead of always reading `false`.
1004
1080
  */
1005
- const recordRunEvent = (rootSpan, input) => {
1006
- if (!rootSpan) return;
1007
- try {
1008
- for (const [key, value] of Object.entries(buildRunEventAttributes(input))) rootSpan.attribute(key, value);
1009
- } catch {}
1081
+ const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
1082
+ const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
1083
+ return {
1084
+ workerCount,
1085
+ parallel: workerCount !== void 0 && workerCount > 1
1086
+ };
1010
1087
  };
1011
1088
  //#endregion
1012
- //#region src/cli/utils/diagnostic-intersects-line-ranges.ts
1013
- const diagnosticIntersectsLineRanges = (diagnostic, lineRanges) => {
1014
- const diagnosticEndLine = Math.max(diagnostic.line, diagnostic.endLine ?? diagnostic.line);
1015
- return lineRanges.some(([rangeStartLine, rangeEndLine]) => diagnostic.line <= rangeEndLine && diagnosticEndLine >= rangeStartLine);
1089
+ //#region src/cli/utils/render-and-record-scan.ts
1090
+ const silentConsole$2 = makeNoopConsole();
1091
+ const deriveScope = (options) => {
1092
+ if (options.baseline) return "changed";
1093
+ if (options.changedLineRanges !== null) return "lines";
1094
+ return options.includePaths.length > 0 ? "files" : "full";
1016
1095
  };
1017
- //#endregion
1018
- //#region src/cli/utils/filter-diagnostics-by-changed-lines.ts
1019
- const filterDiagnosticsByChangedLines = (input) => {
1020
- const rangesByFile = /* @__PURE__ */ new Map();
1021
- for (const entry of input.changedLineRanges) rangesByFile.set(toForwardSlashes(entry.file), entry.ranges);
1022
- return input.diagnostics.filter((diagnostic) => {
1023
- const relativePath = toForwardSlashes(path.isAbsolute(diagnostic.filePath) ? path.relative(input.directory, diagnostic.filePath) : diagnostic.filePath);
1024
- const ranges = rangesByFile.get(relativePath);
1025
- return ranges !== void 0 && diagnosticIntersectsLineRanges(diagnostic, ranges);
1096
+ const buildRunEventConfig = (options, userConfig, resolvedWorkerCount) => {
1097
+ const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
1098
+ return {
1099
+ scope: deriveScope(options),
1100
+ parallel,
1101
+ workerCount,
1102
+ maxDurationMs: options.maxDurationMs,
1103
+ lint: options.lint,
1104
+ deadCode: options.deadCode,
1105
+ supplyChain: options.supplyChain,
1106
+ scoreOnly: options.scoreOnly,
1107
+ noScore: options.noScore,
1108
+ respectInlineDisables: options.respectInlineDisables,
1109
+ showWarnings: options.warnings,
1110
+ usedOutputDir: options.outputDirectory !== null,
1111
+ ignoredTagCount: options.ignoredTags.size,
1112
+ hasCustomConfig: userConfig !== null,
1113
+ userConfig
1114
+ };
1115
+ };
1116
+ const renderAndRecordScan = async (input) => {
1117
+ const cacheStats = {
1118
+ lintCacheHitFileCount: input.cacheStats?.lintCacheHitFileCount ?? null,
1119
+ lintCacheTotalFileCount: input.cacheStats?.lintCacheTotalFileCount ?? null,
1120
+ lintSidecarReplayedFileCount: input.cacheStats?.lintSidecarReplayedFileCount ?? null,
1121
+ lintSidecarTotalFileCount: input.cacheStats?.lintSidecarTotalFileCount ?? null,
1122
+ deadCodeCacheHit: input.cacheStats?.deadCodeCacheHit ?? null,
1123
+ deadCodeSummaryCacheHits: input.cacheStats?.deadCodeSummaryCacheHits ?? null,
1124
+ deadCodeSummaryCacheMisses: input.cacheStats?.deadCodeSummaryCacheMisses ?? null
1125
+ };
1126
+ const finalizeEffect = finalizeInspectResult({
1127
+ options: input.options,
1128
+ elapsedMilliseconds: performance.now() - input.startTime,
1129
+ payload: input.payload,
1130
+ cacheStats
1131
+ });
1132
+ const result = await runPromise(input.options.silent ? finalizeEffect.pipe(provideService(Console, silentConsole$2)) : finalizeEffect);
1133
+ const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
1134
+ recordScanMetrics({
1135
+ result,
1136
+ mode: input.scanMode,
1137
+ baselineDegraded: input.baselineDegraded,
1138
+ parallel,
1139
+ workerCount: resolvedWorkerCount,
1140
+ lint: input.options.lint,
1141
+ deadCode: input.options.deadCode,
1142
+ scoreOnly: input.options.scoreOnly,
1143
+ noScore: input.options.noScore,
1144
+ didLintFail: input.payload.didLintFail,
1145
+ lintFailureReasonKind: input.payload.lintFailureReasonKind,
1146
+ didDeadCodeFail: input.payload.didDeadCodeFail,
1147
+ userConfig: input.payload.userConfig,
1148
+ suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
1149
+ });
1150
+ recordRunEvent(input.rootSpan, {
1151
+ ...buildRunEventConfig(input.options, input.payload.userConfig, resolvedWorkerCount),
1152
+ result,
1153
+ mode: input.scanMode,
1154
+ gateExempt: input.baselineDegraded,
1155
+ wholeRepoCacheHit: input.wholeRepoCacheHit,
1156
+ didLintFail: input.payload.didLintFail,
1157
+ lintFailureReasonKind: input.payload.lintFailureReasonKind,
1158
+ lintPartialFailureCount: input.payload.lintPartialFailures.length,
1159
+ lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
1160
+ lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
1161
+ didDeadCodeFail: input.payload.didDeadCodeFail,
1162
+ supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
1163
+ securityScanFailed: input.payload.securityScanFailed,
1164
+ suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
1026
1165
  });
1166
+ return result;
1027
1167
  };
1028
1168
  //#endregion
1029
- //#region src/cli/utils/resolve-invocation-oxlint-concurrency.ts
1030
- /**
1031
- * The invocation-wide oxlint worker count: an explicit `--concurrency` pin
1032
- * when given, else the env-seeded / auto-budgeted `OxlintConcurrency`, clamped
1033
- * to the spawn ceiling. Every project of a workspace scan shares this one
1034
- * pool, so it also sizes how many projects may be in flight at once.
1035
- */
1036
- const resolveInvocationOxlintConcurrency = (requestedConcurrency) => resolveScanConcurrency(requestedConcurrency ?? runSync(OxlintConcurrency));
1037
- //#endregion
1038
- //#region src/cli/utils/resolve-oxlint-node.ts
1039
- const consoleWarn = (message) => warn(highlighter.warn(message));
1040
- const consoleBreak = log("");
1041
- const consoleDim = (message) => log(highlighter.gray(message));
1042
- const consoleSuccess = (message) => log(highlighter.success(message));
1043
- const promptShouldInstallNode = () => promise(async () => {
1044
- const { shouldInstallNode } = await prompts({
1045
- type: "confirm",
1046
- name: "shouldInstallNode",
1047
- message: `Install Node 24 via nvm to enable lint checks?`,
1048
- initial: true
1049
- });
1050
- return Boolean(shouldInstallNode);
1051
- });
1052
- const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => gen(function* () {
1053
- if (!isLintEnabled) return null;
1054
- const resolver = yield* NodeResolver;
1055
- const initial = yield* resolver.resolve();
1056
- if (initial !== null) {
1057
- if (!initial.isCurrentNode && !isQuiet) {
1058
- yield* consoleWarn(`Node ${process.version} is unsupported by oxlint. Using Node ${initial.version} from nvm.`);
1059
- yield* consoleBreak;
1060
- }
1061
- return initial.binaryPath;
1062
- }
1063
- if (isQuiet) return null;
1064
- yield* consoleWarn(`Node ${process.version} is not compatible with oxlint (requires ${OXLINT_NODE_REQUIREMENT}). Lint checks will be skipped.`);
1065
- const isNvmInstalled = yield* resolver.isNvmInstalled();
1066
- if (isNvmInstalled && process.stdin.isTTY) {
1067
- if (yield* promptShouldInstallNode()) {
1068
- yield* consoleBreak;
1069
- const fresh = (yield* resolver.installViaNvm()) ? yield* resolver.resolve() : null;
1070
- if (fresh) {
1071
- yield* consoleBreak;
1072
- yield* consoleSuccess(`Node ${fresh.version} installed. Using it for lint checks.`);
1073
- yield* consoleBreak;
1074
- return fresh.binaryPath;
1075
- }
1076
- yield* consoleBreak;
1077
- yield* consoleWarn("Failed to install Node via nvm. Skipping lint checks.");
1078
- yield* consoleBreak;
1079
- return null;
1169
+ //#region src/cli/utils/copy-unchanged-baseline-sources.ts
1170
+ const copyUnchangedBaselineSources = async (input) => {
1171
+ const baseMaterializedFiles = new Set(input.baseMaterializedFiles.map(toForwardSlashes));
1172
+ const headChangedFiles = new Set(input.headChangedFiles.map(toForwardSlashes));
1173
+ const untrackedFiles = new Set(input.untrackedFiles.map(toForwardSlashes));
1174
+ const sourceDirectory = path$1.resolve(input.directory);
1175
+ const targetDirectory = path$1.resolve(input.tempDirectory);
1176
+ return (await mapWithConcurrency(input.sourceFiles.filter((filePath) => {
1177
+ const normalizedPath = toForwardSlashes(filePath);
1178
+ return !baseMaterializedFiles.has(normalizedPath) && !headChangedFiles.has(normalizedPath) && !untrackedFiles.has(normalizedPath);
1179
+ }), 32, async (filePath) => {
1180
+ input.signal?.throwIfAborted();
1181
+ if (input.deadlineEpochMs !== null && Date.now() >= input.deadlineEpochMs) return false;
1182
+ const sourcePath = path$1.resolve(sourceDirectory, filePath);
1183
+ const targetPath = path$1.resolve(targetDirectory, filePath);
1184
+ if (!isPathInsideDirectory(sourcePath, sourceDirectory) || !isPathInsideDirectory(targetPath, targetDirectory)) return false;
1185
+ try {
1186
+ if (!(await fs$1.lstat(sourcePath)).isFile()) return false;
1187
+ await fs$1.mkdir(path$1.dirname(targetPath), { recursive: true });
1188
+ await fs$1.copyFile(sourcePath, targetPath);
1189
+ return true;
1190
+ } catch {
1191
+ input.signal?.throwIfAborted();
1192
+ return false;
1080
1193
  }
1081
- } else if (isNvmInstalled) yield* consoleDim(` Run: nvm install 24`);
1082
- else yield* consoleDim(` Install nvm (https://github.com/nvm-sh/nvm) and run: nvm install 24`);
1083
- yield* consoleBreak;
1084
- return null;
1085
- });
1086
- const resolveOxlintNode = (isLintEnabled, isQuiet) => runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(provide(NodeResolver.layerNode)));
1194
+ })).every(Boolean);
1195
+ };
1087
1196
  //#endregion
1088
- //#region src/cli/utils/resolve-cli-categories.ts
1089
- const categoryByLowercase = new Map(DIAGNOSTIC_CATEGORY_BUCKETS.map((category) => [category.toLowerCase(), category]));
1090
- const knownCategoryList = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
1091
- const splitCategoryFlagValue = (value) => value.split(",").map((category) => category.trim()).filter((category) => category.length > 0);
1092
- const resolveCliCategories = (categoryFlag) => {
1093
- const rawCategoryValues = categoryFlag === void 0 ? [] : Array.isArray(categoryFlag) ? categoryFlag : [categoryFlag];
1094
- const resolvedCategories = [];
1095
- const seenCategories = /* @__PURE__ */ new Set();
1096
- for (const rawCategoryValue of rawCategoryValues) for (const categoryQuery of splitCategoryFlagValue(rawCategoryValue)) {
1097
- const matchedCategory = categoryByLowercase.get(categoryQuery.toLowerCase());
1098
- if (matchedCategory === void 0) throw new CliInputError(`Unknown category "${categoryQuery}". Expected one of: ${knownCategoryList}.`);
1099
- if (!seenCategories.has(matchedCategory)) {
1100
- seenCategories.add(matchedCategory);
1101
- resolvedCategories.push(matchedCategory);
1197
+ //#region src/cli/utils/read-diagnostic-evidence.ts
1198
+ const SOURCE_EXTENSIONS = [
1199
+ ".ts",
1200
+ ".tsx",
1201
+ ".js",
1202
+ ".jsx",
1203
+ ".mts",
1204
+ ".cts",
1205
+ ".mjs",
1206
+ ".cjs"
1207
+ ];
1208
+ const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
1209
+ const getScriptKind = (filePath) => {
1210
+ const extension = path.extname(filePath).toLowerCase();
1211
+ if (extension === ".tsx") return typescriptBinding.ScriptKind.TSX;
1212
+ if (extension === ".jsx") return typescriptBinding.ScriptKind.JSX;
1213
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return typescriptBinding.ScriptKind.JS;
1214
+ return typescriptBinding.ScriptKind.TS;
1215
+ };
1216
+ const resolveSafeSourcePath = (rootDirectory, filePath) => {
1217
+ try {
1218
+ const resolvedRootDirectory = fs.realpathSync(rootDirectory);
1219
+ const absolutePath = path.resolve(resolvedRootDirectory, filePath);
1220
+ const relativePath = path.relative(resolvedRootDirectory, absolutePath);
1221
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
1222
+ const stats = fs.lstatSync(absolutePath);
1223
+ if (!stats.isFile() || stats.isSymbolicLink()) return null;
1224
+ const realPath = fs.realpathSync(absolutePath);
1225
+ const realRelativePath = path.relative(resolvedRootDirectory, realPath);
1226
+ return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
1227
+ } catch {
1228
+ return null;
1229
+ }
1230
+ };
1231
+ const readSourceRecord = (rootDirectory, filePath, state) => {
1232
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
1233
+ if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
1234
+ const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
1235
+ if (absolutePath === null) {
1236
+ state.recordByFilePath.set(normalizedFilePath, null);
1237
+ return null;
1238
+ }
1239
+ try {
1240
+ const sourceText = fs.readFileSync(absolutePath, "utf-8");
1241
+ const record = {
1242
+ absolutePath,
1243
+ filePath: normalizedFilePath,
1244
+ sourceFile: typescriptBinding.createSourceFile(absolutePath, sourceText, typescriptBinding.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
1245
+ sourceText
1246
+ };
1247
+ state.recordByFilePath.set(normalizedFilePath, record);
1248
+ return record;
1249
+ } catch {
1250
+ state.recordByFilePath.set(normalizedFilePath, null);
1251
+ return null;
1252
+ }
1253
+ };
1254
+ const listSourceRecords = (rootDirectory, state) => {
1255
+ if (state.sourceRecords !== null) return state.sourceRecords;
1256
+ state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
1257
+ const record = readSourceRecord(rootDirectory, filePath, state);
1258
+ return record === null ? [] : [record];
1259
+ });
1260
+ return state.sourceRecords;
1261
+ };
1262
+ const unwrapExpression = (expression) => {
1263
+ let currentExpression = expression;
1264
+ while (typescriptBinding.isParenthesizedExpression(currentExpression) || typescriptBinding.isAsExpression(currentExpression) || typescriptBinding.isTypeAssertionExpression(currentExpression) || typescriptBinding.isNonNullExpression(currentExpression) || typescriptBinding.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
1265
+ return currentExpression;
1266
+ };
1267
+ const getCalleeName = (expression) => {
1268
+ const unwrappedExpression = unwrapExpression(expression);
1269
+ if (typescriptBinding.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
1270
+ if (typescriptBinding.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
1271
+ return null;
1272
+ };
1273
+ const isComponentWrapperCall = (node) => {
1274
+ const wrapperName = getCalleeName(node.expression);
1275
+ return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
1276
+ };
1277
+ const getFunctionName = (node) => {
1278
+ if (typescriptBinding.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
1279
+ if (typescriptBinding.isArrowFunction(node) || typescriptBinding.isFunctionExpression(node)) {
1280
+ let componentExpression = node;
1281
+ while (true) {
1282
+ const parent = componentExpression.parent;
1283
+ if (typescriptBinding.isParenthesizedExpression(parent) || typescriptBinding.isAsExpression(parent) || typescriptBinding.isTypeAssertionExpression(parent) || typescriptBinding.isNonNullExpression(parent) || typescriptBinding.isSatisfiesExpression(parent)) {
1284
+ componentExpression = parent;
1285
+ continue;
1286
+ }
1287
+ if (typescriptBinding.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
1288
+ componentExpression = parent;
1289
+ continue;
1290
+ }
1291
+ if (typescriptBinding.isVariableDeclaration(parent) && typescriptBinding.isIdentifier(parent.name)) return parent.name.text;
1292
+ return node.name?.text ?? null;
1102
1293
  }
1103
1294
  }
1104
- if (rawCategoryValues.length > 0 && resolvedCategories.length === 0) throw new CliInputError(`--category requires one of: ${knownCategoryList}.`);
1105
- return resolvedCategories.length > 0 ? resolvedCategories : void 0;
1295
+ return null;
1106
1296
  };
1107
- //#endregion
1108
- //#region src/cli/utils/resolve-inspect-options.ts
1109
- const resolveIgnoredTags = (userConfig, includedTags) => {
1110
- const ignoredTags = new Set(userConfig?.ignore?.tags ?? []);
1111
- for (const includedTag of includedTags) ignoredTags.delete(includedTag);
1112
- return ignoredTags;
1297
+ const isComponentFunction = (node) => typescriptBinding.isFunctionDeclaration(node) || typescriptBinding.isFunctionExpression(node) || typescriptBinding.isArrowFunction(node);
1298
+ const isDefaultExportedComponent = (record, node, componentName) => {
1299
+ if (typescriptBinding.canHaveModifiers(node) && typescriptBinding.getModifiers(node)?.some((modifier) => modifier.kind === typescriptBinding.SyntaxKind.DefaultKeyword)) return true;
1300
+ return record.sourceFile.statements.some((statement) => typescriptBinding.isExportAssignment(statement) && typescriptBinding.isIdentifier(statement.expression) && statement.expression.text === componentName);
1113
1301
  };
1114
- const resolveInspectOptions = (inputOptions, userConfig) => {
1115
- const includedTags = inputOptions.includedTags ?? /* @__PURE__ */ new Set();
1116
- const hasIncludedTags = includedTags.size > 0;
1117
- return {
1118
- lint: inputOptions.lint ?? userConfig?.lint ?? true,
1119
- deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,
1120
- supplyChain: inputOptions.supplyChain ?? userConfig?.supplyChain?.enabled ?? true,
1121
- verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
1122
- outputDirectory: inputOptions.outputDirectory || null,
1123
- scoreOnly: inputOptions.scoreOnly ?? false,
1124
- noScore: inputOptions.noScore ?? userConfig?.noScore ?? false,
1125
- isCi: inputOptions.isCi ?? false,
1126
- isCiOrCodingAgentEnvironment: isCiOrCodingAgentEnvironment(),
1127
- isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
1128
- silent: inputOptions.silent ?? false,
1129
- includePaths: inputOptions.includePaths ?? [],
1130
- customRulesOnly: hasIncludedTags ? false : userConfig?.customRulesOnly ?? false,
1131
- share: userConfig?.share ?? true,
1132
- respectInlineDisables: inputOptions.respectInlineDisables ?? userConfig?.respectInlineDisables ?? true,
1133
- warnings: inputOptions.warnings ?? userConfig?.warnings ?? true,
1134
- categoryFilters: new Set(resolveCliCategories(inputOptions.categoryFilters) ?? []),
1135
- adoptExistingLintConfig: hasIncludedTags ? false : userConfig?.adoptExistingLintConfig ?? true,
1136
- ignoredTags: resolveIgnoredTags(userConfig, includedTags),
1137
- includedTags,
1138
- includeTagDefaults: inputOptions.includeTagDefaults ?? false,
1139
- scoreDisabledMessage: inputOptions.scoreDisabledMessage,
1140
- outputSurface: inputOptions.outputSurface ?? "cli",
1141
- suppressRendering: (inputOptions.suppressRendering ?? false) || inputOptions.uiLayers != null,
1142
- uiLayers: inputOptions.uiLayers ?? null,
1143
- concurrentScan: inputOptions.concurrentScan ?? false,
1144
- concurrency: inputOptions.concurrency,
1145
- maxDurationMs: inputOptions.maxDurationMs ?? null,
1146
- baseline: inputOptions.baseline ?? null,
1147
- changedLineRanges: inputOptions.changedLineRanges ?? null,
1148
- supplyChainManifestChanged: inputOptions.supplyChainManifestChanged ?? false,
1149
- excludedProjectDirectories: inputOptions.excludedProjectDirectories ?? [],
1150
- retainExcludedProjectDeadCodeDiagnostics: inputOptions.retainExcludedProjectDeadCodeDiagnostics ?? false,
1151
- precomputedSourceFileCount: inputOptions.precomputedSourceFileCount,
1152
- precomputedSourceFiles: inputOptions.precomputedSourceFiles
1302
+ const findEnclosingComponent = (record, diagnostic) => {
1303
+ if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
1304
+ const lineIndex = diagnostic.line - 1;
1305
+ const lineStarts = record.sourceFile.getLineStarts();
1306
+ const lineStart = lineStarts[lineIndex];
1307
+ if (lineStart === void 0) return null;
1308
+ const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
1309
+ const lineEnd = Math.max(lineStart, nextLineStart - 1);
1310
+ const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
1311
+ const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
1312
+ let component = null;
1313
+ let componentWidth = Number.POSITIVE_INFINITY;
1314
+ const visit = (node) => {
1315
+ if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
1316
+ if (isComponentFunction(node)) {
1317
+ const name = getFunctionName(node);
1318
+ const width = node.end - node.getStart(record.sourceFile);
1319
+ if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
1320
+ component = {
1321
+ isDefaultExport: isDefaultExportedComponent(record, node, name),
1322
+ name,
1323
+ node,
1324
+ record
1325
+ };
1326
+ componentWidth = width;
1327
+ }
1328
+ }
1329
+ typescriptBinding.forEachChild(node, visit);
1153
1330
  };
1331
+ visit(record.sourceFile);
1332
+ return component;
1154
1333
  };
1155
- //#endregion
1156
- //#region src/cli/utils/sum-file-counts-matching.ts
1157
- const sumFileCountsMatching = (messages, countPattern) => messages.reduce((total, message) => {
1158
- const match = countPattern.exec(message);
1159
- return match ? total + Number(match[1]) : total;
1160
- }, 0);
1161
- //#endregion
1162
- //#region src/cli/utils/count-deadline-skipped-files.ts
1163
- const DEADLINE_SKIPPED_MESSAGE_PATTERN = /^(\d+) file\(s\) skipped — max scan duration reached/;
1164
- const countDeadlineSkippedFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DEADLINE_SKIPPED_MESSAGE_PATTERN);
1165
- //#endregion
1166
- //#region src/cli/utils/count-dropped-lint-files.ts
1167
- const DROPPED_FILES_MESSAGE_PATTERN = /^(\d+) file\(s\) failed to lint and were skipped/;
1168
- const countDroppedLintFiles = (lintPartialFailures) => sumFileCountsMatching(lintPartialFailures, DROPPED_FILES_MESSAGE_PATTERN);
1169
- //#endregion
1170
- //#region src/cli/utils/build-empty-report-message.ts
1171
- const buildEmptyReportMessage = (input) => {
1172
- const categoryFilters = [...input.categoryFilters];
1173
- if (categoryFilters.length > 0) return `No issues found in category ${categoryFilters.join(", ")}!`;
1174
- if (input.demotedDiagnosticCount > 0) return `No issues found! (${input.demotedDiagnosticCount} demoted from the ${input.outputSurface} surface — see config.surfaces.)`;
1175
- return "No issues found!";
1176
- };
1177
- //#endregion
1178
- //#region src/cli/utils/build-inspect-result.ts
1179
- const buildInspectResult = (input) => {
1180
- const { payload, cacheStats } = input;
1181
- const { skippedChecks, skippedCheckReasons } = buildSkippedChecks({
1182
- didLintFail: payload.didLintFail,
1183
- lintFailureReason: payload.lintFailureReason,
1184
- lintPartialFailures: payload.lintPartialFailures,
1185
- didDeadCodeFail: payload.didDeadCodeFail,
1186
- deadCodeFailureReason: payload.deadCodeFailureReason,
1187
- supplyChainOverlapTimedOut: payload.supplyChainOverlapTimedOut,
1188
- securityScanFailed: payload.securityScanFailed ?? false,
1189
- securityScanFailureReason: payload.securityScanFailureReason ?? null
1190
- });
1191
- return {
1192
- diagnostics: [...payload.diagnostics],
1193
- score: payload.score,
1194
- skippedChecks,
1195
- ...Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {},
1196
- project: payload.project,
1197
- elapsedMilliseconds: input.elapsedMilliseconds,
1198
- scannedFileCount: payload.scannedFileCount,
1199
- scannedFilePaths: payload.scannedFilePaths,
1200
- analyzedFiles: payload.analyzedFiles ?? [],
1201
- scanElapsedMilliseconds: payload.scanElapsedMilliseconds,
1202
- ...cacheStats.lintCacheTotalFileCount === null ? {} : {
1203
- lintCacheHitFileCount: cacheStats.lintCacheHitFileCount,
1204
- lintCacheTotalFileCount: cacheStats.lintCacheTotalFileCount
1205
- },
1206
- ...cacheStats.lintSidecarTotalFileCount === null ? {} : {
1207
- lintSidecarReplayedFileCount: cacheStats.lintSidecarReplayedFileCount,
1208
- lintSidecarTotalFileCount: cacheStats.lintSidecarTotalFileCount
1209
- },
1210
- ...cacheStats.deadCodeCacheHit === null ? {} : { deadCodeCacheHit: cacheStats.deadCodeCacheHit },
1211
- ...cacheStats.deadCodeSummaryCacheHits === null || cacheStats.deadCodeSummaryCacheMisses === null ? {} : {
1212
- deadCodeSummaryCacheHits: cacheStats.deadCodeSummaryCacheHits,
1213
- deadCodeSummaryCacheMisses: cacheStats.deadCodeSummaryCacheMisses
1214
- },
1215
- ...payload.baselineDelta ? { baselineDelta: payload.baselineDelta } : {}
1216
- };
1334
+ const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
1335
+ if (!moduleSpecifier.startsWith(".")) return false;
1336
+ const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
1337
+ const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
1338
+ const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
1339
+ return [
1340
+ unresolvedPath,
1341
+ ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
1342
+ ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
1343
+ ].includes(componentRecord.absolutePath);
1217
1344
  };
1218
- //#endregion
1219
- //#region src/cli/utils/build-no-score-message.ts
1220
- const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
1221
- const buildNoScoreMessage = (input) => {
1222
- const unavailableReason = resolveScoreUnavailableReason({
1223
- isScoreDisabled: input.isScoreDisabled,
1224
- isAnalysisIncomplete: input.isAnalysisIncomplete ?? false
1225
- });
1226
- let reason;
1227
- switch (unavailableReason) {
1228
- case "disabled":
1229
- reason = input.disabledMessage ?? "Score disabled by --no-score.";
1230
- break;
1231
- case "analysis-incomplete":
1232
- reason = "Score not shown because lint or maintainability analysis could not complete.";
1233
- break;
1234
- case "api-unavailable":
1235
- reason = "Score unavailable (could not reach the score API).";
1236
- break;
1345
+ const getComponentTagNames = (record, component) => {
1346
+ if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
1347
+ const tagNames = /* @__PURE__ */ new Set();
1348
+ for (const statement of record.sourceFile.statements) {
1349
+ if (!typescriptBinding.isImportDeclaration(statement) || !typescriptBinding.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
1350
+ const importClause = statement.importClause;
1351
+ if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
1352
+ const namedBindings = importClause?.namedBindings;
1353
+ if (namedBindings !== void 0 && typescriptBinding.isNamedImports(namedBindings)) {
1354
+ for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
1355
+ }
1237
1356
  }
1238
- return `${reason} ${ENTERPRISE_CONTACT_HINT}`;
1357
+ return tagNames;
1239
1358
  };
1240
- //#endregion
1241
- //#region src/cli/utils/filter-diagnostics-by-categories.ts
1242
- const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
1243
- //#endregion
1244
- //#region src/cli/utils/has-incomplete-score-analysis.ts
1245
- const hasIncompleteScoreAnalysis = (skippedChecks) => skippedChecks.includes("lint") || skippedChecks.includes("dead-code");
1246
- //#endregion
1247
- //#region src/cli/utils/format-diagnostic-site.ts
1248
- const formatDiagnosticSite = (site) => site.line > 0 ? `${site.filePath}:${site.line}` : site.filePath;
1249
- //#endregion
1250
- //#region src/cli/utils/format-rule-summary.ts
1251
- const formatFileContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
1252
- const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
1253
- const firstDiagnostic = ruleDiagnostics[0];
1254
- if (firstDiagnostic === void 0) return "";
1255
- const distinctMessages = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.message))];
1256
- const sections = [
1257
- `Rule: ${ruleKey}`,
1258
- `Severity: ${firstDiagnostic.severity}`,
1259
- `Category: ${firstDiagnostic.category}`,
1260
- `Count: ${ruleDiagnostics.length}`,
1261
- "",
1262
- distinctMessages.join("\n\n")
1263
- ];
1264
- if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
1265
- if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
1266
- const fixRecipeLine = formatFixRecipeLine(firstDiagnostic);
1267
- if (fixRecipeLine) sections.push("", fixRecipeLine);
1268
- sections.push("", "Files:");
1269
- for (const diagnostic of ruleDiagnostics) {
1270
- sections.push(` ${formatDiagnosticSite(diagnostic)}${formatFileContextTag(diagnostic)}`);
1271
- if (diagnostic.suppressionHint) sections.push(` ${diagnostic.suppressionHint}`);
1359
+ const getTransparentCall = (functionNode) => {
1360
+ if (functionNode.body === void 0) return null;
1361
+ if (!typescriptBinding.isBlock(functionNode.body)) {
1362
+ const expression = unwrapExpression(functionNode.body);
1363
+ return typescriptBinding.isCallExpression(expression) ? expression : null;
1272
1364
  }
1273
- return `${sections.join("\n")}\n`;
1274
- };
1275
- //#endregion
1276
- //#region src/cli/utils/write-diagnostics-directory.ts
1277
- const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
1278
- const readPreviousRuleDumpFileNames = (directory) => {
1279
- try {
1280
- const previous = JSON.parse(fs$2.readFileSync(path$1.join(directory, "diagnostics.json"), "utf8"));
1281
- return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
1282
- } catch {
1283
- return /* @__PURE__ */ new Set();
1365
+ if (functionNode.body.statements.length !== 1) return null;
1366
+ const statement = functionNode.body.statements[0];
1367
+ if (statement === void 0) return null;
1368
+ const expression = typescriptBinding.isExpressionStatement(statement) || typescriptBinding.isReturnStatement(statement) ? statement.expression : void 0;
1369
+ if (expression === void 0) return null;
1370
+ const unwrappedExpression = unwrapExpression(expression);
1371
+ return typescriptBinding.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
1372
+ };
1373
+ const resolveTransparentTarget = (functionNode) => {
1374
+ const call = getTransparentCall(functionNode);
1375
+ if (call === null) return null;
1376
+ const target = unwrapExpression(call.expression);
1377
+ if (!typescriptBinding.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
1378
+ for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
1379
+ const argument = call.arguments[parameterIndex];
1380
+ const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
1381
+ if (!typescriptBinding.isIdentifier(parameter.name) || unwrappedArgument === null || !typescriptBinding.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
1284
1382
  }
1383
+ return target.text;
1285
1384
  };
1286
- const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
1287
- const directory = outputDirectory ? path$1.resolve(outputDirectory) : path$1.join(tmpdir(), `react-doctor-${randomUUID()}`);
1288
- if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) fs$2.rmSync(path$1.join(directory, fileName), { force: true });
1289
- fs$2.mkdirSync(directory, { recursive: true });
1290
- for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) fs$2.writeFileSync(path$1.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
1291
- fs$2.writeFileSync(path$1.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
1292
- return directory;
1385
+ const getForwardingFunction = (declaration) => {
1386
+ if (typescriptBinding.isFunctionDeclaration(declaration)) return declaration;
1387
+ if (!typescriptBinding.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
1388
+ const initializer = unwrapExpression(declaration.initializer);
1389
+ if (typescriptBinding.isArrowFunction(initializer) || typescriptBinding.isFunctionExpression(initializer)) return initializer;
1390
+ if (typescriptBinding.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
1391
+ const callback = initializer.arguments[0];
1392
+ if (callback !== void 0 && (typescriptBinding.isArrowFunction(callback) || typescriptBinding.isFunctionExpression(callback))) return callback;
1393
+ }
1394
+ return null;
1293
1395
  };
1294
- //#endregion
1295
- //#region src/cli/utils/print-diagnostics-dump.ts
1296
- const printDiagnosticsDump = (diagnostics, outputDirectory, verbose, stream = "stdout") => gen(function* () {
1297
- const writtenDirectory = yield* try_({
1298
- try: () => writeDiagnosticsDirectory(diagnostics, outputDirectory),
1299
- catch: (cause) => cause
1300
- }).pipe(orElseSucceed(() => null));
1301
- if (writtenDirectory === null || !verbose && !outputDirectory) return;
1302
- const pathLine = highlighter.gray(` Full diagnostics written to ${writtenDirectory}`);
1303
- yield* stream === "stderr" || Boolean(outputDirectory) && isJsonModeActive() ? sync(() => process.stderr.write(`${pathLine}\n`)) : log(pathLine);
1304
- });
1305
- //#endregion
1306
- //#region src/cli/utils/collect-affected-files.ts
1307
- const collectAffectedFiles = (diagnostics) => new Set(diagnostics.map((diagnostic) => diagnostic.filePath));
1308
- //#endregion
1309
- //#region src/cli/utils/print-footer.ts
1310
- const buildShareUrl = (input) => {
1311
- const errorCount = input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1312
- const warningCount = input.diagnostics.length - errorCount;
1313
- const affectedFileCount = collectAffectedFiles(input.diagnostics).size;
1314
- const parameters = new URLSearchParams({ p: input.projectName });
1315
- if (input.scoreResult) parameters.set("s", String(input.scoreResult.score));
1316
- if (errorCount > 0) parameters.set("e", String(errorCount));
1317
- if (warningCount > 0) parameters.set("w", String(warningCount));
1318
- if (affectedFileCount > 0) parameters.set("f", String(affectedFileCount));
1319
- return `${SHARE_BASE_URL}?${parameters.toString()}`;
1396
+ const getDeclaredBindingName = (node) => {
1397
+ if ((typescriptBinding.isVariableDeclaration(node) || typescriptBinding.isParameter(node) || typescriptBinding.isBindingElement(node) || typescriptBinding.isFunctionDeclaration(node)) && node.name !== void 0 && typescriptBinding.isIdentifier(node.name)) return node.name.text;
1398
+ if (typescriptBinding.isImportClause(node)) return node.name?.text ?? null;
1399
+ if (typescriptBinding.isImportSpecifier(node) || typescriptBinding.isNamespaceImport(node)) return node.name.text;
1400
+ return null;
1320
1401
  };
1321
- const printFooter = (input) => gen(function* () {
1322
- yield* log("");
1323
- yield* log(buildSectionDivider());
1324
- yield* log("");
1325
- const shareUrl = input.isOffline ? null : buildShareUrl(input);
1326
- for (const line of buildFooterLinkLines({ shareUrl })) yield* log(line);
1327
- });
1328
- //#endregion
1329
- //#region src/cli/utils/format-elapsed-time.ts
1330
- const formatElapsedTime = (elapsedMilliseconds) => {
1331
- if (elapsedMilliseconds < 1e3) return `${Math.round(elapsedMilliseconds)}ms`;
1332
- return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
1402
+ const findBindingScope = (node) => {
1403
+ let currentNode = node;
1404
+ while (currentNode !== void 0) {
1405
+ if (typescriptBinding.isParameter(currentNode) && typescriptBinding.isFunctionLike(currentNode.parent)) return currentNode.parent;
1406
+ if (typescriptBinding.isBlock(currentNode) || typescriptBinding.isModuleBlock(currentNode) || typescriptBinding.isSourceFile(currentNode)) return currentNode;
1407
+ currentNode = currentNode.parent;
1408
+ }
1409
+ return node.getSourceFile();
1333
1410
  };
1334
- //#endregion
1335
- //#region src/cli/utils/format-skipped-check-label.ts
1336
- const formatSkippedCheckLabel = (skippedCheck) => skippedCheck === "dead-code" ? "maintainability" : skippedCheck;
1337
- //#endregion
1338
- //#region src/cli/utils/pluralize.ts
1339
- const pluralize = (count, singularNoun) => `${count} ${singularNoun}${count === 1 ? "" : "s"}`;
1340
- //#endregion
1341
- //#region src/cli/utils/print-headless-report.ts
1342
- const formatContextTag = (diagnostic) => diagnostic.fileContext ? ` (${diagnostic.fileContext} file)` : "";
1343
- const printHeadlessReport = (input) => gen(function* () {
1344
- yield* log(`${highlighter.success("✔")} Scanned ${pluralize(input.scannedFileCount, "file")} in ${formatElapsedTime(input.elapsedMilliseconds)}`);
1345
- yield* log("");
1346
- yield* log(highlighter.bold(`React Doctor — ${input.projectName}`));
1347
- if (input.scoreResult) yield* log(`Score: ${input.scoreResult.score} / 100 ${input.scoreResult.label}`);
1348
- else yield* log(highlighter.gray(input.noScoreMessage));
1349
- if (input.diagnostics.length === 0) {
1350
- yield* log("");
1351
- yield* log(highlighter.success(`✔ ${input.emptyStateMessage}`));
1352
- } else {
1353
- yield* log("");
1354
- yield* log(highlighter.bold(pluralize(input.diagnostics.length, "issue")));
1355
- const categoryCounts = /* @__PURE__ */ new Map();
1356
- for (const diagnostic of input.diagnostics) {
1357
- const counts = categoryCounts.get(diagnostic.category) ?? {
1358
- error: 0,
1359
- warning: 0
1360
- };
1361
- categoryCounts.set(diagnostic.category, {
1362
- error: counts.error + Number(diagnostic.severity === "error"),
1363
- warning: counts.warning + Number(diagnostic.severity === "warning")
1411
+ const findBindingDeclaration = (record, binding) => {
1412
+ const usagePosition = binding.getStart(record.sourceFile);
1413
+ const candidates = [];
1414
+ const visit = (node) => {
1415
+ if (getDeclaredBindingName(node) === binding.text) {
1416
+ const scope = findBindingScope(node);
1417
+ const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
1418
+ const isVisibleDeclaration = typescriptBinding.isFunctionDeclaration(node) || typescriptBinding.isParameter(node) || typescriptBinding.isBindingElement(node) || typescriptBinding.isImportClause(node) || typescriptBinding.isImportSpecifier(node) || typescriptBinding.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
1419
+ if (isVisibleScope && isVisibleDeclaration) candidates.push({
1420
+ declaration: node,
1421
+ scope
1364
1422
  });
1365
1423
  }
1366
- for (const [category, counts] of categoryCounts) {
1367
- const countParts = [];
1368
- if (counts.error > 0) countParts.push(pluralize(counts.error, "error"));
1369
- if (counts.warning > 0) countParts.push(pluralize(counts.warning, "warning"));
1370
- yield* log(`${category}: ${countParts.join(", ")}`);
1371
- }
1372
- yield* log("");
1373
- for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(input.diagnostics)) {
1374
- const representativeDiagnostic = ruleDiagnostics[0];
1375
- if (representativeDiagnostic === void 0) continue;
1376
- const severityIcon = representativeDiagnostic.severity === "error" ? "✖" : "⚠";
1377
- const colorize = representativeDiagnostic.severity === "error" ? highlighter.error : highlighter.warn;
1378
- const occurrenceSuffix = ruleDiagnostics.length > 1 ? ` ×${ruleDiagnostics.length}` : "";
1379
- yield* log(colorize(`${severityIcon} ${representativeDiagnostic.title ?? ruleKey}${occurrenceSuffix}`));
1380
- yield* log(highlighter.gray(` ${ruleKey}`));
1381
- for (const diagnostic of ruleDiagnostics) yield* log(` ${formatDiagnosticSite(diagnostic)}${formatContextTag(diagnostic)}`);
1382
- }
1424
+ typescriptBinding.forEachChild(node, visit);
1425
+ };
1426
+ visit(record.sourceFile);
1427
+ let nearestCandidate = null;
1428
+ let nearestScopeWidth = Number.POSITIVE_INFINITY;
1429
+ let isNearestScopeAmbiguous = false;
1430
+ for (const candidate of candidates) {
1431
+ const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
1432
+ if (scopeWidth < nearestScopeWidth) {
1433
+ nearestCandidate = candidate;
1434
+ nearestScopeWidth = scopeWidth;
1435
+ isNearestScopeAmbiguous = false;
1436
+ } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
1383
1437
  }
1384
- if (input.skippedChecks.length > 0) {
1385
- yield* log("");
1386
- yield* warn(highlighter.warn(`Results are incomplete: ${input.skippedChecks.map(formatSkippedCheckLabel).join(" and ")} checks failed.`));
1438
+ if (isNearestScopeAmbiguous) return null;
1439
+ const declaration = nearestCandidate?.declaration;
1440
+ return declaration !== void 0 && (typescriptBinding.isVariableDeclaration(declaration) || typescriptBinding.isFunctionDeclaration(declaration)) ? declaration : null;
1441
+ };
1442
+ const resolveHandlerBinding = (record, binding) => {
1443
+ const declaration = findBindingDeclaration(record, binding);
1444
+ if (declaration === null) return binding.text;
1445
+ const forwardingFunction = getForwardingFunction(declaration);
1446
+ const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
1447
+ return target === null || target === binding.text ? binding.text : target;
1448
+ };
1449
+ const getJsxTagName = (node) => typescriptBinding.isIdentifier(node.tagName) ? node.tagName.text : null;
1450
+ const getHandlerBinding = (node, propName) => {
1451
+ for (const property of node.attributes.properties) {
1452
+ if (!typescriptBinding.isJsxAttribute(property) || property.name.getText() !== propName) continue;
1453
+ const initializer = property.initializer;
1454
+ if (initializer === void 0 || !typescriptBinding.isJsxExpression(initializer)) return null;
1455
+ if (initializer.expression === void 0) return null;
1456
+ const expression = unwrapExpression(initializer.expression);
1457
+ return typescriptBinding.isIdentifier(expression) ? expression : null;
1387
1458
  }
1388
- });
1389
- //#endregion
1390
- //#region src/cli/utils/render-agent-guidance.ts
1391
- const AGENT_GUIDANCE_LINES = [
1392
- "Treat React Doctor diagnostics as starting hypotheses. Read the relevant code before confirming or suppressing each finding.",
1393
- "For each group, decide true positive, false positive, or needs-human-review, then assign high/medium/low confidence.",
1394
- "Do not suppress a finding without evidence from the file in question. Confidence requires code context.",
1395
- "Understand the root cause before editing. Fix the underlying code instead of changing react-doctor config or suppressing rules unless explicitly asked.",
1396
- "Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
1397
- "Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
1398
- "Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
1399
- "Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
1400
- "When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
1401
- "Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
1402
- "When one rule spans dozens of files (a migration-scale change), fix a representative sample first, confirm the recipe holds, and get the code owner's sign-off before changing the rest. Don't mass-fix a broad pattern in one unreviewed pass.",
1403
- "For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
1404
- "If a fix needs an API, UX, or architecture decision, stop and ask before editing."
1405
- ];
1406
- const printAgentGuidance = () => gen(function* () {
1407
- yield* log(`${highlighter.bold("Agent guidance")}`);
1408
- for (const line of AGENT_GUIDANCE_LINES) yield* log(highlighter.gray(` - ${line}`));
1409
- yield* log("");
1410
- });
1459
+ return null;
1460
+ };
1461
+ const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
1462
+ const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
1463
+ const cachedAliases = state.aliasesByComponent.get(cacheKey);
1464
+ if (cachedAliases !== void 0) return cachedAliases;
1465
+ const bindingsByPropName = /* @__PURE__ */ new Map();
1466
+ const unresolvedPropNames = /* @__PURE__ */ new Set();
1467
+ let callsiteCount = 0;
1468
+ for (const record of listSourceRecords(rootDirectory, state)) {
1469
+ const tagNames = getComponentTagNames(record, component);
1470
+ if (tagNames.size === 0) continue;
1471
+ const visit = (node) => {
1472
+ if (record === component.record && node === component.node) return;
1473
+ if (typescriptBinding.isJsxOpeningElement(node) || typescriptBinding.isJsxSelfClosingElement(node)) {
1474
+ const tagName = getJsxTagName(node);
1475
+ if (tagName !== null && tagNames.has(tagName)) {
1476
+ callsiteCount += 1;
1477
+ for (const propName of propNames) {
1478
+ const binding = getHandlerBinding(node, propName);
1479
+ if (binding === null) {
1480
+ unresolvedPropNames.add(propName);
1481
+ continue;
1482
+ }
1483
+ const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
1484
+ bindings.add(resolveHandlerBinding(record, binding));
1485
+ bindingsByPropName.set(propName, bindings);
1486
+ }
1487
+ }
1488
+ }
1489
+ typescriptBinding.forEachChild(node, visit);
1490
+ };
1491
+ visit(record.sourceFile);
1492
+ }
1493
+ const aliases = /* @__PURE__ */ new Map();
1494
+ if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
1495
+ if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
1496
+ const [bindingName] = bindings;
1497
+ if (bindingName !== void 0) aliases.set(propName, bindingName);
1498
+ }
1499
+ state.aliasesByComponent.set(cacheKey, aliases);
1500
+ return aliases;
1501
+ };
1502
+ const normalizeForwardedHandlers = (evidence, aliases) => {
1503
+ let normalizedEvidence = evidence;
1504
+ for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
1505
+ return normalizedEvidence;
1506
+ };
1507
+ const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
1508
+ const state = {
1509
+ aliasesByComponent: /* @__PURE__ */ new Map(),
1510
+ recordByFilePath: /* @__PURE__ */ new Map(),
1511
+ sourceRecords: null
1512
+ };
1513
+ return (diagnostic) => {
1514
+ const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
1515
+ if (record === null || !Number.isInteger(diagnostic.line)) return null;
1516
+ const sourceLines = record.sourceText.split(/\r?\n/);
1517
+ const startLineIndex = Math.max(0, diagnostic.line - 1);
1518
+ const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
1519
+ const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
1520
+ if (!options.resolveForwardedHandlers) return evidence;
1521
+ const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
1522
+ if (propNames.size === 0) return evidence;
1523
+ const component = findEnclosingComponent(record, diagnostic);
1524
+ if (component === null) return evidence;
1525
+ return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
1526
+ };
1527
+ };
1411
1528
  //#endregion
1412
- //#region src/cli/utils/finalize-inspect-result.ts
1413
- const buildInspectPresentation = ({ options, payload }) => {
1414
- const surfaceDiagnostics = filterDiagnosticsForSurface([...payload.diagnostics], options.outputSurface, payload.userConfig);
1415
- return {
1416
- diagnostics: filterDiagnosticsByCategories(surfaceDiagnostics, options.categoryFilters),
1417
- demotedDiagnosticCount: payload.diagnostics.length - surfaceDiagnostics.length
1529
+ //#region src/cli/utils/read-source-line.ts
1530
+ /**
1531
+ * Builds a cached `(filePath, line) -> source text` reader rooted at
1532
+ * `rootDirectory`, used as the fallback when construct-level diagnostic
1533
+ * evidence cannot be read. Wraps core's
1534
+ * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
1535
+ * lookup; unreadable files / out-of-range lines return `null`.
1536
+ */
1537
+ const createSourceLineReader = (rootDirectory) => {
1538
+ const readFileLines = createNodeReadFileLinesSync(rootDirectory);
1539
+ const fileLinesCache = /* @__PURE__ */ new Map();
1540
+ return (filePath, line) => {
1541
+ if (!fileLinesCache.has(filePath)) fileLinesCache.set(filePath, readFileLines(filePath));
1542
+ const lines = fileLinesCache.get(filePath) ?? null;
1543
+ if (lines === null || line < 1 || line > lines.length) return null;
1544
+ return lines[line - 1] ?? null;
1418
1545
  };
1419
1546
  };
1420
- const printScoreOnlyInspectResult = ({ options, payload, diagnostics, noScoreMessage }) => gen(function* () {
1421
- if (options.outputDirectory !== null) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, false, "stderr");
1422
- if (payload.score) {
1423
- yield* log(`${payload.score.score}`);
1424
- return;
1425
- }
1426
- yield* error(highlighter.gray(noScoreMessage));
1427
- });
1428
- const printFullInspectResultDetails = ({ options, payload, diagnostics, demotedDiagnosticCount }) => gen(function* () {
1429
- if (options.outputDirectory !== null || options.verbose) yield* printDiagnosticsDump(diagnostics, options.outputDirectory, options.verbose);
1430
- if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
1431
- yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
1432
- yield* log("");
1433
- }
1434
- yield* printFooter({
1435
- diagnostics,
1436
- scoreResult: payload.score,
1437
- projectName: payload.project.projectName,
1438
- isOffline: options.isCi || !options.share || payload.score === null
1547
+ //#endregion
1548
+ //#region src/cli/utils/is-materializable-git-source.ts
1549
+ const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
1550
+ const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
1551
+ //#endregion
1552
+ //#region src/cli/utils/materialize-baseline-files.ts
1553
+ /**
1554
+ * Materializes the base side of the `ref` → worktree diff into
1555
+ * `tempDirectory`, mirroring the project layout plus head's config files so
1556
+ * both sides lint under the same rules. Git rename heuristics are disabled by
1557
+ * the diff planner, so an old path is retained as a base deletion while its
1558
+ * new path is a head addition. Missing head-only additions are expected, and a
1559
+ * base-only deletion that cannot be read has nothing at head to compare
1560
+ * against; only a missing path that the plan says exists on both sides makes
1561
+ * `isComplete` false.
1562
+ */
1563
+ const materializeBaselineFiles = (input) => runPromise(gen(function* () {
1564
+ const git = yield* Git;
1565
+ const diffPlan = yield* git.baselineDiffPlan({
1566
+ directory: input.directory,
1567
+ ref: input.ref
1439
1568
  });
1440
- });
1441
- const printFullInspectResult = ({ options, elapsedMilliseconds, payload, result, diagnostics, demotedDiagnosticCount, noScoreMessage }) => gen(function* () {
1442
- if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
1443
- yield* printHeadlessReport({
1444
- diagnostics,
1445
- elapsedMilliseconds,
1446
- emptyStateMessage: buildEmptyReportMessage({
1447
- categoryFilters: options.categoryFilters,
1448
- demotedDiagnosticCount,
1449
- outputSurface: options.outputSurface
1450
- }),
1451
- noScoreMessage,
1452
- projectName: payload.project.projectName,
1453
- scannedFileCount: payload.scannedFileCount,
1454
- scoreResult: result.skippedChecks.length > 0 ? null : payload.score,
1455
- skippedChecks: result.skippedChecks
1569
+ const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
1570
+ const headFiles = input.headFiles ?? diffPlan?.headFiles;
1571
+ const untrackedFiles = diffPlan?.untrackedFiles ?? [];
1572
+ if (baseFiles === void 0 || headFiles === void 0) return null;
1573
+ const files = [...new Set([...input.files, ...baseFiles])];
1574
+ const tree = yield* materializeSourceTree({
1575
+ directory: input.directory,
1576
+ files,
1577
+ tempDirectory: input.tempDirectory,
1578
+ readContent: (relativePath) => git.showRefContent({
1579
+ directory: input.directory,
1580
+ ref: input.ref,
1581
+ relativePath,
1582
+ options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
1583
+ }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
1456
1584
  });
1457
- yield* printFullInspectResultDetails({
1458
- options,
1459
- payload,
1460
- diagnostics,
1461
- demotedDiagnosticCount
1585
+ const materializedFiles = new Set(tree.materializedFiles);
1586
+ const headFileSet = new Set(headFiles);
1587
+ return {
1588
+ ...tree,
1589
+ baseFiles,
1590
+ headFiles,
1591
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
1592
+ untrackedFiles
1593
+ };
1594
+ }).pipe(provide(Git.layerNode)));
1595
+ /**
1596
+ * Resolves the commit a baseline scan should read base content from — the
1597
+ * merge-base of `ref` and HEAD, so "introduced" is measured against the branch
1598
+ * point. `null` when unresolvable (ref missing/unsafe, no merge-base).
1599
+ */
1600
+ const resolveMergeBaseRef = async (directory, ref) => {
1601
+ try {
1602
+ return await runPromise(gen(function* () {
1603
+ return yield* (yield* Git).mergeBase({
1604
+ directory,
1605
+ ref
1606
+ });
1607
+ }).pipe(provide(Git.layerNode)));
1608
+ } catch {
1609
+ return null;
1610
+ }
1611
+ };
1612
+ //#endregion
1613
+ //#region src/cli/utils/has-unlinted-surviving-base-file.ts
1614
+ const hasUnlintedSurvivingBaseFile = (input) => {
1615
+ const analyzedBaseFiles = new Set(input.analyzedBaseFiles.map(toForwardSlashes));
1616
+ return filterSourceFiles(input.baseLintPaths.map(toForwardSlashes)).some((filePath) => input.headFiles.has(filePath) && !analyzedBaseFiles.has(filePath));
1617
+ };
1618
+ //#endregion
1619
+ //#region src/cli/utils/run-baseline-comparison.ts
1620
+ const silentConsole$1 = makeNoopConsole();
1621
+ const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
1622
+ const runBaselineComparison = async (input) => {
1623
+ const baselineIncludePaths = filterPathsOutsideDirectories({
1624
+ rootDirectory: input.directory,
1625
+ relativePaths: input.options.includePaths,
1626
+ excludedDirectories: input.options.excludedProjectDirectories
1462
1627
  });
1463
- });
1464
- const finalizeInspectResult = (input) => gen(function* () {
1465
- const { payload } = input;
1466
- const result = buildInspectResult(input);
1467
- const noScoreMessage = buildNoScoreMessage({
1468
- isScoreDisabled: input.options.noScore,
1469
- isAnalysisIncomplete: hasIncompleteScoreAnalysis(result.skippedChecks),
1470
- disabledMessage: input.options.scoreDisabledMessage
1628
+ const baselineBaseFiles = input.baseFiles ? filterPathsOutsideDirectories({
1629
+ rootDirectory: input.directory,
1630
+ relativePaths: input.baseFiles,
1631
+ excludedDirectories: input.options.excludedProjectDirectories
1632
+ }) : void 0;
1633
+ const baselineHeadFiles = input.headFiles ? filterPathsOutsideDirectories({
1634
+ rootDirectory: input.directory,
1635
+ relativePaths: input.headFiles,
1636
+ excludedDirectories: input.options.excludedProjectDirectories
1637
+ }) : void 0;
1638
+ const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
1639
+ if (remainingBaselineBudgetMs === 0) return null;
1640
+ const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
1641
+ const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
1642
+ let maintainabilitySourceFiles = [];
1643
+ if (input.options.deadCode) try {
1644
+ maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
1645
+ } catch (error) {
1646
+ if (baselineDeadlineSignal?.aborted) return null;
1647
+ throw error;
1648
+ }
1649
+ const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
1650
+ const snapshot = await materializeBaselineFiles({
1651
+ directory: input.directory,
1652
+ ref: input.baselineRef,
1653
+ files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
1654
+ baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
1655
+ headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
1656
+ tempDirectory: temporaryDirectory
1657
+ }).catch((error) => {
1658
+ rmSync(temporaryDirectory, {
1659
+ recursive: true,
1660
+ force: true
1661
+ });
1662
+ throw error;
1471
1663
  });
1472
- if (input.options.suppressRendering) return result;
1473
- const presentation = buildInspectPresentation(input);
1474
- if (input.options.scoreOnly) {
1475
- yield* printScoreOnlyInspectResult({
1476
- options: input.options,
1477
- payload,
1478
- diagnostics: presentation.diagnostics,
1479
- noScoreMessage
1664
+ if (snapshot === null) {
1665
+ rmSync(temporaryDirectory, {
1666
+ recursive: true,
1667
+ force: true
1668
+ });
1669
+ return null;
1670
+ }
1671
+ try {
1672
+ if (!snapshot.isComplete) return null;
1673
+ if (input.options.deadCode && !await copyUnchangedBaselineSources({
1674
+ directory: input.directory,
1675
+ sourceFiles: maintainabilitySourceFiles,
1676
+ baseMaterializedFiles: snapshot.materializedFiles,
1677
+ headChangedFiles: snapshot.headFiles,
1678
+ untrackedFiles: snapshot.untrackedFiles,
1679
+ tempDirectory: snapshot.tempDirectory,
1680
+ deadlineEpochMs: input.deadlineEpochMs,
1681
+ signal: input.oxlintRuntime.abortSignal
1682
+ })) return null;
1683
+ const filteredSnapshotBaseFiles = filterPathsOutsideDirectories({
1684
+ rootDirectory: input.directory,
1685
+ relativePaths: snapshot.baseFiles,
1686
+ excludedDirectories: input.options.excludedProjectDirectories
1687
+ });
1688
+ const filteredSnapshotHeadFiles = filterPathsOutsideDirectories({
1689
+ rootDirectory: input.directory,
1690
+ relativePaths: snapshot.headFiles,
1691
+ excludedDirectories: input.options.excludedProjectDirectories
1692
+ });
1693
+ const analyzedHeadFiles = new Set(input.headAnalyzedFiles.map(toForwardSlashes));
1694
+ const baseFiles = new Set((baselineBaseFiles ?? filteredSnapshotBaseFiles).map(toForwardSlashes));
1695
+ const expectedHeadFiles = new Set((baselineHeadFiles ?? filteredSnapshotHeadFiles).map(toForwardSlashes));
1696
+ for (const filePath of baselineIncludePaths) {
1697
+ const normalizedFilePath = toForwardSlashes(filePath);
1698
+ if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
1699
+ }
1700
+ if (input.options.lint && filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
1701
+ const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
1702
+ const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
1703
+ const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
1704
+ const baselineExcludedProjectDirectories = input.options.excludedProjectDirectories.map((excludedDirectory) => path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.directory)).map((excludedDirectory) => path.resolve(snapshot.tempDirectory, path.relative(input.directory, excludedDirectory)));
1705
+ const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
1706
+ const runtimeLayers = buildRuntimeLayers({
1707
+ directory: snapshot.tempDirectory,
1708
+ hasConfigOverride: true,
1709
+ userConfig: input.userConfig,
1710
+ configSourceDirectory: input.configSourceDirectory,
1711
+ projectInfoOverride: input.headProjectInfo,
1712
+ shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
1713
+ shouldRunDeadCode: input.options.deadCode,
1714
+ shouldRunSupplyChain: input.options.supplyChain,
1715
+ shouldComputeScore: false,
1716
+ shouldShowProgressSpinners: false,
1717
+ oxlintConcurrency: input.oxlintRuntime.concurrency,
1718
+ oxlintSpawnSlots: input.oxlintRuntime.spawnSlots
1719
+ });
1720
+ const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
1721
+ directory: snapshot.tempDirectory,
1722
+ includePaths: baseIncludePaths,
1723
+ maintainabilityFocusPaths,
1724
+ customRulesOnly: input.options.customRulesOnly,
1725
+ respectInlineDisables: input.options.respectInlineDisables,
1726
+ warnings: input.options.warnings,
1727
+ adoptExistingLintConfig: input.options.adoptExistingLintConfig,
1728
+ ignoredTags: input.options.ignoredTags,
1729
+ includedTags: input.options.includedTags,
1730
+ includeTagDefaults: input.options.includeTagDefaults,
1731
+ nodeBinaryPath: input.resolvedNodeBinaryPath ?? void 0,
1732
+ runDeadCode: input.options.deadCode,
1733
+ isCi: input.options.isCi,
1734
+ doctorVersion: VERSION,
1735
+ runId: getRunId(),
1736
+ resolveLocalGithubViewerPermission: false,
1737
+ suppressScanSummary: true,
1738
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged,
1739
+ deadlineEpochMs: input.deadlineEpochMs ?? void 0,
1740
+ signal: input.oxlintRuntime.abortSignal,
1741
+ excludedProjectDirectories: baselineExcludedProjectDirectories,
1742
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
1743
+ }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
1744
+ if (baseOutput.didLintFail || baseOutput.didDeadCodeFail) return null;
1745
+ if (countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0 && hasUnlintedSurvivingBaseFile({
1746
+ baseLintPaths: materializedLintPaths,
1747
+ headFiles: expectedHeadFiles,
1748
+ analyzedBaseFiles: baseOutput.analyzedFiles
1749
+ })) return null;
1750
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
1751
+ rootDirectory: input.directory,
1752
+ relativePaths: snapshot.untrackedFiles,
1753
+ excludedDirectories: input.options.excludedProjectDirectories
1754
+ }).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
1755
+ const diagnosticDelta = computeDiagnosticDelta({
1756
+ headDiagnostics: input.headDiagnostics,
1757
+ baseDiagnostics: baseOutput.diagnostics,
1758
+ readHeadLine: createSourceLineReader(input.directory),
1759
+ readBaseLine: createSourceLineReader(snapshot.tempDirectory),
1760
+ readHeadEvidence: createDiagnosticEvidenceReader(input.directory, { resolveForwardedHandlers: true }),
1761
+ readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
1480
1762
  });
1481
- return result;
1763
+ return {
1764
+ displayDiagnostics: diagnosticDelta.newDiagnostics,
1765
+ baselineDelta: {
1766
+ baseRef: input.baselineRef,
1767
+ fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : diagnosticDelta.fixedCount,
1768
+ baseTotalCount: baseOutput.diagnostics.length,
1769
+ crossFileMatchCount: diagnosticDelta.crossFileMatchCount
1770
+ }
1771
+ };
1772
+ } finally {
1773
+ snapshot.cleanup();
1482
1774
  }
1483
- yield* printFullInspectResult({
1484
- ...input,
1485
- ...presentation,
1486
- result,
1487
- noScoreMessage
1488
- });
1489
- return result;
1490
- });
1491
- //#endregion
1492
- //#region src/cli/utils/resolve-worker-telemetry.ts
1493
- /**
1494
- * Projects the resolved lint worker count into the `(workerCount, parallel)`
1495
- * telemetry pair. `resolvedWorkerCount` is the count the scan actually fanned
1496
- * out to (`InspectOutput.scanConcurrency`); `pinnedConcurrency` is the caller's
1497
- * `inspect({ concurrency })` pin, used as the fallback when no scan resolved a
1498
- * count (the pre-scan failure path, or a cache entry persisted before the
1499
- * resolved count was tracked). `parallel` is derived from the count — NOT from
1500
- * whether a count was pinned — so the common auto path (no pin) still reports
1501
- * parallelism correctly instead of always reading `false`.
1502
- */
1503
- const resolveWorkerTelemetry = (resolvedWorkerCount, pinnedConcurrency) => {
1504
- const workerCount = resolvedWorkerCount ?? pinnedConcurrency;
1505
- return {
1506
- workerCount,
1507
- parallel: workerCount !== void 0 && workerCount > 1
1508
- };
1509
1775
  };
1510
1776
  //#endregion
1511
- //#region src/cli/utils/render-and-record-scan.ts
1512
- const silentConsole$2 = makeNoopConsole();
1513
- const deriveScope = (options) => {
1514
- if (options.baseline) return "changed";
1515
- if (options.changedLineRanges !== null) return "lines";
1516
- return options.includePaths.length > 0 ? "files" : "full";
1517
- };
1518
- const buildRunEventConfig = (options, userConfig, resolvedWorkerCount) => {
1519
- const { workerCount, parallel } = resolveWorkerTelemetry(resolvedWorkerCount, options.concurrency);
1520
- return {
1521
- scope: deriveScope(options),
1522
- parallel,
1523
- workerCount,
1524
- maxDurationMs: options.maxDurationMs,
1525
- lint: options.lint,
1526
- deadCode: options.deadCode,
1527
- supplyChain: options.supplyChain,
1528
- scoreOnly: options.scoreOnly,
1529
- noScore: options.noScore,
1530
- respectInlineDisables: options.respectInlineDisables,
1531
- showWarnings: options.warnings,
1532
- usedOutputDir: options.outputDirectory !== null,
1533
- ignoredTagCount: options.ignoredTags.size,
1534
- hasCustomConfig: userConfig !== null,
1535
- userConfig
1536
- };
1537
- };
1538
- const renderAndRecordScan = async (input) => {
1539
- const cacheStats = {
1540
- lintCacheHitFileCount: input.cacheStats?.lintCacheHitFileCount ?? null,
1541
- lintCacheTotalFileCount: input.cacheStats?.lintCacheTotalFileCount ?? null,
1542
- lintSidecarReplayedFileCount: input.cacheStats?.lintSidecarReplayedFileCount ?? null,
1543
- lintSidecarTotalFileCount: input.cacheStats?.lintSidecarTotalFileCount ?? null,
1544
- deadCodeCacheHit: input.cacheStats?.deadCodeCacheHit ?? null,
1545
- deadCodeSummaryCacheHits: input.cacheStats?.deadCodeSummaryCacheHits ?? null,
1546
- deadCodeSummaryCacheMisses: input.cacheStats?.deadCodeSummaryCacheMisses ?? null
1547
- };
1548
- const finalizeEffect = finalizeInspectResult({
1549
- options: input.options,
1550
- elapsedMilliseconds: performance.now() - input.startTime,
1551
- payload: input.payload,
1552
- cacheStats
1553
- });
1554
- const result = await runPromise(input.options.silent ? finalizeEffect.pipe(provideService(Console, silentConsole$2)) : finalizeEffect);
1555
- const { workerCount: resolvedWorkerCount, parallel } = resolveWorkerTelemetry(input.payload.scanConcurrency, input.options.concurrency);
1556
- recordScanMetrics({
1557
- result,
1558
- mode: input.scanMode,
1559
- baselineDegraded: input.baselineDegraded,
1560
- parallel,
1561
- workerCount: resolvedWorkerCount,
1562
- lint: input.options.lint,
1563
- deadCode: input.options.deadCode,
1564
- scoreOnly: input.options.scoreOnly,
1565
- noScore: input.options.noScore,
1566
- didLintFail: input.payload.didLintFail,
1567
- lintFailureReasonKind: input.payload.lintFailureReasonKind,
1568
- didDeadCodeFail: input.payload.didDeadCodeFail,
1569
- userConfig: input.payload.userConfig,
1570
- suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
1571
- });
1572
- recordRunEvent(input.rootSpan, {
1573
- ...buildRunEventConfig(input.options, input.payload.userConfig, resolvedWorkerCount),
1574
- result,
1575
- mode: input.scanMode,
1576
- gateExempt: input.baselineDegraded,
1577
- wholeRepoCacheHit: input.wholeRepoCacheHit,
1578
- didLintFail: input.payload.didLintFail,
1579
- lintFailureReasonKind: input.payload.lintFailureReasonKind,
1580
- lintPartialFailureCount: input.payload.lintPartialFailures.length,
1581
- lintDroppedFileCount: countDroppedLintFiles(input.payload.lintPartialFailures),
1582
- lintDeadlineSkippedFileCount: countDeadlineSkippedFiles(input.payload.lintPartialFailures),
1583
- didDeadCodeFail: input.payload.didDeadCodeFail,
1584
- supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
1585
- securityScanFailed: input.payload.securityScanFailed,
1586
- suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
1587
- });
1588
- return result;
1777
+ //#region src/cli/utils/scan-result-cache-payload.ts
1778
+ const decodeDiagnostic = decodeUnknownSync(Diagnostic);
1779
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1780
+ const isNullableString = (value) => value === null || typeof value === "string";
1781
+ const isBoolean = (value) => typeof value === "boolean";
1782
+ const isNumber = (value) => typeof value === "number";
1783
+ const isString = (value) => typeof value === "string";
1784
+ const isDiagnosticArray = (value) => {
1785
+ if (!Array.isArray(value)) return false;
1786
+ try {
1787
+ for (const entry of value) decodeDiagnostic(entry);
1788
+ return true;
1789
+ } catch {
1790
+ return false;
1791
+ }
1589
1792
  };
1590
- //#endregion
1591
- //#region src/cli/utils/copy-unchanged-baseline-sources.ts
1592
- const copyUnchangedBaselineSources = async (input) => {
1593
- const baseMaterializedFiles = new Set(input.baseMaterializedFiles.map(toForwardSlashes));
1594
- const headChangedFiles = new Set(input.headChangedFiles.map(toForwardSlashes));
1595
- const untrackedFiles = new Set(input.untrackedFiles.map(toForwardSlashes));
1596
- const sourceDirectory = path$1.resolve(input.directory);
1597
- const targetDirectory = path$1.resolve(input.tempDirectory);
1598
- return (await mapWithConcurrency(input.sourceFiles.filter((filePath) => {
1599
- const normalizedPath = toForwardSlashes(filePath);
1600
- return !baseMaterializedFiles.has(normalizedPath) && !headChangedFiles.has(normalizedPath) && !untrackedFiles.has(normalizedPath);
1601
- }), 32, async (filePath) => {
1602
- input.signal?.throwIfAborted();
1603
- if (input.deadlineEpochMs !== null && Date.now() >= input.deadlineEpochMs) return false;
1604
- const sourcePath = path$1.resolve(sourceDirectory, filePath);
1605
- const targetPath = path$1.resolve(targetDirectory, filePath);
1606
- if (!isPathInsideDirectory(sourcePath, sourceDirectory) || !isPathInsideDirectory(targetPath, targetDirectory)) return false;
1607
- try {
1608
- if (!(await fs$1.lstat(sourcePath)).isFile()) return false;
1609
- await fs$1.mkdir(path$1.dirname(targetPath), { recursive: true });
1610
- await fs$1.copyFile(sourcePath, targetPath);
1611
- return true;
1612
- } catch {
1613
- input.signal?.throwIfAborted();
1614
- return false;
1615
- }
1616
- })).every(Boolean);
1793
+ const isScoreResult = (value) => value === null || isRecord(value) && typeof value.score === "number" && typeof value.label === "string";
1794
+ const isProjectInfo = (value) => isRecord(value) && typeof value.rootDirectory === "string" && typeof value.projectName === "string" && typeof value.framework === "string" && typeof value.sourceFileCount === "number";
1795
+ const isBaselineDelta = (value) => isRecord(value) && typeof value.baseRef === "string" && typeof value.fixedCount === "number" && typeof value.baseTotalCount === "number" && (value.crossFileMatchCount === void 0 || typeof value.crossFileMatchCount === "number");
1796
+ const isSuppressedRuleCountArray = (value) => Array.isArray(value) && value.every((entry) => isRecord(entry) && typeof entry.rule === "string" && typeof entry.source === "string" && typeof entry.count === "number");
1797
+ const REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS = [
1798
+ {
1799
+ key: "diagnostics",
1800
+ isValid: isDiagnosticArray
1801
+ },
1802
+ {
1803
+ key: "score",
1804
+ isValid: isScoreResult
1805
+ },
1806
+ {
1807
+ key: "project",
1808
+ isValid: isProjectInfo
1809
+ },
1810
+ {
1811
+ key: "userConfig",
1812
+ isValid: (value) => value === null || isRecord(value)
1813
+ },
1814
+ {
1815
+ key: "didLintFail",
1816
+ isValid: isBoolean
1817
+ },
1818
+ {
1819
+ key: "lintFailureReason",
1820
+ isValid: isNullableString
1821
+ },
1822
+ {
1823
+ key: "lintPartialFailures",
1824
+ isValid: isStringArray
1825
+ },
1826
+ {
1827
+ key: "didDeadCodeFail",
1828
+ isValid: isBoolean
1829
+ },
1830
+ {
1831
+ key: "deadCodeFailureReason",
1832
+ isValid: isNullableString
1833
+ },
1834
+ {
1835
+ key: "deadCodeOverlapped",
1836
+ isValid: isBoolean
1837
+ },
1838
+ {
1839
+ key: "directory",
1840
+ isValid: isString
1841
+ },
1842
+ {
1843
+ key: "scannedFileCount",
1844
+ isValid: isNumber
1845
+ },
1846
+ {
1847
+ key: "scannedFilePaths",
1848
+ isValid: isStringArray
1849
+ },
1850
+ {
1851
+ key: "scanElapsedMilliseconds",
1852
+ isValid: isNumber
1853
+ },
1854
+ {
1855
+ key: "lintFailureReasonKind",
1856
+ isValid: isNullableString
1857
+ },
1858
+ {
1859
+ key: "supplyChainOverlapTimedOut",
1860
+ isValid: isBoolean
1861
+ }
1862
+ ];
1863
+ const OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS = [
1864
+ {
1865
+ key: "analyzedFiles",
1866
+ isValid: isStringArray
1867
+ },
1868
+ {
1869
+ key: "baselineDelta",
1870
+ isValid: isBaselineDelta
1871
+ },
1872
+ {
1873
+ key: "scanConcurrency",
1874
+ isValid: isNumber
1875
+ },
1876
+ {
1877
+ key: "securityScanFailed",
1878
+ isValid: isBoolean
1879
+ },
1880
+ {
1881
+ key: "securityScanFailureReason",
1882
+ isValid: isNullableString
1883
+ },
1884
+ {
1885
+ key: "suppressedRuleCounts",
1886
+ isValid: isSuppressedRuleCountArray
1887
+ },
1888
+ {
1889
+ key: "manifestContentHash",
1890
+ isValid: isNullableString
1891
+ }
1892
+ ];
1893
+ const hasValidRequiredCachedScanPayloadFields = (value) => REQUIRED_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => field.isValid(value[field.key]));
1894
+ const hasValidOptionalCachedScanPayloadFields = (value) => OPTIONAL_CACHED_SCAN_PAYLOAD_FIELDS.every((field) => value[field.key] === void 0 || field.isValid(value[field.key]));
1895
+ const isCachedScanPayload = (value) => {
1896
+ if (!isRecord(value)) return false;
1897
+ return hasValidRequiredCachedScanPayloadFields(value) && hasValidOptionalCachedScanPayloadFields(value);
1617
1898
  };
1899
+ const decodeCachedScanPayload = (value) => isCachedScanPayload(value) ? value : null;
1900
+ const shouldStoreScanPayload = (payload) => !payload.didLintFail && !payload.didDeadCodeFail && payload.lintPartialFailures.length === 0 && !payload.supplyChainOverlapTimedOut && payload.securityScanFailed !== true;
1618
1901
  //#endregion
1619
- //#region src/cli/utils/read-diagnostic-evidence.ts
1620
- const SOURCE_EXTENSIONS = [
1621
- ".ts",
1622
- ".tsx",
1623
- ".js",
1624
- ".jsx",
1625
- ".mts",
1626
- ".cts",
1627
- ".mjs",
1628
- ".cjs"
1902
+ //#region src/cli/utils/scan-result-cache.ts
1903
+ const createScanResultCacheInvocationState = () => ({
1904
+ repositoryIdentityByRoot: /* @__PURE__ */ new Map(),
1905
+ firstProjectIdentity: null
1906
+ });
1907
+ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
1908
+ "oxlint/package.json",
1909
+ "oxlint-plugin-react-doctor/package.json",
1910
+ "eslint-plugin-react-hooks/package.json"
1629
1911
  ];
1630
- const COMPONENT_WRAPPER_NAMES = new Set(["memo", "forwardRef"]);
1631
- const getScriptKind = (filePath) => {
1632
- const extension = path.extname(filePath).toLowerCase();
1633
- if (extension === ".tsx") return _react_doctor_require_typescript_default.ScriptKind.TSX;
1634
- if (extension === ".jsx") return _react_doctor_require_typescript_default.ScriptKind.JSX;
1635
- if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return _react_doctor_require_typescript_default.ScriptKind.JS;
1636
- return _react_doctor_require_typescript_default.ScriptKind.TS;
1637
- };
1638
- const resolveSafeSourcePath = (rootDirectory, filePath) => {
1912
+ const bundledRequire = createRequire(import.meta.url);
1913
+ const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
1639
1914
  try {
1640
- const resolvedRootDirectory = fs.realpathSync(rootDirectory);
1641
- const absolutePath = path.resolve(resolvedRootDirectory, filePath);
1642
- const relativePath = path.relative(resolvedRootDirectory, absolutePath);
1643
- if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return null;
1644
- const stats = fs.lstatSync(absolutePath);
1645
- if (!stats.isFile() || stats.isSymbolicLink()) return null;
1646
- const realPath = fs.realpathSync(absolutePath);
1647
- const realRelativePath = path.relative(resolvedRootDirectory, realPath);
1648
- return realRelativePath !== ".." && !realRelativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelativePath) ? realPath : null;
1915
+ return `oxlint-plugin-react-doctor#fingerprint=${hashFileContents(bundledRequire.resolve("oxlint-plugin-react-doctor")) ?? "unreadable"}`;
1649
1916
  } catch {
1650
- return null;
1917
+ return "oxlint-plugin-react-doctor#fingerprint=unresolved";
1651
1918
  }
1652
- };
1653
- const readSourceRecord = (rootDirectory, filePath, state) => {
1654
- const normalizedFilePath = filePath.replace(/\\/g, "/");
1655
- if (state.recordByFilePath.has(normalizedFilePath)) return state.recordByFilePath.get(normalizedFilePath) ?? null;
1656
- const absolutePath = resolveSafeSourcePath(rootDirectory, normalizedFilePath);
1657
- if (absolutePath === null) {
1658
- state.recordByFilePath.set(normalizedFilePath, null);
1659
- return null;
1919
+ })();
1920
+ const normalizeForStableJson = (value) => {
1921
+ if (value === null) return null;
1922
+ if (value === void 0) return void 0;
1923
+ const valueType = typeof value;
1924
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") return value;
1925
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") throw new Error("Unsupported cache key value");
1926
+ if (Array.isArray(value)) return value.map((item) => {
1927
+ const normalized = normalizeForStableJson(item);
1928
+ return normalized === void 0 ? null : normalized;
1929
+ });
1930
+ const prototype = Object.getPrototypeOf(value);
1931
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("Unsupported cache key object");
1932
+ if (!isRecord(value)) throw new Error("Unsupported cache key object");
1933
+ const normalizedRecord = {};
1934
+ for (const key of Object.keys(value).sort()) {
1935
+ const normalized = normalizeForStableJson(value[key]);
1936
+ if (normalized !== void 0) normalizedRecord[key] = normalized;
1660
1937
  }
1938
+ return normalizedRecord;
1939
+ };
1940
+ const stringifyStableJson = (value) => {
1661
1941
  try {
1662
- const sourceText = fs.readFileSync(absolutePath, "utf-8");
1663
- const record = {
1664
- absolutePath,
1665
- filePath: normalizedFilePath,
1666
- sourceFile: _react_doctor_require_typescript_default.createSourceFile(absolutePath, sourceText, _react_doctor_require_typescript_default.ScriptTarget.Latest, true, getScriptKind(absolutePath)),
1667
- sourceText
1668
- };
1669
- state.recordByFilePath.set(normalizedFilePath, record);
1670
- return record;
1942
+ return JSON.stringify(normalizeForStableJson(value));
1671
1943
  } catch {
1672
- state.recordByFilePath.set(normalizedFilePath, null);
1673
1944
  return null;
1674
1945
  }
1675
1946
  };
1676
- const listSourceRecords = (rootDirectory, state) => {
1677
- if (state.sourceRecords !== null) return state.sourceRecords;
1678
- state.sourceRecords = listSourceFiles(rootDirectory).flatMap((filePath) => {
1679
- const record = readSourceRecord(rootDirectory, filePath, state);
1680
- return record === null ? [] : [record];
1681
- });
1682
- return state.sourceRecords;
1683
- };
1684
- const unwrapExpression = (expression) => {
1685
- let currentExpression = expression;
1686
- while (_react_doctor_require_typescript_default.isParenthesizedExpression(currentExpression) || _react_doctor_require_typescript_default.isAsExpression(currentExpression) || _react_doctor_require_typescript_default.isTypeAssertionExpression(currentExpression) || _react_doctor_require_typescript_default.isNonNullExpression(currentExpression) || _react_doctor_require_typescript_default.isSatisfiesExpression(currentExpression)) currentExpression = currentExpression.expression;
1687
- return currentExpression;
1688
- };
1689
- const getCalleeName = (expression) => {
1690
- const unwrappedExpression = unwrapExpression(expression);
1691
- if (_react_doctor_require_typescript_default.isIdentifier(unwrappedExpression)) return unwrappedExpression.text;
1692
- if (_react_doctor_require_typescript_default.isPropertyAccessExpression(unwrappedExpression)) return unwrappedExpression.name.text;
1693
- return null;
1694
- };
1695
- const isComponentWrapperCall = (node) => {
1696
- const wrapperName = getCalleeName(node.expression);
1697
- return wrapperName !== null && COMPONENT_WRAPPER_NAMES.has(wrapperName);
1698
- };
1699
- const getFunctionName = (node) => {
1700
- if (_react_doctor_require_typescript_default.isFunctionDeclaration(node) && node.name !== void 0) return node.name.text;
1701
- if (_react_doctor_require_typescript_default.isArrowFunction(node) || _react_doctor_require_typescript_default.isFunctionExpression(node)) {
1702
- let componentExpression = node;
1703
- while (true) {
1704
- const parent = componentExpression.parent;
1705
- if (_react_doctor_require_typescript_default.isParenthesizedExpression(parent) || _react_doctor_require_typescript_default.isAsExpression(parent) || _react_doctor_require_typescript_default.isTypeAssertionExpression(parent) || _react_doctor_require_typescript_default.isNonNullExpression(parent) || _react_doctor_require_typescript_default.isSatisfiesExpression(parent)) {
1706
- componentExpression = parent;
1707
- continue;
1708
- }
1709
- if (_react_doctor_require_typescript_default.isCallExpression(parent) && parent.arguments[0] === componentExpression && isComponentWrapperCall(parent)) {
1710
- componentExpression = parent;
1711
- continue;
1712
- }
1713
- if (_react_doctor_require_typescript_default.isVariableDeclaration(parent) && _react_doctor_require_typescript_default.isIdentifier(parent.name)) return parent.name.text;
1714
- return node.name?.text ?? null;
1715
- }
1716
- }
1717
- return null;
1718
- };
1719
- const isComponentFunction = (node) => _react_doctor_require_typescript_default.isFunctionDeclaration(node) || _react_doctor_require_typescript_default.isFunctionExpression(node) || _react_doctor_require_typescript_default.isArrowFunction(node);
1720
- const isDefaultExportedComponent = (record, node, componentName) => {
1721
- if (_react_doctor_require_typescript_default.canHaveModifiers(node) && _react_doctor_require_typescript_default.getModifiers(node)?.some((modifier) => modifier.kind === _react_doctor_require_typescript_default.SyntaxKind.DefaultKeyword)) return true;
1722
- return record.sourceFile.statements.some((statement) => _react_doctor_require_typescript_default.isExportAssignment(statement) && _react_doctor_require_typescript_default.isIdentifier(statement.expression) && statement.expression.text === componentName);
1723
- };
1724
- const findEnclosingComponent = (record, diagnostic) => {
1725
- if (!Number.isInteger(diagnostic.line) || diagnostic.line < 1) return null;
1726
- const lineIndex = diagnostic.line - 1;
1727
- const lineStarts = record.sourceFile.getLineStarts();
1728
- const lineStart = lineStarts[lineIndex];
1729
- if (lineStart === void 0) return null;
1730
- const nextLineStart = lineStarts[lineIndex + 1] ?? record.sourceFile.end;
1731
- const lineEnd = Math.max(lineStart, nextLineStart - 1);
1732
- const columnIndex = Number.isInteger(diagnostic.column) ? Math.max(0, diagnostic.column - 1) : 0;
1733
- const diagnosticPosition = Math.min(lineStart + columnIndex, lineEnd);
1734
- let component = null;
1735
- let componentWidth = Number.POSITIVE_INFINITY;
1736
- const visit = (node) => {
1737
- if (diagnosticPosition < node.getStart(record.sourceFile) || diagnosticPosition > node.end) return;
1738
- if (isComponentFunction(node)) {
1739
- const name = getFunctionName(node);
1740
- const width = node.end - node.getStart(record.sourceFile);
1741
- if (name !== null && /^[A-Z]/.test(name) && width < componentWidth) {
1742
- component = {
1743
- isDefaultExport: isDefaultExportedComponent(record, node, name),
1744
- name,
1745
- node,
1746
- record
1747
- };
1748
- componentWidth = width;
1749
- }
1947
+ const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
1948
+ const readHeadSha = (projectDirectory) => runGitAsync(projectDirectory, HEAD_SHA_GIT_ARGUMENTS);
1949
+ const parseWorktreeStatusRecords = (statusOutput) => {
1950
+ const entries = [];
1951
+ const tokens = statusOutput.split("\0");
1952
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
1953
+ let record = tokens[tokenIndex];
1954
+ if (record.length === 0) continue;
1955
+ if (record.length < 3 || record[2] !== " ") {
1956
+ if (tokenIndex !== 0) return null;
1957
+ record = ` ${record}`;
1750
1958
  }
1751
- _react_doctor_require_typescript_default.forEachChild(node, visit);
1752
- };
1753
- visit(record.sourceFile);
1754
- return component;
1755
- };
1756
- const moduleSpecifierTargetsComponent = (callerRecord, moduleSpecifier, componentRecord) => {
1757
- if (!moduleSpecifier.startsWith(".")) return false;
1758
- const unresolvedPath = path.resolve(path.dirname(callerRecord.absolutePath), moduleSpecifier);
1759
- const specifiedExtension = path.extname(unresolvedPath).toLowerCase();
1760
- const sourcePathWithoutExtension = SOURCE_EXTENSIONS.includes(specifiedExtension) ? unresolvedPath.slice(0, -specifiedExtension.length) : unresolvedPath;
1761
- return [
1762
- unresolvedPath,
1763
- ...SOURCE_EXTENSIONS.map((extension) => `${sourcePathWithoutExtension}${extension}`),
1764
- ...sourcePathWithoutExtension === unresolvedPath ? SOURCE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)) : []
1765
- ].includes(componentRecord.absolutePath);
1766
- };
1767
- const getComponentTagNames = (record, component) => {
1768
- if (record.absolutePath === component.record.absolutePath) return new Set([component.name]);
1769
- const tagNames = /* @__PURE__ */ new Set();
1770
- for (const statement of record.sourceFile.statements) {
1771
- if (!_react_doctor_require_typescript_default.isImportDeclaration(statement) || !_react_doctor_require_typescript_default.isStringLiteral(statement.moduleSpecifier) || !moduleSpecifierTargetsComponent(record, statement.moduleSpecifier.text, component.record)) continue;
1772
- const importClause = statement.importClause;
1773
- if (component.isDefaultExport && importClause?.name !== void 0) tagNames.add(importClause.name.text);
1774
- const namedBindings = importClause?.namedBindings;
1775
- if (namedBindings !== void 0 && _react_doctor_require_typescript_default.isNamedImports(namedBindings)) {
1776
- for (const element of namedBindings.elements) if ((element.propertyName?.text ?? element.name.text) === component.name) tagNames.add(element.name.text);
1959
+ if (record.length < 4 || record[2] !== " ") return null;
1960
+ const statusCode = record.slice(0, 2);
1961
+ const recordPath = record.slice(3);
1962
+ if (statusCode.includes("R") || statusCode.includes("C")) {
1963
+ tokenIndex += 1;
1964
+ const originPath = tokens[tokenIndex];
1965
+ if (originPath === void 0 || originPath.length === 0) return null;
1966
+ entries.push({
1967
+ statusCode,
1968
+ path: recordPath,
1969
+ originPath
1970
+ });
1971
+ continue;
1777
1972
  }
1973
+ entries.push({
1974
+ statusCode,
1975
+ path: recordPath
1976
+ });
1778
1977
  }
1779
- return tagNames;
1780
- };
1781
- const getTransparentCall = (functionNode) => {
1782
- if (functionNode.body === void 0) return null;
1783
- if (!_react_doctor_require_typescript_default.isBlock(functionNode.body)) {
1784
- const expression = unwrapExpression(functionNode.body);
1785
- return _react_doctor_require_typescript_default.isCallExpression(expression) ? expression : null;
1786
- }
1787
- if (functionNode.body.statements.length !== 1) return null;
1788
- const statement = functionNode.body.statements[0];
1789
- if (statement === void 0) return null;
1790
- const expression = _react_doctor_require_typescript_default.isExpressionStatement(statement) || _react_doctor_require_typescript_default.isReturnStatement(statement) ? statement.expression : void 0;
1791
- if (expression === void 0) return null;
1792
- const unwrappedExpression = unwrapExpression(expression);
1793
- return _react_doctor_require_typescript_default.isCallExpression(unwrappedExpression) ? unwrappedExpression : null;
1978
+ return entries;
1794
1979
  };
1795
- const resolveTransparentTarget = (functionNode) => {
1796
- const call = getTransparentCall(functionNode);
1797
- if (call === null) return null;
1798
- const target = unwrapExpression(call.expression);
1799
- if (!_react_doctor_require_typescript_default.isIdentifier(target) || call.arguments.length !== functionNode.parameters.length) return null;
1800
- for (const [parameterIndex, parameter] of functionNode.parameters.entries()) {
1801
- const argument = call.arguments[parameterIndex];
1802
- const unwrappedArgument = argument === void 0 ? null : unwrapExpression(argument);
1803
- if (!_react_doctor_require_typescript_default.isIdentifier(parameter.name) || unwrappedArgument === null || !_react_doctor_require_typescript_default.isIdentifier(unwrappedArgument) || unwrappedArgument.text !== parameter.name.text) return null;
1980
+ const buildDirtyPathContentFingerprint = (absolutePath, statusCode) => {
1981
+ let stats;
1982
+ try {
1983
+ stats = fs.lstatSync(absolutePath);
1984
+ } catch {
1985
+ return statusCode.includes("D") ? "deleted" : null;
1804
1986
  }
1805
- return target.text;
1806
- };
1807
- const getForwardingFunction = (declaration) => {
1808
- if (_react_doctor_require_typescript_default.isFunctionDeclaration(declaration)) return declaration;
1809
- if (!_react_doctor_require_typescript_default.isVariableDeclaration(declaration) || declaration.initializer === void 0) return null;
1810
- const initializer = unwrapExpression(declaration.initializer);
1811
- if (_react_doctor_require_typescript_default.isArrowFunction(initializer) || _react_doctor_require_typescript_default.isFunctionExpression(initializer)) return initializer;
1812
- if (_react_doctor_require_typescript_default.isCallExpression(initializer) && getCalleeName(initializer.expression) === "useCallback") {
1813
- const callback = initializer.arguments[0];
1814
- if (callback !== void 0 && (_react_doctor_require_typescript_default.isArrowFunction(callback) || _react_doctor_require_typescript_default.isFunctionExpression(callback))) return callback;
1987
+ if (stats.isSymbolicLink()) try {
1988
+ return `link:${fs.readlinkSync(absolutePath)}`;
1989
+ } catch {
1990
+ return null;
1815
1991
  }
1816
- return null;
1817
- };
1818
- const getDeclaredBindingName = (node) => {
1819
- if ((_react_doctor_require_typescript_default.isVariableDeclaration(node) || _react_doctor_require_typescript_default.isParameter(node) || _react_doctor_require_typescript_default.isBindingElement(node) || _react_doctor_require_typescript_default.isFunctionDeclaration(node)) && node.name !== void 0 && _react_doctor_require_typescript_default.isIdentifier(node.name)) return node.name.text;
1820
- if (_react_doctor_require_typescript_default.isImportClause(node)) return node.name?.text ?? null;
1821
- if (_react_doctor_require_typescript_default.isImportSpecifier(node) || _react_doctor_require_typescript_default.isNamespaceImport(node)) return node.name.text;
1822
- return null;
1992
+ if (!stats.isFile()) return null;
1993
+ if (stats.size > 10485760) return null;
1994
+ return hashFileContents(absolutePath);
1823
1995
  };
1824
- const findBindingScope = (node) => {
1825
- let currentNode = node;
1826
- while (currentNode !== void 0) {
1827
- if (_react_doctor_require_typescript_default.isParameter(currentNode) && _react_doctor_require_typescript_default.isFunctionLike(currentNode.parent)) return currentNode.parent;
1828
- if (_react_doctor_require_typescript_default.isBlock(currentNode) || _react_doctor_require_typescript_default.isModuleBlock(currentNode) || _react_doctor_require_typescript_default.isSourceFile(currentNode)) return currentNode;
1829
- currentNode = currentNode.parent;
1996
+ const buildWorktreeFingerprint = (repositoryRoot, statusOutput) => {
1997
+ if (statusOutput === null) return null;
1998
+ if (statusOutput.length === 0) return [];
1999
+ const statusEntries = parseWorktreeStatusRecords(statusOutput);
2000
+ if (statusEntries === null) return null;
2001
+ if (statusEntries.length > 300) return null;
2002
+ const dirtyEntries = [];
2003
+ for (const statusEntry of statusEntries) {
2004
+ const contentFingerprint = buildDirtyPathContentFingerprint(path.join(repositoryRoot, statusEntry.path), statusEntry.statusCode);
2005
+ if (contentFingerprint === null) return null;
2006
+ dirtyEntries.push({
2007
+ ...statusEntry,
2008
+ contentFingerprint
2009
+ });
1830
2010
  }
1831
- return node.getSourceFile();
2011
+ return dirtyEntries.sort((firstEntry, secondEntry) => {
2012
+ const firstSortKey = `${firstEntry.path}${firstEntry.statusCode}`;
2013
+ const secondSortKey = `${secondEntry.path}${secondEntry.statusCode}`;
2014
+ return firstSortKey < secondSortKey ? -1 : firstSortKey > secondSortKey ? 1 : 0;
2015
+ });
1832
2016
  };
1833
- const findBindingDeclaration = (record, binding) => {
1834
- const usagePosition = binding.getStart(record.sourceFile);
1835
- const candidates = [];
1836
- const visit = (node) => {
1837
- if (getDeclaredBindingName(node) === binding.text) {
1838
- const scope = findBindingScope(node);
1839
- const isVisibleScope = usagePosition >= scope.getStart(record.sourceFile) && usagePosition <= scope.end;
1840
- const isVisibleDeclaration = _react_doctor_require_typescript_default.isFunctionDeclaration(node) || _react_doctor_require_typescript_default.isParameter(node) || _react_doctor_require_typescript_default.isBindingElement(node) || _react_doctor_require_typescript_default.isImportClause(node) || _react_doctor_require_typescript_default.isImportSpecifier(node) || _react_doctor_require_typescript_default.isNamespaceImport(node) || node.getStart(record.sourceFile) < usagePosition;
1841
- if (isVisibleScope && isVisibleDeclaration) candidates.push({
1842
- declaration: node,
1843
- scope
1844
- });
1845
- }
1846
- _react_doctor_require_typescript_default.forEachChild(node, visit);
2017
+ const resolveIdentityFromProbes = async (repositoryRoot, statusPromise, headShaPromise, invocationState) => {
2018
+ const [statusOutput, headSha] = await Promise.all([statusPromise, headShaPromise]);
2019
+ const worktreeFingerprint = buildWorktreeFingerprint(repositoryRoot, statusOutput);
2020
+ const identity = worktreeFingerprint === null || headSha === null ? null : {
2021
+ headSha,
2022
+ worktreeFingerprint
1847
2023
  };
1848
- visit(record.sourceFile);
1849
- let nearestCandidate = null;
1850
- let nearestScopeWidth = Number.POSITIVE_INFINITY;
1851
- let isNearestScopeAmbiguous = false;
1852
- for (const candidate of candidates) {
1853
- const scopeWidth = candidate.scope.end - candidate.scope.getStart(record.sourceFile);
1854
- if (scopeWidth < nearestScopeWidth) {
1855
- nearestCandidate = candidate;
1856
- nearestScopeWidth = scopeWidth;
1857
- isNearestScopeAmbiguous = false;
1858
- } else if (scopeWidth === nearestScopeWidth) isNearestScopeAmbiguous = true;
1859
- }
1860
- if (isNearestScopeAmbiguous) return null;
1861
- const declaration = nearestCandidate?.declaration;
1862
- return declaration !== void 0 && (_react_doctor_require_typescript_default.isVariableDeclaration(declaration) || _react_doctor_require_typescript_default.isFunctionDeclaration(declaration)) ? declaration : null;
2024
+ invocationState.repositoryIdentityByRoot.set(repositoryRoot, identity);
2025
+ return identity;
1863
2026
  };
1864
- const resolveHandlerBinding = (record, binding) => {
1865
- const declaration = findBindingDeclaration(record, binding);
1866
- if (declaration === null) return binding.text;
1867
- const forwardingFunction = getForwardingFunction(declaration);
1868
- const target = forwardingFunction === null ? null : resolveTransparentTarget(forwardingFunction);
1869
- return target === null || target === binding.text ? binding.text : target;
2027
+ const resolveSiblingIdentity = async (projectDirectory, invocationState, repositoryRootPromise, firstProjectIdentity) => {
2028
+ const [repositoryRoot] = await Promise.all([repositoryRootPromise, firstProjectIdentity]);
2029
+ if (repositoryRoot === null) return null;
2030
+ const cachedIdentity = invocationState.repositoryIdentityByRoot.get(repositoryRoot);
2031
+ if (cachedIdentity !== void 0) return cachedIdentity;
2032
+ return resolveIdentityFromProbes(repositoryRoot, runGitAsync(projectDirectory, WORKTREE_STATUS_GIT_ARGUMENTS), readHeadSha(repositoryRoot), invocationState);
1870
2033
  };
1871
- const getJsxTagName = (node) => _react_doctor_require_typescript_default.isIdentifier(node.tagName) ? node.tagName.text : null;
1872
- const getHandlerBinding = (node, propName) => {
1873
- for (const property of node.attributes.properties) {
1874
- if (!_react_doctor_require_typescript_default.isJsxAttribute(property) || property.name.getText() !== propName) continue;
1875
- const initializer = property.initializer;
1876
- if (initializer === void 0 || !_react_doctor_require_typescript_default.isJsxExpression(initializer)) return null;
1877
- if (initializer.expression === void 0) return null;
1878
- const expression = unwrapExpression(initializer.expression);
1879
- return _react_doctor_require_typescript_default.isIdentifier(expression) ? expression : null;
1880
- }
1881
- return null;
2034
+ const resolveRepositoryCacheIdentity = (projectDirectory, invocationState, repositoryRootPromise) => {
2035
+ if (invocationState.firstProjectIdentity !== null) return resolveSiblingIdentity(projectDirectory, invocationState, repositoryRootPromise, invocationState.firstProjectIdentity);
2036
+ const speculativeStatus = runGitAsync(projectDirectory, WORKTREE_STATUS_GIT_ARGUMENTS);
2037
+ const speculativeHeadSha = readHeadSha(projectDirectory);
2038
+ invocationState.firstProjectIdentity = repositoryRootPromise.then((repositoryRoot) => repositoryRoot === null ? null : resolveIdentityFromProbes(repositoryRoot, speculativeStatus, speculativeHeadSha, invocationState));
2039
+ return invocationState.firstProjectIdentity;
1882
2040
  };
1883
- const getForwardedHandlerAliases = (rootDirectory, component, propNames, state) => {
1884
- const cacheKey = `${component.record.filePath}\0${component.name}\0${[...propNames].sort().join("\0")}`;
1885
- const cachedAliases = state.aliasesByComponent.get(cacheKey);
1886
- if (cachedAliases !== void 0) return cachedAliases;
1887
- const bindingsByPropName = /* @__PURE__ */ new Map();
1888
- const unresolvedPropNames = /* @__PURE__ */ new Set();
1889
- let callsiteCount = 0;
1890
- for (const record of listSourceRecords(rootDirectory, state)) {
1891
- const tagNames = getComponentTagNames(record, component);
1892
- if (tagNames.size === 0) continue;
1893
- const visit = (node) => {
1894
- if (record === component.record && node === component.node) return;
1895
- if (_react_doctor_require_typescript_default.isJsxOpeningElement(node) || _react_doctor_require_typescript_default.isJsxSelfClosingElement(node)) {
1896
- const tagName = getJsxTagName(node);
1897
- if (tagName !== null && tagNames.has(tagName)) {
1898
- callsiteCount += 1;
1899
- for (const propName of propNames) {
1900
- const binding = getHandlerBinding(node, propName);
1901
- if (binding === null) {
1902
- unresolvedPropNames.add(propName);
1903
- continue;
1904
- }
1905
- const bindings = bindingsByPropName.get(propName) ?? /* @__PURE__ */ new Set();
1906
- bindings.add(resolveHandlerBinding(record, binding));
1907
- bindingsByPropName.set(propName, bindings);
1908
- }
1909
- }
1910
- }
1911
- _react_doctor_require_typescript_default.forEachChild(node, visit);
1912
- };
1913
- visit(record.sourceFile);
1914
- }
1915
- const aliases = /* @__PURE__ */ new Map();
1916
- if (callsiteCount > 0) for (const [propName, bindings] of bindingsByPropName) {
1917
- if (bindings.size !== 1 || unresolvedPropNames.has(propName)) continue;
1918
- const [bindingName] = bindings;
1919
- if (bindingName !== void 0) aliases.set(propName, bindingName);
2041
+ const DOTENV_FILE_NAME_PATTERN = /^\.env(\.|$)/;
2042
+ const resolveDotenvFingerprint = (projectDirectory) => {
2043
+ try {
2044
+ return fs.readdirSync(projectDirectory).filter((entryName) => DOTENV_FILE_NAME_PATTERN.test(entryName)).sort().map((entryName) => `${entryName}=${hashFileContents(path.join(projectDirectory, entryName)) ?? "unreadable"}`);
2045
+ } catch {
2046
+ return [];
1920
2047
  }
1921
- state.aliasesByComponent.set(cacheKey, aliases);
1922
- return aliases;
1923
- };
1924
- const normalizeForwardedHandlers = (evidence, aliases) => {
1925
- let normalizedEvidence = evidence;
1926
- for (const [propName, bindingName] of aliases) normalizedEvidence = normalizedEvidence.replace(new RegExp(`\\{\\s*${propName}\\s*\\}`, "g"), `{${bindingName}}`).replace(new RegExp(`=>\\s*${propName}\\s*\\(`, "g"), `=> ${bindingName}(`);
1927
- return normalizedEvidence;
1928
2048
  };
1929
- const createDiagnosticEvidenceReader = (rootDirectory, options = {}) => {
1930
- const state = {
1931
- aliasesByComponent: /* @__PURE__ */ new Map(),
1932
- recordByFilePath: /* @__PURE__ */ new Map(),
1933
- sourceRecords: null
1934
- };
1935
- return (diagnostic) => {
1936
- const record = readSourceRecord(rootDirectory, diagnostic.filePath, state);
1937
- if (record === null || !Number.isInteger(diagnostic.line)) return null;
1938
- const sourceLines = record.sourceText.split(/\r?\n/);
1939
- const startLineIndex = Math.max(0, diagnostic.line - 1);
1940
- const endLineIndex = Math.max(startLineIndex, (diagnostic.endLine ?? diagnostic.line) - 1);
1941
- const evidence = sourceLines.slice(startLineIndex, endLineIndex + 1).join("\n");
1942
- if (!options.resolveForwardedHandlers) return evidence;
1943
- const propNames = new Set(evidence.match(/\bon[A-Z]\w*\b/g) ?? []);
1944
- if (propNames.size === 0) return evidence;
1945
- const component = findEnclosingComponent(record, diagnostic);
1946
- if (component === null) return evidence;
1947
- return normalizeForwardedHandlers(evidence, getForwardedHandlerAliases(rootDirectory, component, propNames, state));
1948
- };
2049
+ const isGitIdentityTrustworthy = async (projectDirectory) => {
2050
+ const output = await runGitAsync(projectDirectory, TRACKED_FILE_FLAGS_GIT_ARGUMENTS);
2051
+ if (output === null) return false;
2052
+ const entryLines = output.split("\n").filter((line) => line.length > 0);
2053
+ return entryLines.length > 0 && entryLines.every((line) => line[0] === "H");
1949
2054
  };
1950
- //#endregion
1951
- //#region src/cli/utils/read-source-line.ts
1952
- /**
1953
- * Builds a cached `(filePath, line) -> source text` reader rooted at
1954
- * `rootDirectory`, used as the fallback when construct-level diagnostic
1955
- * evidence cannot be read. Wraps core's
1956
- * `createNodeReadFileLinesSync` with per-file memoization + 1-indexed line
1957
- * lookup; unreadable files / out-of-range lines return `null`.
1958
- */
1959
- const createSourceLineReader = (rootDirectory) => {
1960
- const readFileLines = createNodeReadFileLinesSync(rootDirectory);
1961
- const fileLinesCache = /* @__PURE__ */ new Map();
1962
- return (filePath, line) => {
1963
- if (!fileLinesCache.has(filePath)) fileLinesCache.set(filePath, readFileLines(filePath));
1964
- const lines = fileLinesCache.get(filePath) ?? null;
1965
- if (lines === null || line < 1 || line > lines.length) return null;
1966
- return lines[line - 1] ?? null;
1967
- };
2055
+ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorCacheDir(projectDirectory), SCAN_RESULT_CACHE_FILENAME);
2056
+ const readPersistedCache = (cacheFilePath) => {
2057
+ try {
2058
+ const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
2059
+ if (!isRecord(parsed) || parsed.version !== 7) return {
2060
+ version: 7,
2061
+ entries: []
2062
+ };
2063
+ if (!Array.isArray(parsed.entries)) return {
2064
+ version: 7,
2065
+ entries: []
2066
+ };
2067
+ const entries = [];
2068
+ for (const entry of parsed.entries) {
2069
+ if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
2070
+ const payload = decodeCachedScanPayload(entry.payload);
2071
+ if (payload === null) continue;
2072
+ entries.push({
2073
+ key: entry.key,
2074
+ createdAtMs: entry.createdAtMs,
2075
+ payload
2076
+ });
2077
+ }
2078
+ return {
2079
+ version: 7,
2080
+ entries
2081
+ };
2082
+ } catch {
2083
+ return {
2084
+ version: 7,
2085
+ entries: []
2086
+ };
2087
+ }
1968
2088
  };
1969
- //#endregion
1970
- //#region src/cli/utils/is-materializable-git-source.ts
1971
- const GIT_LFS_POINTER_HEADER = "version https://git-lfs.github.com/spec/v1";
1972
- const isMaterializableGitSource = (content) => !content.includes("\0") && !content.startsWith(GIT_LFS_POINTER_HEADER);
1973
- //#endregion
1974
- //#region src/cli/utils/materialize-baseline-files.ts
1975
- /**
1976
- * Materializes the base side of the `ref` → worktree diff into
1977
- * `tempDirectory`, mirroring the project layout plus head's config files so
1978
- * both sides lint under the same rules. Git rename heuristics are disabled by
1979
- * the diff planner, so an old path is retained as a base deletion while its
1980
- * new path is a head addition. Missing head-only additions are expected, and a
1981
- * base-only deletion that cannot be read has nothing at head to compare
1982
- * against; only a missing path that the plan says exists on both sides makes
1983
- * `isComplete` false.
1984
- */
1985
- const materializeBaselineFiles = (input) => runPromise(gen(function* () {
1986
- const git = yield* Git;
1987
- const diffPlan = yield* git.baselineDiffPlan({
1988
- directory: input.directory,
1989
- ref: input.ref
1990
- });
1991
- const baseFiles = input.baseFiles ?? diffPlan?.baseFiles;
1992
- const headFiles = input.headFiles ?? diffPlan?.headFiles;
1993
- const untrackedFiles = diffPlan?.untrackedFiles ?? [];
1994
- if (baseFiles === void 0 || headFiles === void 0) return null;
1995
- const files = [...new Set([...input.files, ...baseFiles])];
1996
- const tree = yield* materializeSourceTree({
1997
- directory: input.directory,
1998
- files,
1999
- tempDirectory: input.tempDirectory,
2000
- readContent: (relativePath) => git.showRefContent({
2001
- directory: input.directory,
2002
- ref: input.ref,
2003
- relativePath,
2004
- options: { maxBufferBytes: GIT_SHOW_MAX_BUFFER_BYTES }
2005
- }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
2006
- });
2007
- const materializedFiles = new Set(tree.materializedFiles);
2008
- const headFileSet = new Set(headFiles);
2009
- return {
2010
- ...tree,
2011
- baseFiles,
2012
- headFiles,
2013
- isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
2014
- untrackedFiles
2015
- };
2016
- }).pipe(provide(Git.layerNode)));
2017
2089
  /**
2018
- * Resolves the commit a baseline scan should read base content from — the
2019
- * merge-base of `ref` and HEAD, so "introduced" is measured against the branch
2020
- * point. `null` when unresolvable (ref missing/unsafe, no merge-base).
2090
+ * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
2091
+ * cache subsystem: this whole-repo scan cache plus core's per-file lint,
2092
+ * sidecar caches (their `Context.Reference` defaults read the
2093
+ * same variable). Exported for the wide event's `cache.temperature`
2094
+ * derivation, which reports `"disabled"` instead of `"cold"` when the switch
2095
+ * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
2096
+ * not consulted — they leave the other subsystems live.
2021
2097
  */
2022
- const resolveMergeBaseRef = async (directory, ref) => {
2098
+ const resolveProjectIdentity = (projectDirectory) => {
2099
+ try {
2100
+ return fs.realpathSync.native(projectDirectory);
2101
+ } catch {
2102
+ return path.resolve(projectDirectory);
2103
+ }
2104
+ };
2105
+ const resolveManifestContentHash = (projectDirectory) => hashFileContents(getPackageJsonPath(projectDirectory));
2106
+ const fileFingerprint = (filePath) => {
2023
2107
  try {
2024
- return await runPromise(gen(function* () {
2025
- return yield* (yield* Git).mergeBase({
2026
- directory,
2027
- ref
2028
- });
2029
- }).pipe(provide(Git.layerNode)));
2108
+ const stat = fs.statSync(filePath);
2109
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
2030
2110
  } catch {
2031
2111
  return null;
2032
2112
  }
2033
2113
  };
2034
- //#endregion
2035
- //#region src/cli/utils/has-unlinted-surviving-base-file.ts
2036
- const hasUnlintedSurvivingBaseFile = (input) => {
2037
- const analyzedBaseFiles = new Set(input.analyzedBaseFiles.map(toForwardSlashes));
2038
- return filterSourceFiles(input.baseLintPaths.map(toForwardSlashes)).some((filePath) => input.headFiles.has(filePath) && !analyzedBaseFiles.has(filePath));
2039
- };
2040
- //#endregion
2041
- //#region src/cli/utils/run-baseline-comparison.ts
2042
- const silentConsole$1 = makeNoopConsole();
2043
- const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
2044
- const runBaselineComparison = async (input) => {
2045
- const baselineIncludePaths = filterPathsOutsideDirectories({
2046
- rootDirectory: input.directory,
2047
- relativePaths: input.options.includePaths,
2048
- excludedDirectories: input.options.excludedProjectDirectories
2049
- });
2050
- const baselineBaseFiles = input.baseFiles ? filterPathsOutsideDirectories({
2051
- rootDirectory: input.directory,
2052
- relativePaths: input.baseFiles,
2053
- excludedDirectories: input.options.excludedProjectDirectories
2054
- }) : void 0;
2055
- const baselineHeadFiles = input.headFiles ? filterPathsOutsideDirectories({
2056
- rootDirectory: input.directory,
2057
- relativePaths: input.headFiles,
2058
- excludedDirectories: input.options.excludedProjectDirectories
2059
- }) : void 0;
2060
- const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
2061
- if (remainingBaselineBudgetMs === 0) return null;
2062
- const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
2063
- const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
2064
- let maintainabilitySourceFiles = [];
2065
- if (input.options.deadCode) try {
2066
- maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
2067
- } catch (error) {
2068
- if (baselineDeadlineSignal?.aborted) return null;
2069
- throw error;
2070
- }
2071
- const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
2072
- const snapshot = await materializeBaselineFiles({
2073
- directory: input.directory,
2074
- ref: input.baselineRef,
2075
- files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
2076
- baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
2077
- headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
2078
- tempDirectory: temporaryDirectory
2079
- }).catch((error) => {
2080
- rmSync(temporaryDirectory, {
2081
- recursive: true,
2082
- force: true
2083
- });
2084
- throw error;
2085
- });
2086
- if (snapshot === null) {
2087
- rmSync(temporaryDirectory, {
2088
- recursive: true,
2089
- force: true
2090
- });
2091
- return null;
2114
+ const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
2115
+ const fingerprints = [];
2116
+ if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
2117
+ for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
2118
+ const packageJson = bundledRequire(specifier);
2119
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
2120
+ fingerprints.push(`${specifier}=${version}`);
2121
+ } catch {
2122
+ fingerprints.push(`${specifier}=missing`);
2092
2123
  }
2093
- try {
2094
- if (!snapshot.isComplete) return null;
2095
- if (input.options.deadCode && !await copyUnchangedBaselineSources({
2096
- directory: input.directory,
2097
- sourceFiles: maintainabilitySourceFiles,
2098
- baseMaterializedFiles: snapshot.materializedFiles,
2099
- headChangedFiles: snapshot.headFiles,
2100
- untrackedFiles: snapshot.untrackedFiles,
2101
- tempDirectory: snapshot.tempDirectory,
2102
- deadlineEpochMs: input.deadlineEpochMs,
2103
- signal: input.oxlintRuntime.abortSignal
2104
- })) return null;
2105
- const filteredSnapshotBaseFiles = filterPathsOutsideDirectories({
2106
- rootDirectory: input.directory,
2107
- relativePaths: snapshot.baseFiles,
2108
- excludedDirectories: input.options.excludedProjectDirectories
2109
- });
2110
- const filteredSnapshotHeadFiles = filterPathsOutsideDirectories({
2111
- rootDirectory: input.directory,
2112
- relativePaths: snapshot.headFiles,
2113
- excludedDirectories: input.options.excludedProjectDirectories
2114
- });
2115
- const analyzedHeadFiles = new Set(input.headAnalyzedFiles.map(toForwardSlashes));
2116
- const baseFiles = new Set((baselineBaseFiles ?? filteredSnapshotBaseFiles).map(toForwardSlashes));
2117
- const expectedHeadFiles = new Set((baselineHeadFiles ?? filteredSnapshotHeadFiles).map(toForwardSlashes));
2118
- for (const filePath of baselineIncludePaths) {
2119
- const normalizedFilePath = toForwardSlashes(filePath);
2120
- if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
2121
- }
2122
- if (input.options.lint && filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
2123
- const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
2124
- const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
2125
- const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
2126
- const baselineExcludedProjectDirectories = input.options.excludedProjectDirectories.map((excludedDirectory) => path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.directory)).map((excludedDirectory) => path.resolve(snapshot.tempDirectory, path.relative(input.directory, excludedDirectory)));
2127
- const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
2128
- const runtimeLayers = buildRuntimeLayers({
2129
- directory: snapshot.tempDirectory,
2130
- hasConfigOverride: true,
2131
- userConfig: input.userConfig,
2132
- configSourceDirectory: input.configSourceDirectory,
2133
- projectInfoOverride: input.headProjectInfo,
2134
- shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
2135
- shouldRunDeadCode: input.options.deadCode,
2136
- shouldRunSupplyChain: input.options.supplyChain,
2137
- shouldComputeScore: false,
2138
- shouldShowProgressSpinners: false,
2139
- oxlintConcurrency: input.oxlintRuntime.concurrency,
2140
- oxlintSpawnSlots: input.oxlintRuntime.spawnSlots
2141
- });
2142
- const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
2143
- directory: snapshot.tempDirectory,
2144
- includePaths: baseIncludePaths,
2145
- maintainabilityFocusPaths,
2124
+ fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
2125
+ return fingerprints;
2126
+ };
2127
+ const buildScanResultCacheKey = async (input) => {
2128
+ if (isCacheGloballyDisabled()) return null;
2129
+ const isTrustworthyPromise = isGitIdentityTrustworthy(input.projectDirectory);
2130
+ const repositoryIdentityPromise = resolveRepositoryCacheIdentity(input.projectDirectory, input.invocationState, runGitAsync(input.projectDirectory, REPOSITORY_ROOT_GIT_ARGUMENTS));
2131
+ const [isTrustworthy, repositoryIdentity] = await Promise.all([isTrustworthyPromise, repositoryIdentityPromise]);
2132
+ if (!isTrustworthy || repositoryIdentity === null) return null;
2133
+ if (stringifyStableJson(input.userConfig) === null) return null;
2134
+ const cacheKeyJson = stringifyStableJson({
2135
+ schemaVersion: 7,
2136
+ projectIdentity: resolveProjectIdentity(input.projectDirectory),
2137
+ headSha: repositoryIdentity.headSha,
2138
+ worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
2139
+ dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
2140
+ reactDoctorVersion: input.version,
2141
+ nodeVersion: process.version,
2142
+ toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
2143
+ configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
2144
+ hasConfigOverride: input.hasConfigOverride,
2145
+ configSourceDirectory: input.configSourceDirectory,
2146
+ userConfig: input.userConfig,
2147
+ engineOptions: {
2148
+ lint: input.options.lint,
2149
+ deadCode: input.options.deadCode,
2150
+ supplyChain: input.options.supplyChain,
2151
+ includePaths: [...input.options.includePaths].sort(),
2152
+ excludedProjectDirectories: [...input.options.excludedProjectDirectories ?? []].sort(),
2153
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics ?? false,
2146
2154
  customRulesOnly: input.options.customRulesOnly,
2147
2155
  respectInlineDisables: input.options.respectInlineDisables,
2148
2156
  warnings: input.options.warnings,
2149
2157
  adoptExistingLintConfig: input.options.adoptExistingLintConfig,
2150
- ignoredTags: input.options.ignoredTags,
2151
- includedTags: input.options.includedTags,
2158
+ ignoredTags: [...input.options.ignoredTags].sort(),
2159
+ includedTags: [...input.options.includedTags].sort(),
2152
2160
  includeTagDefaults: input.options.includeTagDefaults,
2153
- nodeBinaryPath: input.resolvedNodeBinaryPath ?? void 0,
2154
- runDeadCode: input.options.deadCode,
2161
+ concurrency: input.options.concurrency,
2162
+ lintBatchOrdering: resolveLintBatchOrdering(),
2163
+ baselineRef: input.options.baseline?.ref,
2164
+ changedLineRanges: input.options.changedLineRanges ?? void 0,
2165
+ noScore: input.options.noScore,
2155
2166
  isCi: input.options.isCi,
2156
- doctorVersion: VERSION,
2157
- runId: getRunId(),
2158
- resolveLocalGithubViewerPermission: false,
2159
- suppressScanSummary: true,
2160
- supplyChainManifestChanged: input.options.supplyChainManifestChanged,
2161
- deadlineEpochMs: input.deadlineEpochMs ?? void 0,
2162
- signal: input.oxlintRuntime.abortSignal,
2163
- excludedProjectDirectories: baselineExcludedProjectDirectories,
2164
- retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
2165
- }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
2166
- if (baseOutput.didLintFail || baseOutput.didDeadCodeFail) return null;
2167
- if (countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0 && hasUnlintedSurvivingBaseFile({
2168
- baseLintPaths: materializedLintPaths,
2169
- headFiles: expectedHeadFiles,
2170
- analyzedBaseFiles: baseOutput.analyzedFiles
2171
- })) return null;
2172
- const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
2173
- rootDirectory: input.directory,
2174
- relativePaths: snapshot.untrackedFiles,
2175
- excludedDirectories: input.options.excludedProjectDirectories
2176
- }).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
2177
- const diagnosticDelta = computeDiagnosticDelta({
2178
- headDiagnostics: input.headDiagnostics,
2179
- baseDiagnostics: baseOutput.diagnostics,
2180
- readHeadLine: createSourceLineReader(input.directory),
2181
- readBaseLine: createSourceLineReader(snapshot.tempDirectory),
2182
- readHeadEvidence: createDiagnosticEvidenceReader(input.directory, { resolveForwardedHandlers: true }),
2183
- readBaseEvidence: createDiagnosticEvidenceReader(snapshot.tempDirectory)
2167
+ suppressRendering: input.options.suppressRendering,
2168
+ supplyChainManifestChanged: input.options.supplyChainManifestChanged
2169
+ }
2170
+ });
2171
+ return cacheKeyJson === null ? null : hashString(cacheKeyJson);
2172
+ };
2173
+ const createScanResultCache = (projectDirectory) => {
2174
+ const cacheFilePath = resolveCacheFilePath(projectDirectory);
2175
+ const persistedCache = readPersistedCache(cacheFilePath);
2176
+ const entries = /* @__PURE__ */ new Map();
2177
+ for (const entry of persistedCache.entries) entries.set(entry.key, entry);
2178
+ const persist = () => {
2179
+ const mergedEntries = /* @__PURE__ */ new Map();
2180
+ for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
2181
+ for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
2182
+ atomicWriteJson(cacheFilePath, {
2183
+ version: 7,
2184
+ entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
2184
2185
  });
2185
- return {
2186
- displayDiagnostics: diagnosticDelta.newDiagnostics,
2187
- baselineDelta: {
2188
- baseRef: input.baselineRef,
2189
- fixedCount: hasUnscannedUntrackedSourceFiles ? 0 : diagnosticDelta.fixedCount,
2190
- baseTotalCount: baseOutput.diagnostics.length,
2191
- crossFileMatchCount: diagnosticDelta.crossFileMatchCount
2192
- }
2193
- };
2194
- } finally {
2195
- snapshot.cleanup();
2196
- }
2186
+ };
2187
+ return {
2188
+ lookup: (key) => {
2189
+ const payload = entries.get(key)?.payload;
2190
+ if (payload === void 0) return null;
2191
+ return resolveProjectIdentity(payload.directory) === resolveProjectIdentity(projectDirectory) && (payload.manifestContentHash ?? null) === resolveManifestContentHash(projectDirectory) ? payload : null;
2192
+ },
2193
+ store: (key, payload) => {
2194
+ entries.set(key, {
2195
+ key,
2196
+ createdAtMs: Date.now(),
2197
+ payload: {
2198
+ ...payload,
2199
+ manifestContentHash: resolveManifestContentHash(projectDirectory)
2200
+ }
2201
+ });
2202
+ persist();
2203
+ }
2204
+ };
2197
2205
  };
2198
2206
  //#endregion
2199
2207
  //#region src/cli/utils/scan-result-cache-lifecycle.ts
2200
- const createScanResultCacheLifecycle = (input) => {
2201
- const cacheKey = buildScanResultCacheKey({
2208
+ const createScanResultCacheLifecycle = async (input) => {
2209
+ const cacheKey = await buildScanResultCacheKey({
2202
2210
  projectDirectory: input.directory,
2203
2211
  version: VERSION,
2204
2212
  nodeBinaryPath: input.resolvedNodeBinaryPath,
@@ -2318,7 +2326,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2318
2326
  const resolvedNodeBinaryPath = await resolveOxlintNode(options.lint, options.scoreOnly || options.silent);
2319
2327
  const lintBindingMissing = options.lint && !resolvedNodeBinaryPath;
2320
2328
  await yieldToEventLoop();
2321
- const scanResultCacheLifecycle = createScanResultCacheLifecycle({
2329
+ const scanResultCacheLifecycle = await createScanResultCacheLifecycle({
2322
2330
  directory,
2323
2331
  options,
2324
2332
  userConfig,
@@ -2332,6 +2340,10 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2332
2340
  const cachedResult = scanResultCacheLifecycle.replay();
2333
2341
  if (cachedResult !== null) return cachedResult;
2334
2342
  if (options.lint && resolvedNodeBinaryPath) warmOxlintWorkerPool(resolvedNodeBinaryPath, oxlintRuntime.concurrency);
2343
+ if (options.deadCode && shouldUseMaintainabilityLayer({
2344
+ shouldRunDuplicateJsx: options.deadCode,
2345
+ userConfig
2346
+ })) warmDuplicateJsxWorker();
2335
2347
  const shouldShowProgressSpinners = !options.isCiOrCodingAgentEnvironment && !options.silent && !options.scoreOnly && !options.suppressRendering && options.lint && Boolean(resolvedNodeBinaryPath);
2336
2348
  const layers = buildRuntimeLayers({
2337
2349
  directory,
@@ -2977,10 +2989,21 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
2977
2989
  };
2978
2990
  //#endregion
2979
2991
  //#region src/cli/utils/project-scan-outcome.ts
2980
- const partitionProjectScanOutcomes = (outcomes) => ({
2981
- completedScans: outcomes.flatMap((outcome) => outcome.status === "completed" ? [outcome.value] : []),
2982
- skippedScans: outcomes.flatMap((outcome) => outcome.status === "skipped" ? [outcome.value] : [])
2983
- });
2992
+ const partitionProjectScanOutcomes = (outcomes) => {
2993
+ const completedScans = [];
2994
+ const skippedScans = [];
2995
+ for (const outcome of outcomes) {
2996
+ if (outcome.status === "completed") {
2997
+ completedScans.push(outcome.value);
2998
+ continue;
2999
+ }
3000
+ if (outcome.status === "skipped") skippedScans.push(outcome.value);
3001
+ }
3002
+ return {
3003
+ completedScans,
3004
+ skippedScans
3005
+ };
3006
+ };
2984
3007
  //#endregion
2985
3008
  //#region src/cli/utils/project-manifest-changed.ts
2986
3009
  const PACKAGE_JSON = "package.json";
@@ -3048,5 +3071,5 @@ const shouldFailScanGate = (input) => {
3048
3071
  //#endregion
3049
3072
  export { createInvocationInspect as A, hasIncompleteScoreAnalysis as B, detectLaunchableAgents as C, buildHandoffPayload as D, launchCliAgent as E, formatSkippedCheckLabel as F, hasLintHardFailure as G, buildNoScoreMessage as H, formatElapsedTime as I, formatFixRecipeLine as J, buildRulePriorityMap as K, printFooter as L, resolveMergeBaseRef as M, printHeadlessReport as N, collectProjectSourceFiles as O, pluralize as P, printDiagnosticsDump as R, setUpGitHubActions as S, copyToClipboard as T, buildEmptyReportMessage as U, filterDiagnosticsByCategories as V, resolveInvocationOxlintConcurrency as W, formatLearnMoreLine as Y, retryMissingProjectScores as _, validateModeFlags as a, filterScansForSurface as b, partitionProjectScanOutcomes as c, buildCodeFrame as d, resolveProjectScan as f, resolveProjectRelativeDirectory as g, resolveProjectSourceFilePaths as h, validateIncludeUntrackedScope as i, inspect as j, deduplicateProjectScans as k, discoverWorkspacePackages as l, resolveProjectDiffIncludePaths as m, formatSkippedProjectsMessage as n, resolveExcludedProjectDirectories as o, resolveProjectChangedLineRanges as p, buildSortedRuleGroups as q, validateFilePathSelectionFlags as r, projectManifestChanged as s, shouldFailScanGate as t, selectProjects as u, resolveWorkspaceDeadCodeOwner as v, CLI_AGENT_BINARIES as w, countUniqueScannedFiles as x, resolveCliInspectOptions as y, formatDiagnosticSite as z };
3050
3073
 
3051
- //# sourceMappingURL=should-fail-scan-gate-uW8_hnTc.js.map
3052
- //# debugId=e73476ca-0e71-51dd-a00a-b07e28f2bb36
3074
+ //# sourceMappingURL=should-fail-scan-gate-2mIY6wHj.js.map
3075
+ //# debugId=7ed987a8-a8e5-5486-8e48-829258f26d80