react-doctor 0.9.12-dev.ffc2d14 → 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 (47) hide show
  1. package/README.md +33 -0
  2. package/dist/active-run-trace-dlIZeEvb.js +18 -0
  3. package/dist/active-scan-abort-registry-DwG6tSPy.js +28 -0
  4. package/dist/{build-section-divider-BuOtLrzK.js → build-section-divider-C09RcJrc.js} +8 -22
  5. package/dist/{ci-DVVsf25h.js → ci-CUgNjCr-.js} +12 -11
  6. package/dist/cli.js +116 -35
  7. package/dist/{design-Y1pcA2uj.js → design-CX2j8ubS.js} +4 -4
  8. package/dist/{devtools-D8NTVj81.js → devtools-CC2PAZ85.js} +3 -3
  9. package/dist/{exit-gracefully-DnXGONjj.js → exit-gracefully-cGJM2g3O.js} +15 -9
  10. package/dist/get-east-asian-width-Bv2TD3Xj.js +444 -0
  11. package/dist/{report-error-MGKyeIns.js → handle-error-BCI-Nv3P.js} +18 -68
  12. package/dist/index.d.ts +32 -0
  13. package/dist/index.js +628 -301
  14. package/dist/{inspect-g3_lzs3l.js → inspect-Dq5krKWG.js} +41 -29
  15. package/dist/{install-02Lq6wKC.js → install-C0aiZKgf.js} +9 -8
  16. package/dist/{install-doctor-script-DmEWQpVJ.js → install-doctor-script-CaTIms0r.js} +7 -449
  17. package/dist/{install-github-workflow-Dx6Pl9Wl.js → install-github-workflow-DjPgXVqg.js} +10 -6
  18. package/dist/{install-react-doctor-CMCXgenA.js → install-react-doctor-C-PVDWXv.js} +8 -8
  19. package/dist/{run-scan-app-CDDu7P32.js → jsx-runtime-D_A4d39v.js} +280 -2774
  20. package/dist/path-format-DnHpmJwG.js +9 -0
  21. package/dist/{print-branded-header-kbvWYuGv.js → print-branded-header-Dz0SwyHQ.js} +4 -4
  22. package/dist/project-analysis-worker.js +59 -54
  23. package/dist/prompt-runtime-scan-url-BXdY2Opw.js +240 -0
  24. package/dist/{read-object-file-Cwhr9OIi.js → read-object-file-CmMGK1uu.js} +4 -4
  25. package/dist/{record-metric-CFfsgcVs.js → record-metric-B6T_Ej33.js} +592 -250
  26. package/dist/report-error-DwL3b99q.js +64 -0
  27. package/dist/{report-workflow-result-DGg9xopH.js → report-workflow-result-5PueDIAr.js} +5 -5
  28. package/dist/{resolve-scope-CXS-5jLR.js → resolve-scope-CTlLl7e6.js} +8 -10
  29. package/dist/{rules-CThlosPx.js → rules-DEbHKRag.js} +6 -6
  30. package/dist/run-scan-app-D3huvmXJ.js +2517 -0
  31. package/dist/runtime-scan/browser-probe.iife.js +855 -0
  32. package/dist/runtime-scan-CHFAEYg3.js +770 -0
  33. package/dist/sanitize-runtime-url-Bbq40iIp.js +76 -0
  34. package/dist/scan-C7sCm7wi.js +47 -0
  35. package/dist/{should-fail-scan-gate-C0Bo5Gqy.js → should-fail-scan-gate-CfG_ppAV.js} +50 -117
  36. package/dist/{should-skip-prompts-BqrtVX4G.js → should-skip-prompts-BuNtAVEs.js} +4 -4
  37. package/dist/should-use-tui-B-lCjXVG.js +25 -0
  38. package/dist/skills/improve-threejs/SKILL.md +102 -0
  39. package/dist/skills/performance/SKILL.md +113 -0
  40. package/dist/skills/react-doctor/SKILL.md +6 -0
  41. package/dist/{unref-stdin-BxyvqsOy.js → telemetry-runtime-BF83aBFL.js} +55 -25
  42. package/dist/unref-stdin-SK3WfwxS.js +12 -0
  43. package/dist/{version-S-8vvWFi.js → version-C27gr4Qj.js} +4 -4
  44. package/dist/{why-DXfx0oL9.js → why-CYRkZ2VY.js} +4 -4
  45. package/dist/with-run-span-CdsXSJgE.js +110 -0
  46. package/package.json +9 -6
  47. package/dist/scan-4I1mvxBO.js +0 -53
