react-doctor 0.9.12 → 0.9.13-dev.1239043

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 (52) hide show
  1. package/README.md +34 -1
  2. package/bin/react-doctor.js +1 -9
  3. package/dist/active-run-trace-dlIZeEvb.js +18 -0
  4. package/dist/active-scan-abort-registry-DwG6tSPy.js +28 -0
  5. package/dist/{build-section-divider-BsQvH-7I.js → build-section-divider-C09RcJrc.js} +10 -25
  6. package/dist/{ci-B4x4RjKD.js → ci-CUgNjCr-.js} +13 -12
  7. package/dist/cli.js +121 -41
  8. package/dist/{design-A48PNCf_.js → design-CX2j8ubS.js} +4 -4
  9. package/dist/{devtools-DPRVRu3S.js → devtools-CC2PAZ85.js} +4 -4
  10. package/dist/{exit-gracefully-Bk531kS-.js → exit-gracefully-cGJM2g3O.js} +15 -9
  11. package/dist/get-east-asian-width-Bv2TD3Xj.js +444 -0
  12. package/dist/{report-error-Pb2uO8EG.js → handle-error-BCI-Nv3P.js} +18 -68
  13. package/dist/index.d.ts +53 -42
  14. package/dist/index.js +55146 -54688
  15. package/dist/{inspect-CwH84aSA.js → inspect-Dq5krKWG.js} +47 -34
  16. package/dist/{install-sFN_w425.js → install-C0aiZKgf.js} +9 -8
  17. package/dist/{install-doctor-script-DLayXS7q.js → install-doctor-script-CaTIms0r.js} +7 -449
  18. package/dist/{install-github-workflow-Goz-KFqb.js → install-github-workflow-DjPgXVqg.js} +10 -6
  19. package/dist/{install-react-doctor-Bq7dfEBI.js → install-react-doctor-C-PVDWXv.js} +47 -45
  20. package/dist/{run-scan-app-DXBMW54J.js → jsx-runtime-D_A4d39v.js} +281 -2759
  21. package/dist/path-format-DnHpmJwG.js +9 -0
  22. package/dist/{print-branded-header-D_J8Qdsj.js → print-branded-header-Dz0SwyHQ.js} +4 -4
  23. package/dist/project-analysis-worker.d.ts +1 -0
  24. package/dist/project-analysis-worker.js +51905 -0
  25. package/dist/prompt-runtime-scan-url-BXdY2Opw.js +240 -0
  26. package/dist/{read-object-file-Hk79zI91.js → read-object-file-CmMGK1uu.js} +4 -4
  27. package/dist/{record-metric-CsQXJewH.js → record-metric-B6T_Ej33.js} +3285 -6018
  28. package/dist/report-error-DwL3b99q.js +64 -0
  29. package/dist/{report-workflow-result-6n_wSqzA.js → report-workflow-result-5PueDIAr.js} +5 -5
  30. package/dist/{resolve-scope-DP6rYlNO.js → resolve-scope-CTlLl7e6.js} +8 -10
  31. package/dist/{rules-BnIZ4Fq_.js → rules-DEbHKRag.js} +6 -6
  32. package/dist/run-scan-app-D3huvmXJ.js +2517 -0
  33. package/dist/runtime-scan/browser-probe.iife.js +855 -0
  34. package/dist/runtime-scan-CHFAEYg3.js +770 -0
  35. package/dist/sanitize-runtime-url-Bbq40iIp.js +76 -0
  36. package/dist/scan-C7sCm7wi.js +47 -0
  37. package/dist/semver-DFtnEmN-.js +3219 -0
  38. package/dist/{should-fail-scan-gate-DzZuTxim.js → should-fail-scan-gate-CfG_ppAV.js} +188 -187
  39. package/dist/{should-skip-prompts-D1GWG2qN.js → should-skip-prompts-BuNtAVEs.js} +4 -4
  40. package/dist/should-use-tui-B-lCjXVG.js +25 -0
  41. package/dist/skills/improve-threejs/SKILL.md +102 -0
  42. package/dist/skills/performance/SKILL.md +113 -0
  43. package/dist/skills/react-doctor/SKILL.md +6 -0
  44. package/dist/{unref-stdin-0a_0uwC6.js → telemetry-runtime-BF83aBFL.js} +57 -33
  45. package/dist/unref-stdin-SK3WfwxS.js +12 -0
  46. package/dist/{version-Bo5QDp7F.js → version-C27gr4Qj.js} +4 -4
  47. package/dist/{why-BFYbtnm8.js → why-CYRkZ2VY.js} +4 -4
  48. package/dist/with-run-span-CdsXSJgE.js +110 -0
  49. package/package.json +9 -10
  50. package/dist/chunk-q7NCDQ7-.js +0 -28
  51. package/dist/lsp.js +0 -81145
  52. package/dist/scan-CnnXz78m.js +0 -53
