From 68e7677169a03e46b35916fdd7fdcdef48fcad12 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 16:27:15 +0000 Subject: [PATCH 1/2] refactor license warning detection Co-authored-by: Aiden Bai --- .changeset/ai-training-license.md | 4 +- packages/api/LICENSE | 34 ++++++ packages/api/src/diagnose.ts | 114 +++++++++--------- packages/core/LICENSE | 34 ++++++ .../utils/detect-ai-training-environment.ts | 76 +++++------- .../detect-ai-training-environment.test.ts | 77 ++++++++++++ packages/deslop-cli/README.md | 4 + packages/deslop-js/README.md | 2 +- packages/eslint-plugin-react-doctor/README.md | 2 +- packages/language-server/LICENSE | 34 ++++++ packages/language-server/package.json | 3 +- packages/oxlint-plugin-react-doctor/README.md | 2 +- packages/react-doctor/README.md | 2 +- .../cli/utils/warn-ai-training-environment.ts | 20 +-- .../warn-ai-training-environment.test.ts | 100 +++++++++++++++ packages/vscode-react-doctor/LICENSE | 34 ++++++ packages/vscode-react-doctor/README.md | 4 + packages/zed-react-doctor/README.md | 4 + 18 files changed, 428 insertions(+), 122 deletions(-) create mode 100644 packages/api/LICENSE create mode 100644 packages/core/LICENSE create mode 100644 packages/core/tests/detect-ai-training-environment.test.ts create mode 100644 packages/language-server/LICENSE create mode 100644 packages/react-doctor/tests/warn-ai-training-environment.test.ts create mode 100644 packages/vscode-react-doctor/LICENSE diff --git a/.changeset/ai-training-license.md b/.changeset/ai-training-license.md index 714b875af..789d9c1b3 100644 --- a/.changeset/ai-training-license.md +++ b/.changeset/ai-training-license.md @@ -6,6 +6,6 @@ "deslop-cli": patch --- -Update the license to MIT with additional restrictions: the software may not be used as training, fine-tuning, or evaluation data for machine-learning models or AI systems, nor sold or resold as a commercial product or service (e.g. a paid API, SaaS, or hosted/managed service) whose value derives substantially from the software, without prior written permission (contact founders@million.dev). Each version's additional restrictions expire on the second anniversary of its release, after which that version is available under the standard MIT License (an FSL-style grant of future license). Each published package now ships its own up-to-date `LICENSE` file so the terms travel with the tarball. +Update the license to MIT with additional restrictions: the software may not be used as training, fine-tuning, or evaluation data for machine-learning models or AI systems, nor sold or resold as a commercial product or service (e.g. a paid API, SaaS, or hosted/managed service) whose value derives substantially from the software, without prior written permission (contact founders@million.dev). Each published package now ships its own up-to-date `LICENSE` file so the terms travel with the tarball. -The `react-doctor` CLI also now prints a one-time notice (once per run) when it detects it is running inside an AI/ML training pipeline or agent sandbox, pointing to the license terms. +The `react-doctor` CLI and programmatic `diagnose()` API also now print a one-time notice (once per process) when they detect a high-confidence AI/ML pipeline environment, pointing to the license terms. diff --git a/packages/api/LICENSE b/packages/api/LICENSE new file mode 100644 index 000000000..aef05f702 --- /dev/null +++ b/packages/api/LICENSE @@ -0,0 +1,34 @@ +Modified MIT License + +Copyright (c) 2026 Million Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Our only modification is that the following uses require prior written +permission from the copyright holder. To request permission, contact +founders@million.dev. + +1. Using the Software, its source code, or any derivative works thereof, in + whole or in part, as training, fine-tuning, or evaluation data, or as input + to any automated pipeline for training or improving any machine learning + model or AI system. + +2. Selling the Software, or offering it to third parties as a paid, hosted, or + managed product or service (including any commercial API or SaaS offering) + whose value derives entirely or substantially from the Software. diff --git a/packages/api/src/diagnose.ts b/packages/api/src/diagnose.ts index 757d12124..0f495ff34 100644 --- a/packages/api/src/diagnose.ts +++ b/packages/api/src/diagnose.ts @@ -6,7 +6,6 @@ import { DEFAULT_PROJECT_SCAN_CONCURRENCY, DEFAULT_SHOW_WARNINGS, DeadCode, - detectAiTrainingEnvironment, Files, Git, layerOtlp, @@ -22,6 +21,7 @@ import { runInspect, Score, SupplyChain, + warnAiTrainingLicenseOnce, type InspectOutput, type ResolvedScanTarget, } from "@react-doctor/core"; @@ -36,58 +36,58 @@ import type { ScoreResult, } from "@react-doctor/core"; -// The CLI carries the richer warning (logger + telemetry); the library only -// has stdout, so it warns once per process via console.warn when a scan runs -// inside an AI/ML training environment (license requires written permission). -let didWarnAiTraining = false; const warnIfAiTrainingEnvironment = (): void => { - if (didWarnAiTraining || detectAiTrainingEnvironment() === null) return; - didWarnAiTraining = true; - console.warn( - "[react-doctor] Use in an AI or ML pipeline requires written permission under the react-doctor license. Contact founders@million.dev to request access.", - ); + warnAiTrainingLicenseOnce({ + write: (message) => { + process.stderr.write(`[react-doctor] ${message}\n`); + }, + }); }; -// The production layer stack for the programmatic API. The only axis that -// varies across calls is `Config`: with no override we load from disk -// (`Config.layerNode`); with a per-project override the caller's already -// resolved config drives `Config.layerOf(...)`. The supply-chain gate reads -// `supplyChain.enabled` from that same effective config (default on), so the -// one config input decides both. Every other service is identical, so the -// stack is built once here rather than duplicated per variant. -const buildDiagnoseLayer = ( - config: ReactDoctorConfig | null, - configOverrideTarget?: Pick, -) => { - const configLayer = - configOverrideTarget === undefined - ? Config.layerNode - : Config.layerOf({ - config, - resolvedDirectory: configOverrideTarget.resolvedDirectory, - configSourceDirectory: configOverrideTarget.configSourceDirectory, - }); +interface BuildDiagnoseLayerInput { + readonly config: ReactDoctorConfig | null; + readonly configSourceDirectory: string | null; + readonly resolvedDirectory: string; + readonly shouldRunDeadCode: boolean; + readonly shouldRunLint: boolean; +} + +const buildDiagnoseLayer = (input: BuildDiagnoseLayerInput) => { + const configLayer = Config.layerOf({ + config: input.config, + resolvedDirectory: input.resolvedDirectory, + configSourceDirectory: input.configSourceDirectory, + }); return Layer.mergeAll( Project.layerNode, configLayer, - DeadCode.layerNode, + input.shouldRunDeadCode ? DeadCode.layerNode : DeadCode.layerOf([]), Files.layerNode, Git.layerNode, - Linter.layerOxlint, + input.shouldRunLint ? Linter.layerOxlint : Linter.layerOf([]), LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, Score.layerHttp, - config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]), + input.config?.supplyChain?.enabled !== false ? SupplyChain.layerNode : SupplyChain.layerOf([]), ); }; +const shouldRunDeadCode = ( + options: DiagnoseOptions, + effectiveConfig: ReactDoctorConfig | null, +): boolean => options.deadCode ?? effectiveConfig?.deadCode ?? true; + +const shouldRunLint = ( + options: DiagnoseOptions, + effectiveConfig: ReactDoctorConfig | null, +): boolean => options.lint ?? effectiveConfig?.lint ?? true; + const buildInspectProgram = ( scanTarget: ResolvedScanTarget, options: DiagnoseOptions, - configOverride?: ReactDoctorConfig, + effectiveConfig: ReactDoctorConfig | null, ) => { - const effectiveConfig = configOverride ?? scanTarget.userConfig; const includePaths = options.includePaths ?? []; return runInspect({ @@ -99,7 +99,7 @@ const buildInspectProgram = ( warnings: options.warnings ?? effectiveConfig?.warnings ?? DEFAULT_SHOW_WARNINGS, adoptExistingLintConfig: effectiveConfig?.adoptExistingLintConfig ?? true, ignoredTags: new Set(effectiveConfig?.ignore?.tags ?? []), - runDeadCode: options.deadCode ?? effectiveConfig?.deadCode ?? true, + runDeadCode: shouldRunDeadCode(options, effectiveConfig), isCi: false, resolveLocalGithubViewerPermission: true, }); @@ -133,15 +133,24 @@ const diagnoseDirectory = async ( directory: string, options: DiagnoseOptions, ): Promise => { - warnIfAiTrainingEnvironment(); const startTime = globalThis.performance.now(); const scanTarget = await resolveScanTarget(directory); - const program = buildInspectProgram(scanTarget, options); + warnIfAiTrainingEnvironment(); + const effectiveConfig = scanTarget.userConfig; + const program = buildInspectProgram(scanTarget, options, effectiveConfig); const output: InspectOutput = await Effect.runPromise( restoreLegacyThrow( program.pipe( - Effect.provide(buildDiagnoseLayer(scanTarget.userConfig)), + Effect.provide( + buildDiagnoseLayer({ + config: effectiveConfig, + configSourceDirectory: scanTarget.configSourceDirectory, + resolvedDirectory: scanTarget.resolvedDirectory, + shouldRunDeadCode: shouldRunDeadCode(options, effectiveConfig), + shouldRunLint: shouldRunLint(options, effectiveConfig), + }), + ), Effect.provide(layerOtlp), ), ), @@ -173,35 +182,28 @@ const diagnoseProject = async ( try { const scanTarget = await resolveScanTarget(projectDefinition.directory); const { directory: _, config: projectConfig, ...perProjectOptions } = projectDefinition; + const projectOptions = { ...baseOptions, ...perProjectOptions }; - // Config layers, least to most specific: on-disk `doctor.config.*` ← - // batch `config` ← per-project `config`. With no overrides the merge is - // the identity and the orchestrator loads from disk (`Config.layerNode`). const didOverrideConfig = batchConfig !== undefined || projectConfig !== undefined; const effectiveConfig = mergeReactDoctorConfigs( mergeReactDoctorConfigs(scanTarget.userConfig, batchConfig), projectConfig, ); - const program = buildInspectProgram( - scanTarget, - { ...baseOptions, ...perProjectOptions }, - effectiveConfig ?? undefined, - ); + const program = buildInspectProgram(scanTarget, projectOptions, effectiveConfig); // `plugins` is override-wins in the merge: when a caller layer supplies // it, relative entries resolve against the scan root (caller configs // have no file location); otherwise the on-disk config's directory. const didOverridePlugins = batchConfig?.plugins !== undefined || projectConfig?.plugins !== undefined; - const layer = buildDiagnoseLayer( - effectiveConfig, - didOverrideConfig - ? { - resolvedDirectory: scanTarget.resolvedDirectory, - configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory, - } - : undefined, - ); + const layer = buildDiagnoseLayer({ + config: effectiveConfig, + configSourceDirectory: + didOverrideConfig && didOverridePlugins ? null : scanTarget.configSourceDirectory, + resolvedDirectory: scanTarget.resolvedDirectory, + shouldRunDeadCode: shouldRunDeadCode(projectOptions, effectiveConfig), + shouldRunLint: shouldRunLint(projectOptions, effectiveConfig), + }); const output: InspectOutput = await Effect.runPromise( restoreLegacyThrow(program.pipe(Effect.provide(layer), Effect.provide(layerOtlp))), @@ -224,9 +226,9 @@ const diagnoseProject = async ( const diagnoseProjectBatch = async ( input: DiagnoseProjectsInput, ): Promise => { - warnIfAiTrainingEnvironment(); const startTime = globalThis.performance.now(); const { projects, concurrency, config: batchConfig, ...baseOptions } = input; + if (projects.length > 0) warnIfAiTrainingEnvironment(); // `diagnoseProject` never rejects (failures come back as `ok: false`), // so the pool always drains every project. diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 000000000..aef05f702 --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,34 @@ +Modified MIT License + +Copyright (c) 2026 Million Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Our only modification is that the following uses require prior written +permission from the copyright holder. To request permission, contact +founders@million.dev. + +1. Using the Software, its source code, or any derivative works thereof, in + whole or in part, as training, fine-tuning, or evaluation data, or as input + to any automated pipeline for training or improving any machine learning + model or AI system. + +2. Selling the Software, or offering it to third parties as a paid, hosted, or + managed product or service (including any commercial API or SaaS offering) + whose value derives entirely or substantially from the Software. diff --git a/packages/core/src/utils/detect-ai-training-environment.ts b/packages/core/src/utils/detect-ai-training-environment.ts index bc40e66ca..13c497f5a 100644 --- a/packages/core/src/utils/detect-ai-training-environment.ts +++ b/packages/core/src/utils/detect-ai-training-environment.ts @@ -1,76 +1,62 @@ -// Presence-based env vars that signal an active ML training pipeline, data -// collection job, RL simulation, or AI-agent sandbox. Auth/config vars (e.g. -// OPENAI_API_KEY, HF_TOKEN) are excluded — a stored credential doesn't mean -// the process is inside a training run. Active-run or hardware-binding vars do. -// Shared by the CLI warning and the programmatic API so the list lives once. -const AI_TRAINING_ENV_VARS: ReadonlyArray = [ - // HuggingFace — dataset/cache paths only (active data pipeline, not just auth) - ["HF_DATASETS_CACHE", "huggingface"], - ["HF_HOME", "huggingface"], - ["HUGGINGFACE_HUB_CACHE", "huggingface"], - ["SPACE_ID", "huggingface-spaces"], - // GPU/accelerator binding — implies an active training or inference job - ["CUDA_VISIBLE_DEVICES", "cuda"], - ["NVIDIA_VISIBLE_DEVICES", "nvidia"], - ["ROCR_VISIBLE_DEVICES", "rocm"], - ["TPU_NAME", "google-tpu"], - // Experiment tracking — active run/sweep IDs, not just stored API keys +const AI_TRAINING_BY_ENVIRONMENT_VARIABLE: ReadonlyArray = [ ["WANDB_RUN_ID", "wandb"], ["WANDB_SWEEP_ID", "wandb"], ["MLFLOW_RUN_ID", "mlflow"], - ["MLFLOW_TRACKING_URI", "mlflow"], ["COMET_EXPERIMENT_KEY", "comet"], ["NEPTUNE_RUN_ID", "neptune"], ["CLEARML_TASK_ID", "clearml"], ["DVC_STAGE", "dvc"], - // Distributed ML/RL workers ["RAY_WORKER_PROCESS", "ray"], - ["RAY_ADDRESS", "ray"], - // RL simulation environments - ["MUJOCO_GL", "mujoco"], - ["MUJOCO_PATH", "mujoco"], - ["GYM_DISABLE_ENV_CHECKER", "gymnasium"], - // Managed cloud ML platforms — job/model dir or run IDs imply a training job ["SM_TRAINING_ENV", "sagemaker"], ["TRAINING_JOB_ARN", "sagemaker"], - ["SAGEMAKER_BASE_DIR", "sagemaker"], ["AZUREML_RUN_ID", "azure-ml"], ["AZURE_ML_MODEL_DIR", "azure-ml"], - ["CLOUD_ML_PROJECT_ID", "vertex-ai"], - ["VERTEX_AI_LOG_LEVEL", "vertex-ai"], ["DET_MASTER", "determined-ai"], - ["LIGHTNING_USER_ID", "lightning-ai"], - // ML pipeline orchestrators ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"], - ["ARGO_WORKFLOW_NAME", "argo-workflows"], ["KFP_POD_NAME", "kubeflow-pipelines"], - // Notebook environments — active session markers ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"], ["COLAB_BACKEND_VERSION", "google-colab"], - ["DATABRICKS_RUNTIME_VERSION", "databricks"], - // GPU cloud / sandboxed code-execution platforms used by AI agents ["DAYTONA_WS_ID", "daytona"], - ["DAYTONA_WS_NAME", "daytona"], ["E2B_SANDBOX_ID", "e2b"], ["MODAL_FUNCTION_ID", "modal"], ["MODAL_TASK_ID", "modal"], ["RUNPOD_POD_ID", "runpod"], - ["REPLICATE_USERNAME", "replicate"], ["VAST_CONTAINERLABEL", "vast-ai"], - // Container registries for ML artifacts - ["HARBOR_URL", "harbor"], - ["HARBOR_HOSTNAME", "harbor"], - // Coding-agent evaluation harnesses (SWE-bench and derivatives) ["SWE_BENCH_TASK", "swe-bench"], ["SWEBENCH_TASK", "swe-bench"], ["SWE_AGENT_MODEL", "swe-agent"], ]; -export const detectAiTrainingEnvironment = (): string | null => { - for (const [envVar, label] of AI_TRAINING_ENV_VARS) { - // Truthy, not just defined: an empty value (e.g. `CUDA_VISIBLE_DEVICES=""`, - // which disables the GPU) shouldn't classify the run as a training pipeline. - if (process.env[envVar]) return label; +export const AI_TRAINING_LICENSE_NOTICE = + "Detected an AI or ML pipeline environment. This use may require written permission under the react-doctor license. Contact founders@million.dev to request access."; + +export const AI_TRAINING_ENVIRONMENT_VARIABLES = AI_TRAINING_BY_ENVIRONMENT_VARIABLE.map( + ([environmentVariable]) => environmentVariable, +); + +interface WarnAiTrainingLicenseOnceOptions { + readonly environment?: NodeJS.ProcessEnv; + readonly write: (message: string) => void; +} + +let didWarnAiTrainingLicense = false; + +export const detectAiTrainingEnvironment = ( + environment: NodeJS.ProcessEnv = process.env, +): string | null => { + for (const [environmentVariable, label] of AI_TRAINING_BY_ENVIRONMENT_VARIABLE) { + if (environment[environmentVariable]?.trim()) return label; } return null; }; + +export const warnAiTrainingLicenseOnce = ( + options: WarnAiTrainingLicenseOnceOptions, +): string | null => { + if (didWarnAiTrainingLicense) return null; + const detectedEnvironment = detectAiTrainingEnvironment(options.environment); + if (detectedEnvironment === null) return null; + didWarnAiTrainingLicense = true; + options.write(AI_TRAINING_LICENSE_NOTICE); + return detectedEnvironment; +}; diff --git a/packages/core/tests/detect-ai-training-environment.test.ts b/packages/core/tests/detect-ai-training-environment.test.ts new file mode 100644 index 000000000..b813da11d --- /dev/null +++ b/packages/core/tests/detect-ai-training-environment.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { + AI_TRAINING_ENVIRONMENT_VARIABLES, + AI_TRAINING_LICENSE_NOTICE, + detectAiTrainingEnvironment, +} from "../src/utils/detect-ai-training-environment.js"; + +describe("detectAiTrainingEnvironment", () => { + it("returns null without AI training signals", () => { + expect(detectAiTrainingEnvironment({})).toBeNull(); + }); + + it.each([ + ["WANDB_RUN_ID", "wandb"], + ["SM_TRAINING_ENV", "sagemaker"], + ["E2B_SANDBOX_ID", "e2b"], + ["SWE_BENCH_TASK", "swe-bench"], + ["COLAB_BACKEND_VERSION", "google-colab"], + ])("returns %s's label", (environmentVariable, expectedLabel) => { + expect(detectAiTrainingEnvironment({ [environmentVariable]: "active" })).toBe(expectedLabel); + }); + + it("ignores empty and whitespace-only values", () => { + expect(detectAiTrainingEnvironment({ WANDB_RUN_ID: "" })).toBeNull(); + expect(detectAiTrainingEnvironment({ WANDB_RUN_ID: " " })).toBeNull(); + }); + + it("does not match credentials, coding-agent markers, or broad local config", () => { + expect( + detectAiTrainingEnvironment({ + CUDA_VISIBLE_DEVICES: "0", + CURSOR_AGENT: "1", + HF_HOME: "/tmp/huggingface", + HF_TOKEN: "token", + HARBOR_URL: "https://harbor.example", + OPENAI_API_KEY: "token", + REPLICATE_USERNAME: "user", + }), + ).toBeNull(); + }); + + it("returns the first matching label", () => { + expect( + detectAiTrainingEnvironment({ + E2B_SANDBOX_ID: "sandbox", + WANDB_RUN_ID: "run", + }), + ).toBe("wandb"); + }); + + it("exports the presence-based environment variable list", () => { + expect(AI_TRAINING_ENVIRONMENT_VARIABLES).toContain("WANDB_RUN_ID"); + expect(AI_TRAINING_ENVIRONMENT_VARIABLES).not.toContain("OPENAI_API_KEY"); + }); +}); + +describe("warnAiTrainingLicenseOnce", () => { + it("writes the shared notice once and returns the detected label", async () => { + vi.resetModules(); + const { warnAiTrainingLicenseOnce } = + await import("../src/utils/detect-ai-training-environment.js"); + const messages: string[] = []; + + const firstDetectedEnvironment = warnAiTrainingLicenseOnce({ + environment: { WANDB_RUN_ID: "run" }, + write: (message) => messages.push(message), + }); + const secondDetectedEnvironment = warnAiTrainingLicenseOnce({ + environment: { MLFLOW_RUN_ID: "run" }, + write: (message) => messages.push(message), + }); + + expect(firstDetectedEnvironment).toBe("wandb"); + expect(secondDetectedEnvironment).toBeNull(); + expect(messages).toEqual([AI_TRAINING_LICENSE_NOTICE]); + }); +}); diff --git a/packages/deslop-cli/README.md b/packages/deslop-cli/README.md index 70be48fc9..a21c9e321 100644 --- a/packages/deslop-cli/README.md +++ b/packages/deslop-cli/README.md @@ -100,3 +100,7 @@ Every redundancy finding carries a confidence tier (`high` / `medium` / `low`) v ### Skipped files Files identified as empty, binary, or minified bundles are skipped with an `info`-severity `analysisErrors` note. This isn't an error. It means the file looked machine-generated or non-source and was excluded from analysis to avoid producing irrelevant findings. + +## License + +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/deslop-js/README.md b/packages/deslop-js/README.md index 20f5e4844..9d5c3a681 100644 --- a/packages/deslop-js/README.md +++ b/packages/deslop-js/README.md @@ -305,4 +305,4 @@ pnpm format ## License -MIT +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/eslint-plugin-react-doctor/README.md b/packages/eslint-plugin-react-doctor/README.md index d673c1fb1..ab1385fcd 100644 --- a/packages/eslint-plugin-react-doctor/README.md +++ b/packages/eslint-plugin-react-doctor/README.md @@ -87,4 +87,4 @@ See the [React Doctor README](https://github.com/millionco/react-doctor#readme) ## License -MIT +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/language-server/LICENSE b/packages/language-server/LICENSE new file mode 100644 index 000000000..aef05f702 --- /dev/null +++ b/packages/language-server/LICENSE @@ -0,0 +1,34 @@ +Modified MIT License + +Copyright (c) 2026 Million Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Our only modification is that the following uses require prior written +permission from the copyright holder. To request permission, contact +founders@million.dev. + +1. Using the Software, its source code, or any derivative works thereof, in + whole or in part, as training, fine-tuning, or evaluation data, or as input + to any automated pipeline for training or improving any machine learning + model or AI system. + +2. Selling the Software, or offering it to third parties as a paid, hosted, or + managed product or service (including any commercial API or SaaS offering) + whose value derives entirely or substantially from the Software. diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 935706e5f..af4193998 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -10,7 +10,8 @@ "files": [ "bin/**", "dist/**/*.js", - "dist/**/*.d.ts" + "dist/**/*.d.ts", + "LICENSE" ], "type": "module", "sideEffects": false, diff --git a/packages/oxlint-plugin-react-doctor/README.md b/packages/oxlint-plugin-react-doctor/README.md index 7e81b4b78..97bcc6e7e 100644 --- a/packages/oxlint-plugin-react-doctor/README.md +++ b/packages/oxlint-plugin-react-doctor/README.md @@ -98,4 +98,4 @@ See the [React Doctor README](https://github.com/millionco/react-doctor#readme) ## License -MIT +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/react-doctor/README.md b/packages/react-doctor/README.md index 2fb679ce9..e155eca7f 100644 --- a/packages/react-doctor/README.md +++ b/packages/react-doctor/README.md @@ -67,4 +67,4 @@ To opt out, run: `npx react-doctor@latest --no-telemetry` [Issues welcome!](https://github.com/millionco/react-doctor/issues) -MIT-licensed +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/react-doctor/src/cli/utils/warn-ai-training-environment.ts b/packages/react-doctor/src/cli/utils/warn-ai-training-environment.ts index 7b7d95e60..44e53cddb 100644 --- a/packages/react-doctor/src/cli/utils/warn-ai-training-environment.ts +++ b/packages/react-doctor/src/cli/utils/warn-ai-training-environment.ts @@ -1,21 +1,13 @@ -import { detectAiTrainingEnvironment, highlighter } from "@react-doctor/core"; +import { highlighter, warnAiTrainingLicenseOnce } from "@react-doctor/core"; import { METRIC } from "./constants.js"; import { recordCount } from "./record-metric.js"; -let didWarnAiTraining = false; - export const warnIfAiTrainingEnvironment = (): void => { - if (didWarnAiTraining) return; - const detected = detectAiTrainingEnvironment(); + const detected = warnAiTrainingLicenseOnce({ + write: (message) => { + process.stderr.write(`${highlighter.warn(`[react-doctor] ${message}`)}\n`); + }, + }); if (detected === null) return; - didWarnAiTraining = true; - // Written straight to stderr (not via `cliLogger`/`Console`) so the license - // notice survives `--json` mode, which no-ops the global console, and never - // lands on stdout where it would corrupt the JSON report. - process.stderr.write( - `${highlighter.warn( - "react-doctor detected use in an AI or ML pipeline. This use requires written permission under the react-doctor license — contact founders@million.dev to request access.", - )}\n`, - ); recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected }); }; diff --git a/packages/react-doctor/tests/warn-ai-training-environment.test.ts b/packages/react-doctor/tests/warn-ai-training-environment.test.ts new file mode 100644 index 000000000..47b492186 --- /dev/null +++ b/packages/react-doctor/tests/warn-ai-training-environment.test.ts @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import { AI_TRAINING_ENVIRONMENT_VARIABLES } from "@react-doctor/core"; +import { silenceConsoleForTest } from "./helpers/silence-console.js"; + +const { mockRecordCount } = vi.hoisted(() => ({ + mockRecordCount: vi.fn(), +})); + +vi.mock("../src/cli/utils/record-metric.js", () => ({ + recordCount: mockRecordCount, + recordDistribution: vi.fn(), +})); + +interface CapturedStderr { + readonly chunks: string[]; + readonly restore: () => void; +} + +const ENVIRONMENT_VARIABLES = [...AI_TRAINING_ENVIRONMENT_VARIABLES]; + +const captureStderr = (): CapturedStderr => { + const chunks: string[] = []; + const spy = vi.spyOn(process.stderr, "write").mockImplementation((( + chunk: string | Uint8Array, + ) => { + chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8")); + return true; + }) as never); + return { chunks, restore: () => spy.mockRestore() }; +}; + +describe("warnIfAiTrainingEnvironment", () => { + let savedEnv: Record; + let capturedStderr: CapturedStderr; + + beforeEach(() => { + vi.resetModules(); + mockRecordCount.mockReset(); + capturedStderr = captureStderr(); + savedEnv = {}; + for (const environmentVariable of ENVIRONMENT_VARIABLES) { + savedEnv[environmentVariable] = process.env[environmentVariable]; + delete process.env[environmentVariable]; + } + }); + + afterEach(() => { + capturedStderr.restore(); + for (const environmentVariable of ENVIRONMENT_VARIABLES) { + const previousValue = savedEnv[environmentVariable]; + if (previousValue === undefined) { + delete process.env[environmentVariable]; + } else { + process.env[environmentVariable] = previousValue; + } + } + }); + + it("does not write or emit metrics without a training signal", async () => { + const { warnIfAiTrainingEnvironment } = + await import("../src/cli/utils/warn-ai-training-environment.js"); + + warnIfAiTrainingEnvironment(); + + expect(capturedStderr.chunks).toEqual([]); + expect(mockRecordCount).not.toHaveBeenCalled(); + }); + + it("writes the license notice to stderr once and records the detected environment", async () => { + process.env.WANDB_RUN_ID = "run"; + const { warnIfAiTrainingEnvironment } = + await import("../src/cli/utils/warn-ai-training-environment.js"); + + warnIfAiTrainingEnvironment(); + warnIfAiTrainingEnvironment(); + + expect(capturedStderr.chunks).toHaveLength(1); + expect(capturedStderr.chunks.join("")).toContain("[react-doctor]"); + expect(capturedStderr.chunks.join("")).toContain("founders@million.dev"); + expect(mockRecordCount).toHaveBeenCalledTimes(1); + expect(mockRecordCount).toHaveBeenCalledWith("ai.training.warning_shown", 1, { + environment: "wandb", + }); + }); + + it("still writes to stderr when the global console is silenced", async () => { + process.env.E2B_SANDBOX_ID = "sandbox"; + const restoreConsole = silenceConsoleForTest(); + try { + const { warnIfAiTrainingEnvironment } = + await import("../src/cli/utils/warn-ai-training-environment.js"); + + warnIfAiTrainingEnvironment(); + } finally { + restoreConsole(); + } + + expect(capturedStderr.chunks.join("")).toContain("AI or ML pipeline"); + }); +}); diff --git a/packages/vscode-react-doctor/LICENSE b/packages/vscode-react-doctor/LICENSE new file mode 100644 index 000000000..aef05f702 --- /dev/null +++ b/packages/vscode-react-doctor/LICENSE @@ -0,0 +1,34 @@ +Modified MIT License + +Copyright (c) 2026 Million Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Our only modification is that the following uses require prior written +permission from the copyright holder. To request permission, contact +founders@million.dev. + +1. Using the Software, its source code, or any derivative works thereof, in + whole or in part, as training, fine-tuning, or evaluation data, or as input + to any automated pipeline for training or improving any machine learning + model or AI system. + +2. Selling the Software, or offering it to third parties as a paid, hosted, or + managed product or service (including any commercial API or SaaS offering) + whose value derives entirely or substantially from the Software. diff --git a/packages/vscode-react-doctor/README.md b/packages/vscode-react-doctor/README.md index 38882f3dd..38f8ae7e5 100644 --- a/packages/vscode-react-doctor/README.md +++ b/packages/vscode-react-doctor/README.md @@ -48,3 +48,7 @@ configuration the CLI uses. No editor-specific config is required. `pnpm run package` builds a self-contained `.vsix` (the client bundle is produced with esbuild). Publishing to the VS Code Marketplace / Open VSX is a follow-up. + +## License + +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. diff --git a/packages/zed-react-doctor/README.md b/packages/zed-react-doctor/README.md index 2fa4311ce..0865e4d21 100644 --- a/packages/zed-react-doctor/README.md +++ b/packages/zed-react-doctor/README.md @@ -42,3 +42,7 @@ Zed compiles the Rust extension to WebAssembly on install. Reload the extension ## Roadmap - Publishing to the Zed extension registry is a planned follow-up. + +## License + +Modified MIT License. See `LICENSE`; AI/ML training or evaluation use and paid hosted resale require prior written permission from founders@million.dev. From 8c2f7915910a916ab0a67e1525c8d21af032d882 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Jun 2026 16:44:28 +0000 Subject: [PATCH 2/2] test: relax language server startup timeout Co-authored-by: Aiden Bai --- packages/language-server/tests/integration/server.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/language-server/tests/integration/server.test.ts b/packages/language-server/tests/integration/server.test.ts index 7a3bb81f1..bfab9d118 100644 --- a/packages/language-server/tests/integration/server.test.ts +++ b/packages/language-server/tests/integration/server.test.ts @@ -7,6 +7,7 @@ import { LspTestClient, pathToUri, waitForNotification } from "../lsp-client.js" const PACKAGE_ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))); const FIXTURE_DIR = path.join(PACKAGE_ROOT, "tests", "fixtures", "simple-app"); const APP_FILE = path.join(FIXTURE_DIR, "src", "App.tsx"); +const LANGUAGE_SERVER_START_TIMEOUT_MS = 30_000; interface PublishDiagnosticsParams { uri: string; @@ -74,7 +75,7 @@ describe("react-doctor language server (stdio)", () => { }); appDiagnostics = (await publishPromise) as PublishDiagnosticsParams; - }); + }, LANGUAGE_SERVER_START_TIMEOUT_MS); afterAll(async () => { await client.stop();