@@ -0,0 +1,76 @@
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]="39fe62ec-70de-53ef-9168-84e5123b75a2")}catch(e){}}();
3
+ import { z as CliInputError } from "./record-metric-B6T_Ej33.js";
4
+ //#region src/cli/runtime-scan/constants.ts
5
+ const RUNTIME_SCAN_MAX_COMPONENT_EVENTS = 2e3;
6
+ const RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES = 5 * 1024 * 1024;
7
+ const RUNTIME_SCAN_MAX_SNAPSHOT_BYTES = RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES - 64 * 1024;
8
+ Math.floor(RUNTIME_SCAN_MAX_SNAPSHOT_BYTES / 3);
9
+ const RUNTIME_SCAN_MAX_TRACE_BYTES = 512 * 1024 * 1024;
10
+ const RUNTIME_SCAN_MAX_STRING_LENGTH = 4096;
11
+ const RUNTIME_SCAN_LAYOUT_THRASH_RATIO = .25;
12
+ const RUNTIME_SCAN_MAX_RECORDING_DURATION_MS = 300 * 1e3;
13
+ const RUNTIME_SCAN_SNAPSHOT_TIMEOUT_MS = 1e4;
14
+ const RUNTIME_SCAN_LOCAL_DEV_PORTS = [
15
+ 1234,
16
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 3e3 + portOffset),
17
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 4e3 + portOffset),
18
+ 4173,
19
+ 4200,
20
+ 4321,
21
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 5e3 + portOffset),
22
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 5173 + portOffset),
23
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 8e3 + portOffset),
24
+ ...Array.from({ length: 11 }, (_unusedValue, portOffset) => 8080 + portOffset),
25
+ 8888
26
+ ];
27
+ const RUNTIME_SCAN_BROWSER_WIDTH_PX = 1440;
28
+ const RUNTIME_SCAN_TRACE_FILE_EXTENSION = ".json.gz";
29
+ const RUNTIME_SCAN_TRACING_COMPLETE_TIMEOUT_MS = 6e4;
30
+ const RUNTIME_SCAN_PROBE_RELATIVE_PATH = "runtime-scan/browser-probe.iife.js";
31
+ const RUNTIME_SCAN_PROBE_SNAPSHOT_BINDING_NAME_PLACEHOLDER = "__REACT_DOCTOR_RUNTIME_SCAN_CAPTURE__";
32
+ const RUNTIME_SCAN_PROBE_SNAPSHOT_ATTRIBUTE_NAME_PLACEHOLDER = "data-react-doctor-runtime-scan-snapshot";
33
+ const RUNTIME_SCAN_PROBE_SNAPSHOT_TOKEN_PLACEHOLDER = "__REACT_DOCTOR_RUNTIME_SCAN_TOKEN__";
34
+ const RUNTIME_SCAN_SAFE_CDP_PAGE_URLS = [
35
+ "about:blank",
36
+ "chrome://newtab/",
37
+ "chrome://new-tab-page/"
38
+ ];
39
+ const RUNTIME_SCAN_CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f]/g;
40
+ const RUNTIME_SCAN_TRACE_CATEGORIES = [
41
+ "-*",
42
+ "blink.console",
43
+ "blink.user_timing",
44
+ "devtools.timeline",
45
+ "disabled-by-default-devtools.timeline",
46
+ "disabled-by-default-devtools.timeline.invalidationTracking",
47
+ "disabled-by-default-devtools.timeline.frame",
48
+ "disabled-by-default-devtools.timeline.stack",
49
+ "disabled-by-default-v8.cpu_profiler",
50
+ "disabled-by-default-v8.cpu_profiler.hires",
51
+ "latencyInfo",
52
+ "loading",
53
+ "v8",
54
+ "v8.execute"
55
+ ];
56
+ //#endregion
57
+ //#region src/cli/runtime-scan/sanitize-runtime-url.ts
58
+ const sanitizeRuntimeUrl = (rawUrl) => {
59
+ let parsedUrl;
60
+ try {
61
+ parsedUrl = new URL(rawUrl);
62
+ } catch {
63
+ throw new CliInputError(`Expected an absolute http(s) URL, received "${rawUrl}".`);
64
+ }
65
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new CliInputError(`Expected an http(s) URL, received "${parsedUrl.protocol}".`);
66
+ parsedUrl.username = "";
67
+ parsedUrl.password = "";
68
+ parsedUrl.search = "";
69
+ parsedUrl.hash = "";
70
+ return parsedUrl.href;
71
+ };
72
+ //#endregion
73
+ export { RUNTIME_SCAN_SNAPSHOT_TIMEOUT_MS as _, RUNTIME_SCAN_LOCAL_DEV_PORTS as a, RUNTIME_SCAN_TRACING_COMPLETE_TIMEOUT_MS as b, RUNTIME_SCAN_MAX_SNAPSHOT_BYTES as c, RUNTIME_SCAN_MAX_TRACE_BYTES as d, RUNTIME_SCAN_PROBE_RELATIVE_PATH as f, RUNTIME_SCAN_SAFE_CDP_PAGE_URLS as g, RUNTIME_SCAN_PROBE_SNAPSHOT_TOKEN_PLACEHOLDER as h, RUNTIME_SCAN_LAYOUT_THRASH_RATIO as i, RUNTIME_SCAN_MAX_SNAPSHOT_PAYLOAD_BYTES as l, RUNTIME_SCAN_PROBE_SNAPSHOT_BINDING_NAME_PLACEHOLDER as m, RUNTIME_SCAN_BROWSER_WIDTH_PX as n, RUNTIME_SCAN_MAX_COMPONENT_EVENTS as o, RUNTIME_SCAN_PROBE_SNAPSHOT_ATTRIBUTE_NAME_PLACEHOLDER as p, RUNTIME_SCAN_CONTROL_CHARACTERS_PATTERN as r, RUNTIME_SCAN_MAX_RECORDING_DURATION_MS as s, sanitizeRuntimeUrl as t, RUNTIME_SCAN_MAX_STRING_LENGTH as u, RUNTIME_SCAN_TRACE_CATEGORIES as v, RUNTIME_SCAN_TRACE_FILE_EXTENSION as y };
74
+
75
+ //# sourceMappingURL=sanitize-runtime-url-Bbq40iIp.js.map
76
+ //# debugId=39fe62ec-70de-53ef-9168-84e5123b75a2
@@ -0,0 +1,47 @@
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]="11af106d-169b-5022-be6a-f7f67fb8e76e")}catch(e){}}();
3
+ import { T as METRIC, fn as resolveScanTarget, t as recordCount } from "./record-metric-B6T_Ej33.js";
4
+ import { i as warnDeprecatedDiff } from "./resolve-scope-CTlLl7e6.js";
5
+ import { a as validateModeFlags, r as validateFilePathSelectionFlags, y as resolveCliInspectOptions } from "./should-fail-scan-gate-CfG_ppAV.js";
6
+ import { n as warnDeprecatedFailOn, r as runProjectMigrations, t as inspectAction } from "./inspect-Dq5krKWG.js";
7
+ import { n as shouldUseTui, r as resolveTuiEnvironment } from "./should-use-tui-B-lCjXVG.js";
8
+ import path from "node:path";
9
+ //#region src/cli/commands/scan.ts
10
+ const runScanCommand = async (input) => {
11
+ if (input.flags.cache === false) process.env.REACT_DOCTOR_NO_CACHE = "1";
12
+ if (input.filePaths !== void 0) validateFilePathSelectionFlags(input.flags);
13
+ const tuiEnvironment = {
14
+ flags: input.flags,
15
+ ...resolveTuiEnvironment()
16
+ };
17
+ if (input.filePaths !== void 0) {
18
+ await inspectAction(input.directory, input.flags, input.invocationCommand, input.filePaths);
19
+ return;
20
+ }
21
+ if (!shouldUseTui(tuiEnvironment)) {
22
+ await inspectAction(input.directory, input.flags, input.invocationCommand);
23
+ return;
24
+ }
25
+ await runProjectMigrations(path.resolve(input.directory));
26
+ const scanTarget = await resolveScanTarget(input.directory, { allowAmbiguous: true });
27
+ validateModeFlags(input.flags);
28
+ warnDeprecatedFailOn(input.flags, scanTarget.userConfig);
29
+ warnDeprecatedDiff(input.flags, scanTarget.userConfig);
30
+ recordCount(METRIC.cliInvoked, 1, { command: input.invocationCommand });
31
+ const { runScanApp } = await import("./run-scan-app-D3huvmXJ.js");
32
+ const { shouldFail } = await runScanApp({
33
+ directory: input.directory,
34
+ scanTarget,
35
+ options: resolveCliInspectOptions(input.flags, null),
36
+ projectFlag: input.flags.project,
37
+ skipPrompts: input.flags.yes ?? false,
38
+ blocking: input.flags.blocking ?? input.flags.failOn,
39
+ flags: input.flags
40
+ });
41
+ if (shouldFail) process.exitCode = 1;
42
+ };
43
+ //#endregion
44
+ export { runScanCommand };
45
+
46
+ //# sourceMappingURL=scan-C7sCm7wi.js.map
47
+ //# debugId=11af106d-169b-5022-be6a-f7f67fb8e76e
@@ -1,12 +1,15 @@
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]="b04e98bc-8ff1-560d-94c4-ce519f681b46")}catch(e){}}();
3
- import { $ as Maintainability, $t as listSourceFiles, An as makeSpan, At as filterDiagnosticsForSurface, Bn as try_, Ct as computeConfigFingerprint, Dn as decodeUnknownSync, Dt as createOxlintSpawnSlots, E as NANOSECONDS_PER_MILLISECOND, En as Diagnostic, Et as createNodeReadFileLinesSync, Fn as provideContext, G as isCiOrCodingAgentEnvironment, Gn as succeed, Gt as highlighter, H as ACTION_INPUT_ENVIRONMENT_VARIABLES, Ht as hasPublishedFixRecipe, In as provideService, J as Files, Jn as fail, Kt as isMonorepoRoot, Ln as runPromise, Mn as orElseSucceed, Mt as filterSourceFiles, Nn as promise, Pn as provide, Q as Linter, R as CliInputError, Rn as runSync, Rt as getDiagnosticRuleIdentity, Sn as error, St as collectSourceFileCountsByDirectory, Tn as warn, Tt as countOptInProjectRuleSelections, U as detectRunnerOs, Un as mergeAll, Vt as groupBy, W as isCiEnvironment, Wt as hashFileContents, Xn as ParentSpan, Y as Git, Yn as void_, Yt as isReactDoctorError, Z as LintPartialFailures, Zt as isScanComplete, _ as makeNoopConsole, _n as summarizeDiagnostics, _t as buildSkippedChecks, an as mergeReactDoctorConfigs, at as Project, bn as yieldToEventLoop, br as isFile, bt as classifyFileContext, c as setSentryProjectInfo, cn as resolveLintBatchOrdering, cr as GIT_SHOW_MAX_BUFFER_BYTES, ct as SidecarLintCacheEnabled, d as isJsonModeActive, dn as resolveScanTarget, dr as JSX_FILE_PATTERN, en as listSourceFilesCooperative, er as CI_URL, et as NodeResolver, fn as restoreLegacyThrow, fr as MILLISECONDS_PER_SECOND, g as VERSION, gn as shouldUseMaintainabilityLayer, gt as buildRuleSeverityControls, ht as buildRuleDocsUrl, in as materializeSourceTree, ir as DIAGNOSTIC_CATEGORY_BUCKETS, it as Progress, jn as map, jt as filterPathsOutsideDirectories, k as SCAN_RESULT_CACHE_FILENAME, kn as gen, kt as discoverSupportedSubprojects, l as getRunId, ln as resolveReactDoctorCacheDir, lr as HTML_FILE_PATTERN, mr as SHARE_BASE_URL, n as recordDistribution, nt as OxlintSpawnSlots, o as buildSentryScope, on as remainingDeadlineBudgetMs, or as ENTERPRISE_CONTACT_URL, ot as Reporter, pn as runInspect, pr as OXLINT_NODE_REQUIREMENT, pt as atomicWriteJson, q as Config, qt as isPathInsideDirectory, r as toSpanAttributes, rn as mapWithConcurrency, rt as PerFileLintCacheEnabled, s as buildSentryProjectContext, sn as resolveGithubActionsScoreMetadata, st as Score, t as recordCount, tn as listWorkspacePackages, tt as OxlintConcurrency, un as resolveScanConcurrency, ur as JSX_DUPLICATION_SOURCE_FILE_PATTERN, ut as SupplyChain, vr as isDirectory, vt as calculateScore, w as METRIC, wn as log, wt as computeDiagnosticDelta, x as BASELINE_FILES_TEMP_DIR_PREFIX, xn as Console, yt as canonicalizeUserRuleKey, z as isNonInteractiveEnvironment, zn as sync } from "./record-metric-CFfsgcVs.js";
4
- import { a as setActiveRunTrace, i as recordRunTraceId, o as getTelemetryContext } from "./unref-stdin-BxyvqsOy.js";
5
- import { n as buildFooterLinkLines, r as activeScanAbortRegistry, t as buildSectionDivider } from "./build-section-divider-BuOtLrzK.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-DmEWQpVJ.js";
7
- import { d as detectDefaultBranch, l as prompts, r as installReactDoctorWorkflow, u as isCommandAvailable } from "./install-github-workflow-Dx6Pl9Wl.js";
8
- import { a as isValidBlockingLevel, c as openWorkflowPullRequest, l as stageWorkflowFile, o as pickBlockingLevel, u as toForwardSlashes } from "./resolve-scope-CXS-5jLR.js";
9
- import { h as detectAvailableAgents, t as reportWorkflowResult } from "./report-workflow-result-DGg9xopH.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";
@@ -102,107 +105,6 @@ const buildRuntimeLayers = (input) => {
102
105
  return input.oxlintSpawnSlots === void 0 ? layersWithConcurrency : mergeAll(layersWithConcurrency, succeed(OxlintSpawnSlots, input.oxlintSpawnSlots));
103
106
  };