@@ -1,21 +1,25 @@
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]="00cf2dc8-65a5-523e-b3b1-8400324c9d87")}catch(e){}}();
3
- import { $n as orElseSucceed, An as resolveReactDoctorCacheDir, Bt as computeConfigFingerprint, Cn as mapWithConcurrency, Ct as SHARE_BASE_URL, Dt as SidecarLintCacheEnabled, E as NANOSECONDS_PER_MILLISECOND, Et as Score, Ft as buildSkippedChecks, G as isCiOrCodingAgentEnvironment, Gn as log, Gt as discoverReactSubprojects, H as ACTION_INPUT_ENVIRONMENT_VARIABLES, Hn as Console, Ht as createNodeReadFileLinesSync, It as calculateScore, Jn as decodeUnknownSync, Jt as filterSourceFiles, Kn as warn, Kt as filterDiagnosticsForSurface, Ln as summarizeDiagnostics, Lt as canonicalizeUserRuleKey, Mn as resolveScanTarget, Mt as atomicWriteJson, Nn as restoreLegacyThrow, On as resolveGithubActionsScoreMetadata, Pn as runInspect, Pt as buildRuleDocsUrl, Qn as map, R as CliInputError, St as Reporter, Tn as mergeReactDoctorConfigs, U as detectRunnerOs, Un as error, Ut as createOxlintSpawnSlots, Vn as yieldToEventLoop, Vt as computeDiagnosticDelta, W as isCiEnvironment, X as CI_URL, Xn as gen, Zn as makeSpan, _ as makeNoopConsole, _r as ParentSpan, _t as OxlintConcurrency, an as hasPublishedFixRecipe, ar as runSync, at as Files, bn as listSourceFiles, bt as Progress, c as setSentryProjectInfo, cn as highlighter, ct as Git, d as isJsonModeActive, dn as isFile, er as promise, et as Config, fn as isMonorepoRoot, fr as succeed, ft as LintPartialFailures, g as VERSION, gn as isReactDoctorError, gr as void_, gt as OXLINT_NODE_REQUIREMENT, hr as fail, ht as NodeResolver, in as groupBy, ir as runPromise, it as ENTERPRISE_CONTACT_URL, jn as resolveScanConcurrency, k as SCAN_RESULT_CACHE_FILENAME, kn as resolveLintBatchOrdering, kt as SupplyChain, l as getRunId, ln as isDirectory, lt as HTML_FILE_PATTERN, mt as MILLISECONDS_PER_SECOND, n as recordDistribution, nr as provideContext, o as buildSentryScope, or as sync, pt as Linter, qn as Diagnostic, qt as filterPathsOutsideDirectories, r as toSpanAttributes, rr as provideService, rt as DeadCode, s as buildSentryProjectContext, sn as hashFileContents, sr as try_, st as GIT_SHOW_MAX_BUFFER_BYTES, t as recordCount, tn as getDiagnosticRuleIdentity, tr as provide, tt as DIAGNOSTIC_CATEGORY_BUCKETS, ur as mergeAll, ut as JSX_FILE_PATTERN, vn as isScanComplete, vt as OxlintSpawnSlots, w as METRIC, wn as materializeSourceTree, x as BASELINE_FILES_TEMP_DIR_PREFIX, xn as listWorkspacePackages, xt as Project, yt as PerFileLintCacheEnabled, z as isNonInteractiveEnvironment, zt as collectSourceFileCountsByDirectory } from "./record-metric-CsQXJewH.js";
4
- import { a as setActiveRunTrace, i as recordRunTraceId, o as getTelemetryContext } from "./unref-stdin-0a_0uwC6.js";
5
- import { n as buildFooterLinkLines, r as activeScanAbortRegistry, t as buildSectionDivider } from "./build-section-divider-BsQvH-7I.js";
6
- import { D as getPackageJsonPath, L as cliLogger, O as isRecord, P as runGit, a as installReactDoctorScriptStep, c as spinner, n as findNearestPackageDirectory, o as isSpinnerSilent, s as setSpinnerSilent } from "./install-doctor-script-DLayXS7q.js";
7
- import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-Goz-KFqb.js";
8
- import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel, u as toForwardSlashes } from "./resolve-scope-DP6rYlNO.js";
9
- import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-6n_wSqzA.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]="159aa887-619a-5d42-9d32-79fce37783d6")}catch(e){}}();
3
+ import { $ as Linter, A as SCAN_RESULT_CACHE_FILENAME, An as gen, At as discoverSupportedSubprojects, B as isNonInteractiveEnvironment, Bn as sync, Cn as error, Ct as collectSourceFileCountsByDirectory, Dn as Diagnostic, Dt as createNodeReadFileLinesSync, En as warn, Et as countOptInProjectRuleSelections, Fn as provide, G as isCiEnvironment, Gt as hashFileContents, Ht as groupBy, In as provideContext, J as Config, Jt as isPathInsideDirectory, K as isCiOrCodingAgentEnvironment, Kn as succeed, Kt as highlighter, Ln as provideService, Mn as map, Mt as filterPathsOutsideDirectories, Nn as orElseSucceed, Nt as filterSourceFiles, On as decodeUnknownSync, Ot as createOxlintSpawnSlots, Pn as promise, Q as LintPartialFailures, Qt as isScanComplete, Rn as runPromise, S as BASELINE_FILES_TEMP_DIR_PREFIX, Sn as Console, T as METRIC, Tn as log, Tt as computeDiagnosticDelta, U as ACTION_INPUT_ENVIRONMENT_VARIABLES, Ut as hasPublishedFixRecipe, Vn as try_, W as detectRunnerOs, Wn as mergeAll, X as Git, Xt as isReactDoctorError, Y as Files, Zn as ParentSpan, _ as VERSION, _n as shouldUseMaintainabilityLayer, _t as buildRuleSeverityControls, an as materializeSourceTree, ar as DIAGNOSTIC_CATEGORY_BUCKETS, at as Progress, bt as canonicalizeUserRuleKey, cn as resolveGithubActionsScoreMetadata, ct as Score, d as isJsonModeActive, dn as resolveScanConcurrency, dr as JSX_FILE_PATTERN, dt as SupplyChain, en as listSourceFiles, et as Maintainability, fn as resolveScanTarget, fr as MAINTAINABILITY_SOURCE_FILE_PATTERN, gt as buildRuleDocsUrl, hr as SHARE_BASE_URL, in as mapWithConcurrency, it as PerFileLintCacheEnabled, jt as filterDiagnosticsForSurface, l as getRunId, ln as resolveLintBatchOrdering, lr as GIT_SHOW_MAX_BUFFER_BYTES, lt as SidecarLintCacheEnabled, mn as runInspect, mr as OXLINT_NODE_REQUIREMENT, mt as atomicWriteJson, n as recordDistribution, nn as listWorkspacePackages, nt as OxlintConcurrency, on as mergeReactDoctorConfigs, ot as Project, pn as restoreLegacyThrow, pr as MILLISECONDS_PER_SECOND, qt as isMonorepoRoot, r as toSpanAttributes, rt as OxlintSpawnSlots, sn as remainingDeadlineBudgetMs, sr as ENTERPRISE_CONTACT_URL, st as Reporter, t as recordCount, tn as listSourceFilesCooperative, tr as CI_URL, tt as NodeResolver, un as resolveReactDoctorCacheDir, ur as HTML_FILE_PATTERN, v as makeNoopConsole, vn as summarizeDiagnostics, vt as buildSkippedChecks, wt as computeConfigFingerprint, xn as yieldToEventLoop, xr as isFile, xt as classifyFileContext, yr as isDirectory, yt as calculateScore, z as CliInputError, zn as runSync, zt as getDiagnosticRuleIdentity } from "./record-metric-B6T_Ej33.js";
4
+ import { t as getTelemetryContext } from "./telemetry-runtime-BF83aBFL.js";
5
+ import { n as buildFooterLinkLines, t as buildSectionDivider } from "./build-section-divider-C09RcJrc.js";
6
+ import { t as activeScanAbortRegistry } from "./active-scan-abort-registry-DwG6tSPy.js";
7
+ import { t as toForwardSlashes } from "./path-format-DnHpmJwG.js";
8
+ 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-CaTIms0r.js";
9
+ import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-DjPgXVqg.js";
10
+ import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel } from "./resolve-scope-CTlLl7e6.js";
11
+ import { n as resetSentryRunState, r as withRunSpan, t as recordSentryProjectContext } from "./with-run-span-CdsXSJgE.js";
12
+ import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-5PueDIAr.js";
10
13
  import { createRequire } from "node:module";
11
14
  import { spawn } from "node:child_process";
12
15
  import * as path$1 from "node:path";
13
16
  import path from "node:path";
14
- import * as fs$1 from "node:fs";
17
+ import * as fs$2 from "node:fs";
15
18
  import fs, { mkdtempSync, rmSync } from "node:fs";
16
- import crypto, { randomUUID } from "node:crypto";
17
19
  import ts from "typescript";
20
+ import crypto, { randomUUID } from "node:crypto";
18
21
  import { tmpdir } from "node:os";
22
+ import * as fs$1 from "node:fs/promises";
19
23
  import { performance } from "node:perf_hooks";
20
24
  //#region src/cli/utils/apply-observability.ts
21
25
  /**
@@ -83,7 +87,10 @@ const buildSpinnerProgressHandle = (text) => {
83
87
  */
84
88
  const buildRuntimeLayers = (input) => {
85
89
  const linterLayer = input.shouldSkipLint ? Linter.layerOf([]) : Linter.layerOxlint;
86
- const deadCodeLayer = input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]);
90
+ const maintainabilityLayer = shouldUseMaintainabilityLayer({
91
+ shouldRunDuplicateJsx: input.shouldRunDeadCode,
92
+ userConfig: input.userConfig
93
+ }) ? Maintainability.layerNode : Maintainability.layerOf([]);
87
94
  const scoreLayer = input.shouldComputeScore ? Score.layerHttp : Score.layerOf(null);
88
95
  const supplyChainLayer = input.shouldRunSupplyChain ? SupplyChain.layerNode : SupplyChain.layerOf([]);
89
96
  const projectLayer = input.projectInfoOverride === void 0 ? Project.layerNode : Project.layerOf(input.projectInfoOverride);