104
107
  //#endregion
105
- //#region src/cli/utils/with-run-span.ts
106
- /**
107
- * Ends the run span without letting a telemetry failure change the run's
108
- * outcome — the invariant every other emit site here already holds.
109
- *
110
- * The failure path is the reason this matters most: a throw from `end` there
111
- * would replace the user's original scan error, so a project-config mistake
112
- * could surface as an exporter stack trace, be reclassified as a crash, and get
113
- * reported as one.
114
- */
115
- const endSpanSafely = (span, exit) => {
116
- try {
117
- span.end(BigInt(Date.now()) * NANOSECONDS_PER_MILLISECOND, exit);
118
- } catch {}
119
- };
120
- /**
121
- * Clears the module-level run-scoped telemetry state — the current scanned
122
- * project and the active run trace. `inspect()` calls this at the start of every
123
- * run and again after a clean one, so a prior or just-finished scan can't attach
124
- * its project tags / trace to a later run or to a non-scan error (e.g.
125
- * inspectAction's post-loop finalize/handoff steps). A thrown scan error skips
126
- * the post-run reset, leaving the state for the command catch to attribute and
127
- * link the crash. Concurrent batch members (`concurrentScan`) never touch this
128
- * state — they neither write nor reset it. Safe to call when telemetry is off
129
- * (the refs are read only when an event is built).
130
- */
131
- const resetSentryRunState = () => {
132
- setSentryProjectInfo(null);
133
- setActiveRunTrace(null);
134
- };
135
- /**
136
- * Runs an inspect invocation inside a root span so each `react-doctor` run is a
137
- * first-class trace with timing and the run snapshot as attributes. The span is
138
- * handed to `run` so the scan's Effect spans parent under it and so the wide
139
- * event can be stamped onto it once the outcome is known.
140
- *
141
- * A no-op pass-through when telemetry is off (`--no-telemetry`, tests, or an
142
- * unconfigured build) — `run` receives `undefined` and no span is created, so
143
- * there's no added exit latency.
144
- *
145
- * The span is created against the shared telemetry context rather than a
146
- * freshly-provided layer, so it lives in the same tracer as the scan program's
147
- * spans; see `telemetry-runtime.ts` for why that matters. It is started and
148
- * ended manually because the body it wraps is a promise-returning imperative
149
- * function, not an Effect — the same shape the Sentry inactive span had.
150
- *
151
- * While the span runs, its trace context is recorded as the active run trace so
152
- * `reportErrorToSentry` can attach a crash thrown during the scan back to this
153
- * trace (errors surface in the command catch, after the span has ended).
154
- *
155
- * A `concurrentScan` (one member of the CLI's multi-project pool) still gets its
156
- * own root span, but skips recording the active run trace — the module-level
157
- * handle has single-scan semantics, and overlapping writers would link a crash
158
- * to an arbitrary sibling's trace.
159
- */
160
- const withRunSpan = async (run, options = {}) => {
161
- const telemetryContext = getTelemetryContext();
162
- if (telemetryContext === null) return run(void 0);
163
- const { tags } = buildSentryScope();
164
- const command = typeof tags.command === "string" ? tags.command : "inspect";
165
- let rootSpan;
166
- try {
167
- rootSpan = runSync(makeSpan(`react-doctor ${command}`, { attributes: toSpanAttributes(tags) }).pipe(provideContext(telemetryContext)));
168
- } catch {
169
- return run(void 0);
170
- }
171
- recordRunTraceId(rootSpan.traceId);
172
- if (options.concurrentScan !== true) setActiveRunTrace({
173
- traceId: rootSpan.traceId,
174
- spanId: rootSpan.spanId,
175
- sampled: rootSpan.sampled
176
- });
177
- try {
178
- const result = await run(rootSpan);
179
- endSpanSafely(rootSpan, void_);
180
- return result;
181
- } catch (error) {
182
- endSpanSafely(rootSpan, fail(error));
183
- throw error;
184
- }
185
- };
186
- /**
187
- * Records the scanned project (discovered in the `beforeLint` hook): remembers
188
- * it for the lazy Sentry error-capture path (`buildSentryScope` folds it into
189
- * exception events) and for per-emit metric attributes, and sets it on the run's
190
- * root span so the trace carries the project shape too. Always cheap — the span
191
- * attributes are skipped when `rootSpan` is absent (telemetry off), and storing
192
- * the info is a plain assignment.
193
- *
194
- * A `concurrentScan` only sets the span attributes: the module-level project ref
195
- * has single-scan semantics, and overlapping writers would stamp events and
196
- * metrics with an arbitrary sibling's project. Wide events keep full attribution
197
- * (they ride the span); per-emit metrics simply omit the project shape during a
198
- * concurrent batch (absent, never wrong).
199
- */
200
- const recordSentryProjectContext = (projectInfo, rootSpan, options = {}) => {
201
- if (options.concurrentScan !== true) setSentryProjectInfo(projectInfo);
202
- if (rootSpan === void 0) return;
203
- for (const [key, value] of Object.entries(toSpanAttributes(buildSentryProjectContext(projectInfo).tags))) rootSpan.attribute(key, value);
204
- };
205
- //#endregion
206
108
  //#region src/cli/utils/diagnostic-grouping.ts
207
109
  const buildRulePriorityMap = (scores) => {
208
110
  const rulePriority = /* @__PURE__ */ new Map();
@@ -2064,8 +1966,10 @@ const isMaterializableGitSource = (content) => !content.includes("\0") && !conte
2064
1966
  * `tempDirectory`, mirroring the project layout plus head's config files so
2065
1967
  * both sides lint under the same rules. Git rename heuristics are disabled by
2066
1968
  * the diff planner, so an old path is retained as a base deletion while its
2067
- * new path is a head addition. Missing head-only additions are expected;
2068
- * 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.
2069
1973
  */
2070
1974
  const materializeBaselineFiles = (input) => runPromise(gen(function* () {
2071
1975
  const git = yield* Git;
@@ -2090,11 +1994,12 @@ const materializeBaselineFiles = (input) => runPromise(gen(function* () {
2090
1994
  }).pipe(map((content) => content !== null && isMaterializableGitSource(content) ? content : null))
2091
1995
  });
2092
1996
  const materializedFiles = new Set(tree.materializedFiles);
1997
+ const headFileSet = new Set(headFiles);
2093
1998
  return {
2094
1999
  ...tree,
2095
2000
  baseFiles,
2096
2001
  headFiles,
2097
- isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath)),
2002
+ isComplete: baseFiles.every((filePath) => materializedFiles.has(filePath) || !headFileSet.has(filePath)),
2098
2003
  untrackedFiles
2099
2004
  };
2100
2005
  }).pipe(provide(Git.layerNode)));