@@ -93,112 +100,11 @@ const buildRuntimeLayers = (input) => {
93
100
  config: input.userConfig,
94
101
  resolvedDirectory: input.directory,
95
102
  configSourceDirectory: input.configSourceDirectory
96
- }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, deadCodeLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
103
+ }) : Config.layerNode, Files.layerNode, Git.layerNode, linterLayer, LintPartialFailures.layerLive, maintainabilityLayer, progressLayer, reporterLayer, scoreLayer, supplyChainLayer);
97
104
  const layersWithConcurrency = input.oxlintConcurrency === void 0 ? baseLayers : mergeAll(baseLayers, succeed(OxlintConcurrency, input.oxlintConcurrency));
98
105
  return input.oxlintSpawnSlots === void 0 ? layersWithConcurrency : mergeAll(layersWithConcurrency, succeed(OxlintSpawnSlots, input.oxlintSpawnSlots));
99
106
  };
100
107
  //#endregion
101
- //#region src/cli/utils/with-run-span.ts
102
- /**
103
- * Ends the run span without letting a telemetry failure change the run's
104
- * outcome — the invariant every other emit site here already holds.
105
- *
106
- * The failure path is the reason this matters most: a throw from `end` there
107
- * would replace the user's original scan error, so a project-config mistake
108
- * could surface as an exporter stack trace, be reclassified as a crash, and get
109
- * reported as one.
110
- */
111
- const endSpanSafely = (span, exit) => {
112
- try {
113
- span.end(BigInt(Date.now()) * NANOSECONDS_PER_MILLISECOND, exit);
114
- } catch {}
115
- };
116
- /**
117
- * Clears the module-level run-scoped telemetry state — the current scanned
118
- * project and the active run trace. `inspect()` calls this at the start of every
119
- * run and again after a clean one, so a prior or just-finished scan can't attach
120
- * its project tags / trace to a later run or to a non-scan error (e.g.
121
- * inspectAction's post-loop finalize/handoff steps). A thrown scan error skips
122
- * the post-run reset, leaving the state for the command catch to attribute and
123
- * link the crash. Concurrent batch members (`concurrentScan`) never touch this
124
- * state — they neither write nor reset it. Safe to call when telemetry is off
125
- * (the refs are read only when an event is built).
126
- */
127
- const resetSentryRunState = () => {
128
- setSentryProjectInfo(null);
129
- setActiveRunTrace(null);
130
- };
131
- /**
132
- * Runs an inspect invocation inside a root span so each `react-doctor` run is a
133
- * first-class trace with timing and the run snapshot as attributes. The span is
134
- * handed to `run` so the scan's Effect spans parent under it and so the wide
135
- * event can be stamped onto it once the outcome is known.
136
- *
137
- * A no-op pass-through when telemetry is off (`--no-telemetry`, tests, or an
138
- * unconfigured build) — `run` receives `undefined` and no span is created, so
139
- * there's no added exit latency.
140
- *
141
- * The span is created against the shared telemetry context rather than a
142
- * freshly-provided layer, so it lives in the same tracer as the scan program's
143
- * spans; see `telemetry-runtime.ts` for why that matters. It is started and
144
- * ended manually because the body it wraps is a promise-returning imperative
145
- * function, not an Effect — the same shape the Sentry inactive span had.
146
- *
147
- * While the span runs, its trace context is recorded as the active run trace so
148
- * `reportErrorToSentry` can attach a crash thrown during the scan back to this
149
- * trace (errors surface in the command catch, after the span has ended).
150
- *
151
- * A `concurrentScan` (one member of the CLI's multi-project pool) still gets its
152
- * own root span, but skips recording the active run trace — the module-level
153
- * handle has single-scan semantics, and overlapping writers would link a crash
154
- * to an arbitrary sibling's trace.
155
- */
156
- const withRunSpan = async (run, options = {}) => {
157
- const telemetryContext = getTelemetryContext();
158
- if (telemetryContext === null) return run(void 0);
159
- const { tags } = buildSentryScope();
160
- const command = typeof tags.command === "string" ? tags.command : "inspect";
161
- let rootSpan;
162
- try {
163
- rootSpan = runSync(makeSpan(`react-doctor ${command}`, { attributes: toSpanAttributes(tags) }).pipe(provideContext(telemetryContext)));
164
- } catch {
165
- return run(void 0);
166
- }
167
- recordRunTraceId(rootSpan.traceId);
168
- if (options.concurrentScan !== true) setActiveRunTrace({
169
- traceId: rootSpan.traceId,
170
- spanId: rootSpan.spanId,
171
- sampled: rootSpan.sampled
172
- });
173
- try {
174
- const result = await run(rootSpan);
175
- endSpanSafely(rootSpan, void_);
176
- return result;
177
- } catch (error) {
178
- endSpanSafely(rootSpan, fail(error));
179
- throw error;
180
- }
181
- };
182
- /**
183
- * Records the scanned project (discovered in the `beforeLint` hook): remembers
184
- * it for the lazy Sentry error-capture path (`buildSentryScope` folds it into
185
- * exception events) and for per-emit metric attributes, and sets it on the run's
186
- * root span so the trace carries the project shape too. Always cheap — the span
187
- * attributes are skipped when `rootSpan` is absent (telemetry off), and storing
188
- * the info is a plain assignment.
189
- *
190
- * A `concurrentScan` only sets the span attributes: the module-level project ref
191
- * has single-scan semantics, and overlapping writers would stamp events and
192
- * metrics with an arbitrary sibling's project. Wide events keep full attribution
193
- * (they ride the span); per-emit metrics simply omit the project shape during a
194
- * concurrent batch (absent, never wrong).
195
- */
196
- const recordSentryProjectContext = (projectInfo, rootSpan, options = {}) => {
197
- if (options.concurrentScan !== true) setSentryProjectInfo(projectInfo);
198
- if (rootSpan === void 0) return;
199
- for (const [key, value] of Object.entries(toSpanAttributes(buildSentryProjectContext(projectInfo).tags))) rootSpan.attribute(key, value);
200
- };
201
- //#endregion
202
108
  //#region src/cli/utils/diagnostic-grouping.ts