@@ -2116,6 +2021,12 @@ const resolveMergeBaseRef = async (directory, ref) => {
2116
2021
  }
2117
2022
  };
2118
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
2119
2030
  //#region src/cli/utils/run-baseline-comparison.ts
2120
2031
  const silentConsole$1 = makeNoopConsole();
2121
2032
  const countIncompleteLintFiles = (lintPartialFailures) => countDroppedLintFiles(lintPartialFailures) + countDeadlineSkippedFiles(lintPartialFailures);
@@ -2141,7 +2052,7 @@ const runBaselineComparison = async (input) => {
2141
2052
  const baselineListingSignal = baselineDeadlineSignal === void 0 ? input.oxlintRuntime.abortSignal : input.oxlintRuntime.abortSignal === void 0 ? baselineDeadlineSignal : AbortSignal.any([input.oxlintRuntime.abortSignal, baselineDeadlineSignal]);
2142
2053
  let maintainabilitySourceFiles = [];
2143
2054
  if (input.options.deadCode) try {
2144
- maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => JSX_DUPLICATION_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
2055
+ maintainabilitySourceFiles = (await listSourceFilesCooperative(input.directory, baselineListingSignal)).filter((filePath) => MAINTAINABILITY_SOURCE_FILE_PATTERN.test(filePath) && classifyFileContext(filePath) === "production");
2145
2056
  } catch (error) {
2146
2057
  if (baselineDeadlineSignal?.aborted) return null;
2147
2058
  throw error;
@@ -2241,7 +2152,12 @@ const runBaselineComparison = async (input) => {
2241
2152
  excludedProjectDirectories: baselineExcludedProjectDirectories,
2242
2153
  retainExcludedProjectDeadCodeDiagnostics: input.options.retainExcludedProjectDeadCodeDiagnostics
2243
2154
  }, {}).pipe(provide(runtimeLayers), provideService(PerFileLintCacheEnabled, false), provideService(SidecarLintCacheEnabled, false), provideService(Console, silentConsole$1))), { signal: input.oxlintRuntime.abortSignal });
2244
- if (baseOutput.didLintFail || baseOutput.didDeadCodeFail || countIncompleteLintFiles(baseOutput.lintPartialFailures) > 0) return null;
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;
2245
2161
  const hasUnscannedUntrackedSourceFiles = filterSourceFiles(filterPathsOutsideDirectories({
2246
2162
  rootDirectory: input.directory,
2247
2163
  relativePaths: snapshot.untrackedFiles,
@@ -3063,6 +2979,17 @@ const projectManifestChanged = (rootDirectory, projectDirectory, diffInfo) => {
3063
2979
  return diffInfo.changedFiles.some((filePath) => toForwardSlashes(filePath) === manifestPath);
3064
2980
  };
3065
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
3066
2993
  //#region src/cli/utils/validate-mode-flags.ts
3067
2994
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
3068
2995
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
@@ -3070,6 +2997,12 @@ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
3070
2997
  if (!includeUntracked || scope !== void 0 && scope !== "full") return;
3071
2998
  throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
3072
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
+ };
3073
3006
  const validateModeFlags = (flags) => {
3074
3007
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
3075
3008
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
@@ -3094,7 +3027,7 @@ const shouldFailScanGate = (input) => {
3094
3027
  return shouldBlockCi(filterScansForSurface(input.scans, "ciFailure"), input.blockingLevel);
3095
3028
  };
3096
3029
  //#endregion
3097
- export { resolveMergeBaseRef as A, buildNoScoreMessage as B, copyToClipboard as C, deduplicateProjectScans as D, collectProjectSourceFileCounts as E, printFooter as F, formatFixRecipeLine as G, hasLintHardFailure as H, printDiagnosticsDump as I, formatLearnMoreLine as K, formatDiagnosticSite as L, pluralize as M, formatSkippedCheckLabel as N, createInvocationInspect as O, formatElapsedTime as P, hasIncompleteScoreAnalysis as R, CLI_AGENT_BINARIES as S, buildHandoffPayload as T, buildRulePriorityMap as U, buildEmptyReportMessage as V, buildSortedRuleGroups 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, filterDiagnosticsByCategories 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 };
3098
3031
 
3099
- //# sourceMappingURL=should-fail-scan-gate-C0Bo5Gqy.js.map
3100
- //# debugId=b04e98bc-8ff1-560d-94c4-ce519f681b46
3032
+ //# sourceMappingURL=should-fail-scan-gate-CfG_ppAV.js.map
3033
+ //# debugId=159aa887-619a-5d42-9d32-79fce37783d6
@@ -1,10 +1,10 @@
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]="0fe3c6c5-2652-5908-928b-a30fbf2196c5")}catch(e){}}();
3
- import { z as isNonInteractiveEnvironment } from "./record-metric-CFfsgcVs.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]="82a072bc-1267-5b0f-b69c-f997e7622281")}catch(e){}}();
3
+ import { B as isNonInteractiveEnvironment } from "./record-metric-B6T_Ej33.js";
4
4
  //#region src/cli/utils/should-skip-prompts.ts
5
5
  const shouldSkipPrompts = (input = {}) => Boolean(input.yes) || Boolean(input.json) || isNonInteractiveEnvironment() || !process.stdin.isTTY;
6
6
  //#endregion
7
7
  export { shouldSkipPrompts as t };
8
8
 