203
109
  const buildRulePriorityMap = (scores) => {
204
110
  const rulePriority = /* @__PURE__ */ new Map();
@@ -309,7 +215,7 @@ const recordScanMetrics = (input) => {
309
215
  recordCount(METRIC.scanCompleted, 1, {
310
216
  mode: input.mode,
311
217
  lint: input.lint,
312
- deadCode: input.deadCode,
218
+ maintainability: input.deadCode,
313
219
  parallel: input.parallel,
314
220
  scoreOnly: input.scoreOnly,
315
221
  didLintFail: input.didLintFail,
@@ -349,7 +255,7 @@ const recordScanMetrics = (input) => {
349
255
  else if (!input.noScore && !input.didLintFail && !input.didDeadCodeFail) recordCount(METRIC.scoreUnavailable, 1, { mode: input.mode });
350
256
  if (input.baselineDegraded) recordCount(METRIC.baselineDegraded, 1);
351
257
  if (input.didLintFail) recordCount(METRIC.lintFailed, 1, { reasonKind: input.lintFailureReasonKind });
352
- if (input.didDeadCodeFail) recordCount(METRIC.deadCodeFailed, 1);
258
+ if (input.didDeadCodeFail) recordCount(METRIC.maintainabilityFailed, 1);
353
259
  for (const check of result.skippedChecks) recordCount(METRIC.scanCheckSkipped, 1, {
354
260
  check,
355
261
  reason: result.skippedCheckReasons?.[check] ?? null
@@ -493,10 +399,16 @@ const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
493
399
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
494
400
  "oxlint/package.json",
495
401
  "oxlint-plugin-react-doctor/package.json",
496
- "deslop-js/package.json",
497
402
  "eslint-plugin-react-hooks/package.json"
498
403
  ];
499
404
  const bundledRequire = createRequire(import.meta.url);
405
+ const RULE_PLUGIN_CONTENT_FINGERPRINT = (() => {
406
+ try {
407
+ return `oxlint-plugin-react-doctor#fingerprint=${hashFileContents(bundledRequire.resolve("oxlint-plugin-react-doctor")) ?? "unreadable"}`;
408
+ } catch {
409
+ return "oxlint-plugin-react-doctor#fingerprint=unresolved";
410
+ }
411
+ })();
500
412
  const normalizeForStableJson = (value) => {
501
413
  if (value === null) return null;
502
414
  if (value === void 0) return void 0;
@@ -632,12 +544,12 @@ const resolveCacheFilePath = (projectDirectory) => path.join(resolveReactDoctorC
632
544
  const readPersistedCache = (cacheFilePath) => {
633
545
  try {
634
546
  const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
635
- if (!isRecord(parsed) || parsed.version !== 6) return {
636
- version: 6,
547
+ if (!isRecord(parsed) || parsed.version !== 7) return {
548
+ version: 7,
637
549
  entries: []
638
550
  };
639
551
  if (!Array.isArray(parsed.entries)) return {
640
- version: 6,
552
+ version: 7,
641
553
  entries: []
642
554
  };
643
555
  const entries = [];
@@ -652,12 +564,12 @@ const readPersistedCache = (cacheFilePath) => {
652
564
  });
653
565
  }
654
566
  return {
655
- version: 6,
567
+ version: 7,
656
568
  entries
657
569
  };
658
570
  } catch {
659
571
  return {
660
- version: 6,
572
+ version: 7,
661
573
  entries: []
662
574
  };
663
575
  }
@@ -665,7 +577,7 @@ const readPersistedCache = (cacheFilePath) => {
665
577
  /**
666
578
  * The global cache off-switch (`REACT_DOCTOR_NO_CACHE`), which disables every
667
579
  * cache subsystem: this whole-repo scan cache plus core's per-file lint,
668
- * sidecar, and dead-code caches (their `Context.Reference` defaults read the
580
+ * sidecar caches (their `Context.Reference` defaults read the
669
581
  * same variable). Exported for the wide event's `cache.temperature`
670
582
  * derivation, which reports `"disabled"` instead of `"cold"` when the switch
671
583
  * is on. Granular knobs (`REACT_DOCTOR_NO_FILE_CACHE`, …) are deliberately
@@ -688,7 +600,7 @@ const fileFingerprint = (filePath) => {
688
600
  return null;
689
601
  }
690
602
  };
691
- const resolveToolchainFingerprint = (nodeBinaryPath) => {
603
+ const resolveScanResultToolchainFingerprint = (nodeBinaryPath) => {
692
604
  const fingerprints = [];
693
605
  if (nodeBinaryPath !== null) fingerprints.push(nodeBinaryPath === process.execPath ? `node=${process.version}` : `node=${fileFingerprint(nodeBinaryPath) ?? "unreadable"}`);
694
606
  for (const specifier of TOOLCHAIN_PACKAGE_SPECIFIERS) try {
@@ -698,6 +610,7 @@ const resolveToolchainFingerprint = (nodeBinaryPath) => {
698
610
  } catch {
699
611
  fingerprints.push(`${specifier}=missing`);
700
612
  }
613
+ fingerprints.push(RULE_PLUGIN_CONTENT_FINGERPRINT);
701
614
  return fingerprints;
702
615
  };
703
616
  const buildScanResultCacheKey = (input) => {
@@ -707,14 +620,14 @@ const buildScanResultCacheKey = (input) => {
707
620
  if (repositoryIdentity === null) return null;
708
621
  if (stringifyStableJson(input.userConfig) === null) return null;
709
622
  const cacheKeyJson = stringifyStableJson({
710
- schemaVersion: 6,
623
+ schemaVersion: 7,
711
624
  projectIdentity: resolveProjectIdentity(input.projectDirectory),
712
625
  headSha: repositoryIdentity.headSha,
713
626
  worktreeFingerprint: repositoryIdentity.worktreeFingerprint,
714
627
  dotenvFingerprint: resolveDotenvFingerprint(input.projectDirectory),
715
628
  reactDoctorVersion: input.version,
716
629
  nodeVersion: process.version,
717
- toolchainFingerprint: resolveToolchainFingerprint(input.nodeBinaryPath),
630
+ toolchainFingerprint: resolveScanResultToolchainFingerprint(input.nodeBinaryPath),
718
631
  configFingerprint: computeConfigFingerprint(input.projectDirectory, input.version),
719
632
  hasConfigOverride: input.hasConfigOverride,
720
633
  configSourceDirectory: input.configSourceDirectory,
@@ -755,7 +668,7 @@ const createScanResultCache = (projectDirectory) => {
755
668
  for (const entry of readPersistedCache(cacheFilePath).entries) mergedEntries.set(entry.key, entry);
756
669
  for (const entry of entries.values()) mergedEntries.set(entry.key, entry);
757
670
  atomicWriteJson(cacheFilePath, {
758
- version: 6,
671
+ version: 7,
759
672
  entries: [...mergedEntries.values()].sort((firstEntry, secondEntry) => secondEntry.createdAtMs - firstEntry.createdAtMs).slice(0, 20)
760
673
  });
761
674
  };
@@ -790,8 +703,7 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
790
703
  /**
791
704
  * Lowercase, key-safe form of a rule category for the `diag.category.*`
792
705
  * telemetry attribute namespace (categories carry spaces / capitals, e.g.
793
- * "Performance" → `performance`, "Dead Code" → `dead_code`). Shared by the
794
- * CLI run event and the editor LSP wide event so both namespace identically.
706
+ * "Performance" → `performance`, "Dead Code" → `dead_code`).
795
707
  */
796
708
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
797
709
  //#endregion
@@ -821,12 +733,6 @@ const readEnvBoolean = (name) => {
821
733
  return value.toLowerCase() === "true" || value === "1";
822
734
  };
823
735
  const ratioOf = (numerator, denominator) => denominator != null && denominator > 0 ? (numerator ?? 0) / denominator : null;
824
- const resolveDeadCodeReuseRatio = (result) => {
825
- if (result.deadCodeCacheHit === true) return 1;
826
- const summaryTotal = (result.deadCodeSummaryCacheHits ?? 0) + (result.deadCodeSummaryCacheMisses ?? 0);
827
- if (summaryTotal > 0) return (result.deadCodeSummaryCacheHits ?? 0) / summaryTotal;
828
- return result.deadCodeCacheHit === false ? 0 : null;
829
- };
830
736
  /**
831
737
  * One queryable cache temperature per scan, derived from the whole stack:
832
738
  *
@@ -834,8 +740,7 @@ const resolveDeadCodeReuseRatio = (result) => {
834
740
  * `wholeRepoCacheHit` flag from the CLI's cachedPayload
835
741
  * branch; no scan work ran).
836
742
  * - `"warm"` — any incremental reuse: per-file lint hits, sidecar
837
- * replays, a dead-code whole-result hit, or dead-code
838
- * summary-cache hits.
743
+ * replays.
839
744
  * - `"disabled"` — zero reuse because the global `REACT_DOCTOR_NO_CACHE`
840
745
  * off-switch is on. Granular per-cache opt-outs
841
746
  * (`REACT_DOCTOR_NO_FILE_CACHE`, …) still read warm/cold,
@@ -844,7 +749,7 @@ const resolveDeadCodeReuseRatio = (result) => {
844
749
  *
845
750
  * `cache.warmth` is the headline reuse magnitude in [0, 1]: the plain mean of
846
751
  * the subsystem reuse fractions known this run (lint hit ratio, sidecar
847
- * replay ratio, dead-code reuse), skipping subsystems that never consulted a
752
+ * replay ratio), skipping subsystems that never consulted a
848
753
  * cache; `1` on turbo, dropped when nothing consulted any cache. Deliberately
849
754
  * unweighted — the per-subsystem dims stay the precise signal; warmth is the
850
755
  * p50/p90-able summary. Emitted only on the success path.
@@ -857,11 +762,7 @@ const buildCacheAttributes = (input) => {
857
762
  warmth: 1
858
763
  });
859
764
  const result = input.result;
860
- const subsystemReuseRatios = [
861
- ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount),
862
- ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount),
863
- resolveDeadCodeReuseRatio(result)
864
- ];
765
+ const subsystemReuseRatios = [ratioOf(result.lintCacheHitFileCount, result.lintCacheTotalFileCount), ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)];
865
766
  let knownSubsystemCount = 0;
866
767
  let reuseRatioSum = 0;
867
768
  for (const reuseRatio of subsystemReuseRatios) {
@@ -1013,13 +914,7 @@ const buildOutcomeAttributes = (input) => {
1013
914
  sidecarTotalFiles: result.lintSidecarTotalFileCount,
1014
915
  sidecarReplayRatio: ratioOf(result.lintSidecarReplayedFileCount, result.lintSidecarTotalFileCount)
1015
916
  }),
1016
- ...withNamespace("deadCode", {
1017
- failed: input.didDeadCodeFail,
1018
- overlapped: input.deadCodeOverlapped,
1019
- cacheHit: result.deadCodeCacheHit,
1020
- summaryCacheHits: result.deadCodeSummaryCacheHits,
1021
- summaryCacheMisses: result.deadCodeSummaryCacheMisses
1022
- }),
917
+ ...withNamespace("maintainability", { failed: input.didDeadCodeFail }),
1023
918
  ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut }),
1024
919
  ...withNamespace("securityScan", { failed: input.securityScanFailed }),
1025
920
  ...withNamespace("timing", {
@@ -1062,7 +957,7 @@ const buildScanAttributes = (input) => {
1062
957
  workerCount: input.workerCount,
1063
958
  maxDurationMs: input.maxDurationMs,
1064
959
  lint: input.lint,
1065
- deadCode: input.deadCode,
960
+ maintainability: input.deadCode,
1066
961
  supplyChain: input.supplyChain,
1067
962
  scoreOnly: input.scoreOnly,
1068
963
  noScore: input.noScore,
@@ -1073,6 +968,7 @@ const buildScanAttributes = (input) => {
1073
968
  hasCustomConfig: input.hasCustomConfig,
1074
969
  rulesConfigured: ruleKeys.length,
1075
970
  rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
971
+ projectAnalysisRuleCount: countOptInProjectRuleSelections(buildRuleSeverityControls(input.userConfig)),
1076
972
  fileCount: input.result?.scannedFileCount,
1077
973
  nonJsxFileCount: input.result?.analyzedFiles?.filter((filePath) => !JSX_FILE_PATTERN.test(filePath)).length,
1078
974
  htmlFileCount: input.result?.analyzedFiles?.filter((filePath) => HTML_FILE_PATTERN.test(filePath)).length,
@@ -1083,7 +979,7 @@ const buildScanAttributes = (input) => {
1083
979
  * Projects a scan into the namespaced attribute set for its root span — the
1084
980
  * canonical per-scan "wide event". Every attribute carries a dotted namespace
1085
981
  * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
1086
- * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`, `cache.*`,
982
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `maintainability.*`, `cache.*`,
1087
983
  * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
1088
984
  * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
1089
985
  * in the Spans dataset. Pure and exported so the projection (outcome
@@ -1322,7 +1218,7 @@ const buildNoScoreMessage = (input) => {
1322
1218
  reason = input.disabledMessage ?? "Score disabled by --no-score.";
1323
1219
  break;
1324
1220
  case "analysis-incomplete":
1325
- reason = "Score not shown because lint or dead-code analysis could not complete.";
1221
+ reason = "Score not shown because lint or maintainability analysis could not complete.";
1326
1222
  break;
1327
1223
  case "api-unavailable":
1328
1224
  reason = "Score unavailable (could not reach the score API).";
@@ -1370,7 +1266,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
1370
1266
  const ruleDumpFileName = (ruleKey) => ruleKey.replace(/\//g, "--") + ".txt";
1371
1267
  const readPreviousRuleDumpFileNames = (directory) => {
1372
1268
  try {
1373
- const previous = JSON.parse(fs$1.readFileSync(path$1.join(directory, "diagnostics.json"), "utf8"));
1269
+ const previous = JSON.parse(fs$2.readFileSync(path$1.join(directory, "diagnostics.json"), "utf8"));
1374
1270
  return new Set(previous.filter((d) => typeof d?.plugin === "string" && typeof d?.rule === "string").map((d) => ruleDumpFileName(`${d.plugin}/${d.rule}`)));
1375
1271
  } catch {
1376
1272
  return /* @__PURE__ */ new Set();
@@ -1378,10 +1274,10 @@ const readPreviousRuleDumpFileNames = (directory) => {
1378
1274
  };
1379
1275
  const writeDiagnosticsDirectory = (diagnostics, outputDirectory) => {
1380
1276
  const directory = outputDirectory ? path$1.resolve(outputDirectory) : path$1.join(tmpdir(), `react-doctor-${randomUUID()}`);
1381
- if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) fs$1.rmSync(path$1.join(directory, fileName), { force: true });
1382
- fs$1.mkdirSync(directory, { recursive: true });
1383
- for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) fs$1.writeFileSync(path$1.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
1384
- fs$1.writeFileSync(path$1.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
1277
+ if (outputDirectory) for (const fileName of readPreviousRuleDumpFileNames(directory)) fs$2.rmSync(path$1.join(directory, fileName), { force: true });
1278
+ fs$2.mkdirSync(directory, { recursive: true });
1279
+ for (const [ruleKey, ruleDiagnostics] of buildSortedRuleGroups(diagnostics)) fs$2.writeFileSync(path$1.join(directory, ruleDumpFileName(ruleKey)), formatRuleSummary(ruleKey, ruleDiagnostics));
1280
+ fs$2.writeFileSync(path$1.join(directory, "diagnostics.json"), JSON.stringify(diagnostics));
1385
1281
  return directory;
1386
1282
  };
1387
1283
  //#endregion
@@ -1425,6 +1321,9 @@ const formatElapsedTime = (elapsedMilliseconds) => {
1425
1321
  return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
1426
1322
  };
1427
1323
  //#endregion
1324
+ //#region src/cli/utils/format-skipped-check-label.ts
1325
+ const formatSkippedCheckLabel = (skippedCheck) => skippedCheck === "dead-code" ? "maintainability" : skippedCheck;
1326
+ //#endregion
1428
1327
  //#region src/cli/utils/pluralize.ts
1429
1328
  const pluralize = (count, singularNoun) => `${count} ${singularNoun}${count === 1 ? "" : "s"}`;
1430
1329
  //#endregion
@@ -1473,7 +1372,7 @@ const printHeadlessReport = (input) => gen(function* () {
1473
1372
  }
1474
1373
  if (input.skippedChecks.length > 0) {
1475
1374
  yield* log("");
1476
- yield* warn(highlighter.warn(`Results are incomplete: ${input.skippedChecks.join(" and ")} checks failed.`));
1375
+ yield* warn(highlighter.warn(`Results are incomplete: ${input.skippedChecks.map(formatSkippedCheckLabel).join(" and ")} checks failed.`));
1477
1376
  }
1478
1377
  });
1479
1378
  //#endregion
@@ -1673,12 +1572,39 @@ const renderAndRecordScan = async (input) => {
1673
1572
  didDeadCodeFail: input.payload.didDeadCodeFail,
1674
1573
  supplyChainOverlapTimedOut: input.payload.supplyChainOverlapTimedOut,
1675
1574
  securityScanFailed: input.payload.securityScanFailed,
1676
- deadCodeOverlapped: input.payload.deadCodeOverlapped,
1677
1575
  suppressedRuleCounts: input.payload.suppressedRuleCounts ?? []
1678
1576
  });
1679
1577
  return result;
1680
1578
  };
1681
1579
  //#endregion
1580
+ //#region src/cli/utils/copy-unchanged-baseline-sources.ts
1581
+ const copyUnchangedBaselineSources = async (input) => {
1582
+ const baseMaterializedFiles = new Set(input.baseMaterializedFiles.map(toForwardSlashes));
1583
+ const headChangedFiles = new Set(input.headChangedFiles.map(toForwardSlashes));
1584
+ const untrackedFiles = new Set(input.untrackedFiles.map(toForwardSlashes));
1585
+ const sourceDirectory = path$1.resolve(input.directory);
1586
+ const targetDirectory = path$1.resolve(input.tempDirectory);
1587
+ return (await mapWithConcurrency(input.sourceFiles.filter((filePath) => {
1588
+ const normalizedPath = toForwardSlashes(filePath);
1589
+ return !baseMaterializedFiles.has(normalizedPath) && !headChangedFiles.has(normalizedPath) && !untrackedFiles.has(normalizedPath);
1590
+ }), 32, async (filePath) => {
1591
+ input.signal?.throwIfAborted();
1592
+ if (input.deadlineEpochMs !== null && Date.now() >= input.deadlineEpochMs) return false;
1593
+ const sourcePath = path$1.resolve(sourceDirectory, filePath);
1594
+ const targetPath = path$1.resolve(targetDirectory, filePath);
1595
+ if (!isPathInsideDirectory(sourcePath, sourceDirectory) || !isPathInsideDirectory(targetPath, targetDirectory)) return false;
1596
+ try {
1597
+ if (!(await fs$1.lstat(sourcePath)).isFile()) return false;
1598
+ await fs$1.mkdir(path$1.dirname(targetPath), { recursive: true });
1599
+ await fs$1.copyFile(sourcePath, targetPath);
1600
+ return true;
1601
+ } catch {
1602
+ input.signal?.throwIfAborted();
1603
+ return false;
1604
+ }
1605
+ })).every(Boolean);
1606
+ };
1607
+ //#endregion
1682
1608
  //#region src/cli/utils/read-diagnostic-evidence.ts
1683
1609
  const SOURCE_EXTENSIONS = [
1684
1610
  ".ts",
@@ -2040,8 +1966,10 @@ const isMaterializableGitSource = (content) => !content.includes("\0") && !conte
2040
1966
  * `tempDirectory`, mirroring the project layout plus head's config files so
2041
1967
  * both sides lint under the same rules. Git rename heuristics are disabled by
2042
1968
  * the diff planner, so an old path is retained as a base deletion while its
2043
- * new path is a head addition. Missing head-only additions are expected;
2044
- * missing paths that the plan says must exist at base make `isComplete` false.
1969
+ * new path is a head addition. Missing head-only additions are expected, and a
1970
+ * base-only deletion that cannot be read has nothing at head to compare
1971
+ * against; only a missing path that the plan says exists on both sides makes
1972
+ * `isComplete` false.
2045
1973
  */
2046
1974
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
2047
1975
  const git = yield* Git;
@@ -2066,11 +1994,12 @@ const materializeBaselineFiles = (input) => runPromise(gen(function* () {
2066
1994
  }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
2067
1995
  });
2068
1996
  const materializedFiles = new Set(tree.materializedFiles);
1997
+ const headFileSet = new Set(headFiles);
2069
1998
  return {
2070
1999
  ...tree,
2071
2000
  baseFiles,
2072
2001
  headFiles,
2073
- isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
2002
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
2074
2003
  untrackedFiles
2075
2004
  };
2076
2005
  }).pipe(provide(Git.layerNode)));
@@ -2092,11 +2021,16 @@ const resolveMergeBaseRef = async (directory, ref) => {
2092
2021
  }
2093
2022
  };
2094
2023
  //#endregion
2024
+ //#region src/cli/utils/has-unlinted-surviving-base-file.ts
2025
+ const hasUnlintedSurvivingBaseFile = (input) => {
2026
+ const analyzedBaseFiles = new Set(input.analyzedBaseFiles.map(toForwardSlashes));
2027
+ return filterSourceFiles(input.baseLintPaths.map(toForwardSlashes)).some((filePath) => input.headFiles.has(filePath) && !analyzedBaseFiles.has(filePath));
2028
+ };
2029
+ //#endregion
2095
2030
  //#region src/cli/utils/run-baseline-comparison.ts
2096
2031
  const silentConsole$1 = makeNoopConsole();
2097
2032
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
2098
2033
  const runBaselineComparison = async (input) => {
2099
- const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
2100
2034
  const baselineIncludePaths = filterPathsOutsideDirectories({
2101
2035
  rootDirectory: input.directory,
2102
2036
  relativePaths: input.options.includePaths,
@@ -2112,12 +2046,24 @@ const runBaselineComparison = async (input) => {
2112
2046
  relativePaths: input.headFiles,
2113
2047
  excludedDirectories: input.options.excludedProjectDirectories
2114
2048
  }) : void 0;
2049
+ const remainingBaselineBudgetMs = input.deadlineEpochMs === null ? null : remainingDeadlineBudgetMs(input.deadlineEpochMs);
2050
+ if (remainingBaselineBudgetMs === 0) return null;
2051
+ const baselineDeadlineSignal = remainingBaselineBudgetMs === null ? void 0 : AbortSignal.timeout(remainingBaselineBudgetMs);
2052
+ const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
2053
+ let maintainabilitySourceFiles = [];
2054
+ if (input.options.deadCode) try {
2055
+ maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
2056
+ } catch (error) {
2057
+ if (baselineDeadlineSignal?.aborted) return null;
2058
+ throw error;
2059
+ }
2060
+ const temporaryDirectory = mkdtempSync(path.join(tmpdir(), BASELINE_FILES_TEMP_DIR_PREFIX));
2115
2061
  const snapshot = await materializeBaselineFiles({
2116
2062
  directory: input.directory,
2117
2063
  ref: input.baselineRef,
2118
- files: baselineIncludePaths,
2119
- baseFiles: baselineBaseFiles,
2120
- headFiles: baselineHeadFiles,
2064
+ files: input.options.deadCode ? input.options.includePaths : baselineIncludePaths,
2065
+ baseFiles: input.options.deadCode ? input.baseFiles : baselineBaseFiles,
2066
+ headFiles: input.options.deadCode ? input.headFiles : baselineHeadFiles,
2121
2067
  tempDirectory: temporaryDirectory
2122
2068
  }).catch((error) => {
2123
2069
  rmSync(temporaryDirectory, {
@@ -2135,22 +2081,47 @@ const runBaselineComparison = async (input) => {
2135
2081
  }
2136
2082
  try {
2137
2083
  if (!snapshot.isComplete) return null;
2084
+ if (input.options.deadCode && !await copyUnchangedBaselineSources({
2085
+ directory: input.directory,
2086
+ sourceFiles: maintainabilitySourceFiles,
2087
+ baseMaterializedFiles: snapshot.materializedFiles,
2088
+ headChangedFiles: snapshot.headFiles,
2089
+ untrackedFiles: snapshot.untrackedFiles,
2090
+ tempDirectory: snapshot.tempDirectory,
2091
+ deadlineEpochMs: input.deadlineEpochMs,
2092
+ signal: input.oxlintRuntime.abortSignal
2093
+ })) return null;
2094
+ const filteredSnapshotBaseFiles = filterPathsOutsideDirectories({
2095
+ rootDirectory: input.directory,
2096
+ relativePaths: snapshot.baseFiles,
2097
+ excludedDirectories: input.options.excludedProjectDirectories
2098
+ });
2099
+ const filteredSnapshotHeadFiles = filterPathsOutsideDirectories({
2100
+ rootDirectory: input.directory,
2101
+ relativePaths: snapshot.headFiles,
2102
+ excludedDirectories: input.options.excludedProjectDirectories
2103
+ });
2138
2104
  const analyzedHeadFiles = new Set(input.headAnalyzedFiles.map(toForwardSlashes));
2139
- const baseFiles = new Set(snapshot.baseFiles.map(toForwardSlashes));
2140
- const expectedHeadFiles = new Set(snapshot.headFiles.map(toForwardSlashes));
2105
+ const baseFiles = new Set((baselineBaseFiles ?? filteredSnapshotBaseFiles).map(toForwardSlashes));
2106
+ const expectedHeadFiles = new Set((baselineHeadFiles ?? filteredSnapshotHeadFiles).map(toForwardSlashes));
2141
2107
  for (const filePath of baselineIncludePaths) {
2142
2108
  const normalizedFilePath = toForwardSlashes(filePath);
2143
2109
  if (!baseFiles.has(normalizedFilePath)) expectedHeadFiles.add(normalizedFilePath);
2144
2110
  }
2145
- if (filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
2111
+ if (input.options.lint && filterSourceFiles([...expectedHeadFiles]).some((filePath) => !analyzedHeadFiles.has(filePath))) return null;
2112
+ const baselineLintPaths = new Set([...baselineIncludePaths, ...filteredSnapshotBaseFiles].map(toForwardSlashes));
2113
+ const materializedLintPaths = snapshot.materializedFiles.filter((filePath) => baselineLintPaths.has(toForwardSlashes(filePath)));
2114
+ const maintainabilityFocusPaths = [...new Set([...input.options.includePaths, ...snapshot.baseFiles].map(toForwardSlashes))];
2115
+ 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)));
2116
+ const baseIncludePaths = materializedLintPaths.length > 0 ? materializedLintPaths : maintainabilityFocusPaths;
2146
2117
  const runtimeLayers = buildRuntimeLayers({
2147
2118
  directory: snapshot.tempDirectory,
2148
2119
  hasConfigOverride: true,
2149
2120
  userConfig: input.userConfig,
2150
2121
  configSourceDirectory: input.configSourceDirectory,
2151
2122
  projectInfoOverride: input.headProjectInfo,
2152
- shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath,
2153
- shouldRunDeadCode: false,
2123
+ shouldSkipLint: !input.options.lint || !input.resolvedNodeBinaryPath || materializedLintPaths.length === 0,
2124
+ shouldRunDeadCode: input.options.deadCode,
2154
2125
  shouldRunSupplyChain: input.options.supplyChain,
2155
2126
  shouldComputeScore: false,
2156
2127
  shouldShowProgressSpinners: false,
@@ -2159,7 +2130,8 @@ const runBaselineComparison = async (input) => {
2159
2130
  });
2160
2131
  const baseOutput = await runPromise(restoreLegacyThrow(runInspect({
2161
2132
  directory: snapshot.tempDirectory,
2162
- includePaths: snapshot.materializedFiles,
2133
+ includePaths: baseIncludePaths,
2134
+ maintainabilityFocusPaths,
2163
2135
  customRulesOnly: input.options.customRulesOnly,
2164
2136
  respectInlineDisables: input.options.respectInlineDisables,
2165
2137
  warnings: input.options.warnings,
@@ -2168,7 +2140,7 @@ const runBaselineComparison = async (input) => {
2168
2140
  includedTags: input.options.includedTags,
2169
2141
  includeTagDefaults: input.options.includeTagDefaults,
2170
2142
  nodeBinaryPath: input.resolvedNodeBinaryPath ?? void 0,
2171
- runDeadCode: false,
2143
+ runDeadCode: input.options.deadCode,
2172
2144
  isCi: input.options.isCi,
2173
2145
  doctorVersion: VERSION,
2174
2146
  runId: getRunId(),
@@ -2176,10 +2148,21 @@ const runBaselineComparison = async (input) => {
2176
2148
  suppressScanSummary: true,
2177
2149
  supplyChainManifestChanged: input.options.supplyChainManifestChanged,
2178
2150
  deadlineEpochMs: input.deadlineEpochMs ?? void 0,
2179
- signal: input.oxlintRuntime.abortSignal
2151
+ signal: input.oxlintRuntime.abortSignal,
2152
+ excludedProjectDirectories: baselineExcludedProjectDirectories,
2153
+ retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
2180
2154
  }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
2181
- if (baseOutput.didLintFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
2182
- const hasUnscannedUntrackedSourceFiles = filterSourceFiles(snapshot.untrackedFiles.map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
2155
+ if (baseOutput.didLintFail || baseOutput.didDeadCodeFail) return null;
2156
+ if (countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0 && hasUnlintedSurvivingBaseFile({
2157
+ baseLintPaths: materializedLintPaths,
2158
+ headFiles: expectedHeadFiles,
2159
+ analyzedBaseFiles: baseOutput.analyzedFiles
2160
+ })) return null;
2161
+ const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
2162
+ rootDirectory: input.directory,
2163
+ relativePaths: snapshot.untrackedFiles,
2164
+ excludedDirectories: input.options.excludedProjectDirectories
2165
+ }).map(toForwardSlashes)).some((filePath) => !analyzedHeadFiles.has(filePath));
2183
2166
  const diagnosticDelta = computeDiagnosticDelta({
2184
2167
  headDiagnostics: input.headDiagnostics,
2185
2168
  baseDiagnostics: baseOutput.diagnostics,
@@ -2352,6 +2335,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2352
2335
  directory,
2353
2336
  precomputedSourceFileCount: options.precomputedSourceFileCount,
2354
2337
  includePaths: options.includePaths,
2338
+ changedLineRanges: options.changedLineRanges ?? void 0,
2355
2339
  customRulesOnly: options.customRulesOnly,
2356
2340
  respectInlineDisables: options.respectInlineDisables,
2357
2341
  warnings: options.warnings,
@@ -2382,7 +2366,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
2382
2366
  else runSync(error(highlighter.error(lintFailureReason)));
2383
2367
  let inspectDiagnostics = output.diagnostics;
2384
2368
  let baselineDelta;
2385
- if (options.baseline && isDiffMode && !didLintFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
2369
+ if (options.baseline && isDiffMode && !didLintFail && !output.didDeadCodeFail && countIncompleteLintFiles(output.lintPartialFailures) === 0) {
2386
2370
  const comparison = await runBaselineComparison({
2387
2371
  directory,
2388
2372
  options,
@@ -2535,13 +2519,13 @@ const resolveCursorBundledNode = () => {
2535
2519
  if (!localAppData) return null;
2536
2520
  const cursorAgentRoot = path$1.join(localAppData, "cursor-agent", "versions");
2537
2521
  try {
2538
- const versions = fs$1.readdirSync(cursorAgentRoot);
2522
+ const versions = fs$2.readdirSync(cursorAgentRoot);
2539
2523
  if (versions.length === 0) return null;
2540
2524
  const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
2541
2525
  for (const version of sortedVersions) try {
2542
2526
  const bundledNodePath = path$1.join(cursorAgentRoot, version, "node.exe");
2543
2527
  const entryScriptPath = path$1.join(cursorAgentRoot, version, "index.js");
2544
- if (fs$1.statSync(bundledNodePath).isFile() && fs$1.statSync(entryScriptPath).isFile()) return {
2528
+ if (fs$2.statSync(bundledNodePath).isFile() && fs$2.statSync(entryScriptPath).isFile()) return {
2545
2529
  command: bundledNodePath,
2546
2530
  args: [entryScriptPath]
2547
2531
  };
@@ -2555,15 +2539,15 @@ const resolveWindowsCmdEntryScript = (command) => {
2555
2539
  for (const directory of pathDirectories) {
2556
2540
  const cmdFilePath = path$1.join(directory, `${command}.cmd`);
2557
2541
  try {
2558
- if (!fs$1.statSync(cmdFilePath).isFile()) continue;
2542
+ if (!fs$2.statSync(cmdFilePath).isFile()) continue;
2559
2543
  } catch {
2560
2544
  continue;
2561
2545
  }
2562
2546
  try {
2563
- const entryScriptMatch = fs$1.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
2547
+ const entryScriptMatch = fs$2.readFileSync(cmdFilePath, "utf8").match(/"%(?:~dp0|dp0%)\\([^"]+\.(?:m?js|cjs))"/);
2564
2548
  if (!entryScriptMatch) continue;
2565
2549
  const resolvedScriptPath = path$1.resolve(directory, entryScriptMatch[1]);
2566
- if (fs$1.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
2550
+ if (fs$2.statSync(resolvedScriptPath).isFile()) return resolvedScriptPath;
2567
2551
  } catch {}
2568
2552
  }
2569
2553
  return null;
@@ -2889,7 +2873,7 @@ const buildCodeFrame = (input) => {
2889
2873
  const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
2890
2874
  let source;
2891
2875
  try {
2892
- source = fs$1.readFileSync(absolutePath, "utf8");
2876
+ source = fs$2.readFileSync(absolutePath, "utf8");
2893
2877
  } catch {
2894
2878
  return null;
2895
2879
  }
@@ -2910,7 +2894,7 @@ const buildCodeFrame = (input) => {
2910
2894
  const discoverWorkspacePackages = (rootDirectory) => {
2911
2895
  const hasRootPackageJson = isFile(path$1.join(rootDirectory, "package.json"));
2912
2896
  const packages = listWorkspacePackages(rootDirectory);
2913
- if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) return discoverReactSubprojects(rootDirectory);
2897
+ if (packages.length === 0 && (!hasRootPackageJson || isMonorepoRoot(rootDirectory))) return discoverSupportedSubprojects(rootDirectory);
2914
2898
  return packages;
2915
2899
  };
2916
2900
  const selectProjects = async (rootDirectory, projectFlag, skipPrompts, configProjects) => {
@@ -2995,6 +2979,17 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
2995
2979
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
2996
2980
  };
2997
2981
  //#endregion
2982
+ //#region src/cli/utils/resolve-excluded-project-directories.ts
2983
+ const resolveExcludedProjectDirectories = (scanDirectory, projectDirectories) => {
2984
+ const excludedDirectories = /* @__PURE__ */ new Map();
2985
+ for (const projectDirectory of projectDirectories) {
2986
+ if (!isPathInsideDirectory(projectDirectory, scanDirectory)) continue;
2987
+ const resolvedDirectory = path$1.resolve(projectDirectory);
2988
+ if (!excludedDirectories.has(resolvedDirectory)) excludedDirectories.set(resolvedDirectory, projectDirectory);
2989
+ }
2990
+ return [...excludedDirectories.values()];
2991
+ };
2992
+ //#endregion
2998
2993
  //#region src/cli/utils/validate-mode-flags.ts
2999
2994
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
3000
2995
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
@@ -3002,6 +2997,12 @@ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
3002
2997
  if (!includeUntracked || scope !== void 0 && scope !== "full") return;
3003
2998
  throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
3004
2999
  };
3000
+ const validateFilePathSelectionFlags = (flags) => {
3001
+ if (flags.staged || flags.changedFilesFrom !== void 0) throw new CliInputError("Cannot combine file path arguments with --staged or --changed-files-from. Use one file source.");
3002
+ if (usedDiffAlias(flags)) throw new CliInputError("Cannot combine file path arguments with --diff. File path arguments use the files scope.");
3003
+ if (usedScope(flags) && flags.scope !== "files") throw new CliInputError(`Cannot combine file path arguments with --scope ${flags.scope}. File path arguments use --scope files.`);
3004
+ if (flags.base !== void 0) throw new CliInputError("Cannot combine file path arguments with --base. The selected files do not need a Git base.");
3005
+ };
3005
3006
  const validateModeFlags = (flags) => {
3006
3007
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
3007
3008
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
@@ -3026,7 +3027,7 @@ const shouldFailScanGate = (input) => {
3026
3027
  return shouldBlockCi(filterScansForSurface(input.scans, "ciFailure"), input.blockingLevel);
3027
3028
  };
3028
3029
  //#endregion
3029
- export { resolveMergeBaseRef as A, buildEmptyReportMessage as B, copyToClipboard as C, deduplicateProjectScans as D, collectProjectSourceFileCounts as E, printDiagnosticsDump as F, formatLearnMoreLine as G, buildRulePriorityMap as H, formatDiagnosticSite as I, hasIncompleteScoreAnalysis as L, pluralize as M, formatElapsedTime as N, createInvocationInspect as O, printFooter as P, filterDiagnosticsByCategories as R, CLI_AGENT_BINARIES as S, buildHandoffPayload as T, buildSortedRuleGroups as U, hasLintHardFailure as V, formatFixRecipeLine as W, resolveCliInspectOptions as _, projectManifestChanged as a, setUpGitHubActions as b, selectProjects as c, resolveProjectChangedLineRanges as d, resolveProjectDiffIncludePaths as f, resolveWorkspaceDeadCodeOwner as g, retryMissingProjectScores as h, validateModeFlags as i, printHeadlessReport as j, inspect as k, buildCodeFrame as l, resolveProjectRelativeDirectory as m, formatSkippedProjectsMessage as n, partitionProjectScanOutcomes as o, resolveProjectSourceFilePaths as p, validateIncludeUntrackedScope as r, discoverWorkspacePackages as s, shouldFailScanGate as t, resolveProjectScan as u, filterScansForSurface as v, launchCliAgent as w, detectLaunchableAgents as x, countUniqueScannedFiles as y, buildNoScoreMessage as z };
3030
+ export { createInvocationInspect as A, hasIncompleteScoreAnalysis as B, detectLaunchableAgents as C, buildHandoffPayload as D, launchCliAgent as E, formatSkippedCheckLabel as F, buildRulePriorityMap as G, buildNoScoreMessage as H, formatElapsedTime as I, formatLearnMoreLine as J, buildSortedRuleGroups as K, printFooter as L, resolveMergeBaseRef as M, printHeadlessReport as N, collectProjectSourceFileCounts as O, pluralize as P, printDiagnosticsDump as R, setUpGitHubActions as S, copyToClipboard as T, buildEmptyReportMessage as U, filterDiagnosticsByCategories as V, hasLintHardFailure as W, 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, formatFixRecipeLine 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 };
3030
3031
 
3031
- //# sourceMappingURL=should-fail-scan-gate-DzZuTxim.js.map
3032
- //# debugId=00cf2dc8-65a5-523e-b3b1-8400324c9d87
3032
+ //# sourceMappingURL=should-fail-scan-gate-CfG_ppAV.js.map
3033
+ //# debugId=159aa887-619a-5d42-9d32-79fce37783d6