9
- //# sourceMappingURL=should-skip-prompts-BqrtVX4G.js.map
10
- //# debugId=0fe3c6c5-2652-5908-928b-a30fbf2196c5
9
+ //# sourceMappingURL=should-skip-prompts-BuNtAVEs.js.map
10
+ //# debugId=82a072bc-1267-5b0f-b69c-f997e7622281
@@ -0,0 +1,25 @@
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]="0e5a5f1f-ac58-5ea9-8205-f8d5913b3bc6")}catch(e){}}();
3
+ import { B as isNonInteractiveEnvironment } from "./record-metric-B6T_Ej33.js";
4
+ //#region src/cli/utils/resolve-tui-environment.ts
5
+ const resolveTuiEnvironment = (outputIsTty = process.stdout.isTTY === true) => ({
6
+ isNonInteractiveEnvironment: isNonInteractiveEnvironment(),
7
+ nodeMajorVersion: Number(process.versions.node.split(".")[0]),
8
+ outputIsTty,
9
+ stdinIsTty: process.stdin.isTTY === true,
10
+ supportsRawMode: typeof process.stdin.setRawMode === "function",
11
+ terminalName: process.env.TERM
12
+ });
13
+ //#endregion
14
+ //#region src/cli/utils/should-use-tui.ts
15
+ const isTuiEnvironmentSupported = (input) => !input.isNonInteractiveEnvironment && input.stdinIsTty && input.outputIsTty && input.supportsRawMode && input.nodeMajorVersion >= 22 && input.terminalName !== "dumb";
16
+ const shouldUseTui = (input) => {
17
+ if (!isTuiEnvironmentSupported(input)) return false;
18
+ const flags = input.flags;
19
+ return !(flags.score === true || flags.json === true || flags.jsonCompact === true || flags.jsonOut !== void 0 || flags.staged === true || flags.changedFilesFrom !== void 0);
20
+ };
21
+ //#endregion
22
+ export { shouldUseTui as n, resolveTuiEnvironment as r, isTuiEnvironmentSupported as t };
23
+
24
+ //# sourceMappingURL=should-use-tui-B-lCjXVG.js.map
25
+ //# debugId=0e5a5f1f-ac58-5ea9-8205-f8d5913b3bc6
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: improve-threejs
3
+ description: Audit and fix Three.js and React Three Fiber apps for frame-loop performance, GPU memory leaks, scene-graph correctness, and visual defects like z-fighting, shadow acne, wrong color space, and broken resize handling. Uses React Doctor as the scanning engine plus a visual rubric checked against rendered output. Use when the user asks to improve, audit, scan, or clean up a Three.js, R3F, react-three-fiber, drei, or WebGL app, or types `/improve-threejs`.
4
+ ---
5
+
6
+ # Improve Three.js
7
+
8
+ Audits a Three.js or React Three Fiber (R3F) codebase and fixes what hurts most: work that runs every frame, GPU resources that never get disposed, scene-graph objects rebuilt on every render, and visual defects the user can see. React Doctor supplies the machine-verified code scan; this skill supplies the frame-loop judgment and the visual inspection a general React scanner lacks.
9
+
10
+ The core principle: severity follows the render loop. Code inside `useFrame` or a `requestAnimationFrame` callback runs 60 times per second, so a minor inefficiency there outweighs a major one in a settings panel. Rank every finding by where it runs, not by the rule's default severity.
11
+
12
+ ## Workflow
13
+
14
+ ### Step 1: Recon
15
+
16
+ Identify the stack before scanning: plain Three.js or R3F, which helper libraries are in use (drei, postprocessing, rapier), and where the render loop lives (`useFrame` hooks, `requestAnimationFrame`, the `<Canvas frameloop>` setting).
17
+
18
+ Build a hot-path map: every `useFrame` body, every RAF callback, every pointer-move handler. These files get the strictest review in Step 3.
19
+
20
+ ### Step 2: Scan
21
+
22
+ Run React Doctor read-only to collect structured evidence:
23
+
24
+ ```bash
25
+ npx react-doctor@latest --verbose
26
+ ```
27
+
28
+ For a regression check after making changes, run with `--scope changed` and confirm the score did not drop.
29
+
30
+ ### Step 3: Triage by frame-loop leverage
31
+
32
+ Re-rank the scanner's findings using the hot-path map, then hunt for the Three.js-specific problems the scanner cannot see. Confirm every finding at its `file:line` before reporting it.
33
+
34
+ **HIGH severity, runs every frame or leaks GPU memory:**
35
+
36
+ - **Allocation inside `useFrame`**: `new Vector3()`, `new Color()`, or fresh arrays passed to Three.js APIs each frame. Fix: hoist a scratch object to module scope or `useMemo`, then mutate it in place
37
+ - **`setState` inside `useFrame`**: re-renders the React tree on every frame. Fix: mutate refs directly; reserve state for discrete changes like selection or visibility
38
+ - **Missing disposal**: geometries, materials, textures, or render targets created imperatively and never disposed. Fix: call `dispose()` in the cleanup function, or move the object into R3F's declarative tree so it owns the lifecycle
39
+ - **Object reconstruction in render**: geometry or material instances created without `useMemo`, or inline `args` arrays whose identity changes each render, forcing R3F to rebuild the underlying object
40
+
41
+ **MEDIUM severity, per-render or per-interaction waste:**
42
+
43
+ - **Unstable scene-graph props**: inline `new THREE.Vector3()` or fresh material objects as props (plain arrays like `position={[x, y, z]}` are fine; R3F handles them)
44
+ - **Missing instancing**: hundreds of identical meshes rendered individually instead of through `<Instances>` or `InstancedMesh`
45
+ - **Wasted frames**: `frameloop="always"` on a scene that only changes on interaction. Fix: `frameloop="demand"` plus `invalidate()`
46
+ - **Uncached asset loading**: textures and models loaded outside `useLoader`, `useTexture`, or `useGLTF`, losing caching and Suspense integration
47
+
48
+ **LOW severity, hygiene:** React Doctor findings on non-canvas UI code, missing `<Preload>`, oversized textures.
49
+
50
+ ### Step 4: Visual audit
51
+
52
+ Inspect what the scene actually renders. Every visual finding needs evidence: a screenshot, a frame capture, or a reproduced observation, never a guess from reading source. When a dev server and browser are available, load the app, capture the first stable frame, then capture again after moving the camera and interacting. When no browser is available, check the code-level causes listed below and label each finding as inferred from source.
53
+
54
+ Apply the mini rubric. A row fails only when the evidence shows the failure condition:
55
+
56
+ | Area | Check | Fail when |
57
+ | ---------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
58
+ | Render sanity | The scene reaches a stable frame after load | Black canvas, WebGL context errors, or content that never appears |
59
+ | Geometry | Move the camera along seams, edges, and boundaries | Gaps, missing faces, visible backfaces, or two surfaces flickering at the same depth (z-fighting) |
60
+ | Transparency and depth | Cross depth-order boundaries with overlapping or transmissive surfaces | Wrong sort order, halos, opaque surfaces that should transmit, or flicker at grazing angles |
61
+ | Textures | View mapped surfaces close, far, and at grazing angles | Missing textures, stretching, seams, moiré, shimmer, or washed-out colors from a wrong color space |
62
+ | Materials and lighting | Change light and view direction on lit surfaces | Surfaces that ignore light direction, or reflective metals with no environment to reflect |
63
+ | Shadows | Move casters, receivers, and the light through their range | Acne, detached or floating shadows, flicker at rest, or shadows that outlive their caster |
64
+ | Camera | Follow the primary subject through movement and transitions | Subject leaves frame, camera clips into geometry, or foreground blocks the play area |
65
+ | Scale and contact | Compare object scale and resting contact against surroundings | Objects float above, sink into, or intersect their support surface, or sit at implausible scale |
66
+ | Image stability | Pan the camera slowly at supported resolutions | Silhouettes, thin geometry, or highlights that crawl, sparkle, or ghost |
67
+ | Resize and DPR | Change viewport size, zoom, and device pixel ratio | Distortion, blur, stretched output, or content leaving the viewport |
68
+
69
+ Each rubric row has a small set of usual code-level causes. Check these first when a row fails:
70
+
71
+ - **Washed-out or too-dark colors**: `renderer.outputColorSpace` not set to `SRGBColorSpace`, color textures missing `texture.colorSpace = SRGBColorSpace`, or a data texture (normal, roughness) wrongly marked sRGB
72
+ - **Z-fighting**: coplanar geometry needing `polygonOffset` or a position nudge, or a near plane set far too small for the scene scale
73
+ - **Shadow acne or floating shadows**: `shadow.bias` and `shadow.normalBias` untuned, or a shadow camera frustum far larger than the scene
74
+ - **Blurry or stretched canvas**: renderer size not synced to canvas CSS size, `setPixelRatio` never called, or a resize handler that forgets `camera.updateProjectionMatrix()`
75
+ - **Black metals**: `metalness: 1` with no `scene.environment` set
76
+ - **Transparency sorting glitches**: large transparent meshes needing `depthWrite: false`, manual `renderOrder`, or a split into smaller meshes
77
+ - **Shimmer and crawl**: missing texture anisotropy, antialiasing disabled, or thin geometry needing thicker forms
78
+
79
+ ### Step 5: Fix
80
+
81
+ Fix in severity order: HIGH performance findings and failed visual rows first. When a finding maps to a React Doctor rule, fetch the canonical recipe instead of improvising:
82
+
83
+ ```text
84
+ https://www.react.doctor/prompts/rules/<plugin>/<rule>.md
85
+ ```
86
+
87
+ For Three.js-specific findings, apply the fix named in the triage list or the cause list above.
88
+
89
+ ### Step 6: Validate
90
+
91
+ Run `npx react-doctor@latest --verbose --scope changed` and confirm the score did not regress. Re-check every visual rubric row that failed, using the same viewpoint and interaction as the original evidence, and confirm it now passes. Then verify behavior: the scene renders, animations play, and interactions respond. If browser dev tools are available, watch the memory profile while orbiting an idle scene; a rising heap during idle means a disposal leak survived.
92
+
93
+ ## Checks the scanner always misses
94
+
95
+ Review these by hand on every audit:
96
+
97
+ - `dispose()` coverage for every imperatively created GPU resource
98
+ - Allocations and `setState` inside `useFrame` and RAF callbacks
99
+ - Event listeners and `ResizeObserver`s on the canvas or window without cleanup
100
+ - Raycasting against the full scene on every pointer move instead of a filtered target list
101
+ - Shadows or postprocessing enabled globally when one part of the scene needs them
102
+ - Color space configuration on the renderer and every color texture