Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

validate

One command that validates any project, from any directory, on any machine.

cd ~/anything
validate

validate is not installed per project and does not live inside your repositories. It is a single global command that detects which languages a repository actually contains, resolves the right tools, and runs a fixed chain of non-overlapping quality gates. Python and TypeScript are supported today; a repo containing both gets both.

Two principles hold everywhere:

  • The repository's configuration wins. Defaults are supplied only for what a repo has not declared, because this tool runs against repositories it does not own.
  • Nothing is faked and nothing is written. A gate that cannot run honestly is reported as skipped, never quietly passed, and no file in the target directory is modified unless you pass --fix.

Install

On any machine:

curl -fsSL https://raw.githubusercontent.com/ricardofelixb/validate/main/install.sh | bash

That clones the repo to ~/projects/validate, symlinks ~/.local/bin/validate onto your PATH, and pre-warms the Python tool cache. Re-running it updates an existing install.

Manual install:

git clone https://github.com/ricardofelixb/validate.git ~/projects/validate
ln -sf ~/projects/validate/validate ~/.local/bin/validate

Custom locations:

INSTALL_DIR=~/dev/validate BIN_DIR=~/bin ./install.sh

Make sure the bin directory is on your PATH:

export PATH="$HOME/.local/bin:$PATH"

Requirements

  • bash 3.2 or newer, so stock macOS works
  • uv for the Python track — it fetches and globally caches ruff, basedpyright, deptry, and pip-audit
  • node and a package manager for the TypeScript track — pnpm, npm, yarn, or bun, detected from the lockfile
curl -LsSf https://astral.sh/uv/install.sh | sh

You never install the Python tools yourself. TypeScript tools are a different story on purpose: see Why TypeScript tools are not fetched.

Usage

validate              # fast: environment, format, lint, types, tests
validate static       # no tests — good for a pre-commit hook
validate full         # everything: coverage, production build, dead code, audits
validate --fix        # apply formatting and safe lint fixes, then validate

Every language present is validated by default. Narrow when you want to:

validate py           # Python only
validate ts           # TypeScript / JavaScript only
validate ts full      # combine freely with modes
Option Meaning
--fix Write formatting and safe lint fixes instead of only reporting them
--min N Coverage floor when the repository declares none (default 85)
--root PATH Validate PATH instead of the current directory
--no-env Skip the lockfile / install gate
-h, --help Usage
-V, --version Version

Environment variables: VALIDATE_MIN_COVERAGE, VALIDATE_LINE_LENGTH, NO_COLOR.

Exit codes: 0 every applicable gate passed, 1 a gate failed, 2 bad usage or no recognizable project.

Tracks are independent. Gates within a track run in order of cost and stop at the first failure, but a failing Python track never prevents the TypeScript track from running — otherwise a mostly-TypeScript repo with a few Python scripts would never reach its real gates.

The gates

Each gate is a distinct concern, deliberately non-overlapping. Ten redundant linters is not a validation stack.

Python

Gate Tool Purpose Mode
Environment uv sync --locked Lockfile matches project metadata all
Formatting ruff format --check Deterministic formatting all
Static bugs ruff check Undefined names, async and security mistakes all
Type contracts basedpyright Cross-module correctness all
Behavior pytest Runtime behavior fast, full
Test effectiveness coverage with branch coverage Untested branches fast, full
Dependency hygiene deptry Missing, unused, transitive dependencies full
Dependency security pip-audit Known vulnerabilities full
Architecture import-linter Declared dependency boundaries full
Startup your command The app imports and wires up full

TypeScript

Gate Tool Purpose Mode
Environment frozen lockfile install Manifests and lockfile agree all
Formatting Prettier or Biome Deterministic formatting all
Semantics ESLint or Biome Type-aware lint rules all
Type correctness tsc --noEmit Type correctness all
Behavior Vitest, or your test script Runtime behavior fast, full
Test effectiveness coverage thresholds Untested branches full
Production build next build, vite build, … Real bundler and module graph full
Dead code Knip Unused files, exports, dependencies full
Generated code opt-in check:generated Committed codegen is not stale full
Architecture dependency-cruiser Declared dependency boundaries full
Peer dependencies pnpm peers check Unmet peers full
Security pnpm audit Known vulnerabilities full
Signatures pnpm audit signatures Registry signature verification full
User flow opt-in check:e2e The app actually works full

The production build is a hard gate, and it is the biggest difference from Python. tsc validates TypeScript's model of the code; only the real build validates the bundler, framework transforms, module graph, assets, and production configuration. Never rely on the build as your only typecheck, and never set something like Next's ignoreBuildErrors to make validation pass.

Other deliberate choices: a frozen install fails when manifests and lockfile disagree instead of quietly resolving something new. Ruff uses an explicit rule set rather than ALL, so upgrading Ruff never silently adds gates. Coverage measures branches, because line coverage calls an if covered when only one outcome was ever tested. Exactly one type checker is authoritative — running both mypy and basedpyright buys disagreements, not correctness. And validation never mutates: no eslint --fix, no knip --fix, no pnpm audit --fix.

Deliberately excluded: Black, Flake8, isort, Pylint, Bandit, compileall, Vulture, complexity limits, Biome alongside Prettier, Oxlint as a second mandatory linter, ts-prune/depcheck/unimported, and a 100% coverage mandate.

How it adapts

Python environments

Detected Environment gate Tools from
pyproject.toml + uv.lock uv sync --locked project .venv, else uvx
Any existing virtualenv skipped that venv, else uvx
No environment skipped uvx

With no environment, the gates that genuinely cannot work are skipped and reported. A type checker without installed dependencies emits thousands of phantom "unresolved import" errors, and tests cannot import a package that was never installed.

TypeScript projects

The package manager comes from the lockfile, with packageManager as a tiebreak: pnpm-lock.yaml, package-lock.json, yarn.lock, or bun.lock. The frozen install is spelled correctly for each — pnpm install --frozen-lockfile, npm ci, bun install --frozen-lockfile, and yarn install --frozen-lockfile or --immutable depending on whether yarn is classic or Berry.

Every gate prefers a script the repository already declares, since that is the project's own definition of the check, and falls back to invoking the tool directly. For types that means check:types, then typecheck, then check-types, then tsc -p tsconfig.json --noEmit, then a workspace-recursive run. Formatting and linting only run when the repo declares a config, because a formatter with no declared config would impose defaults the project never chose.

Scripts that would mutate code or hang are refused. Real repos define lint as eslint --fix and test as bare vitest, which is watch mode. Any script containing --fix, --write, --apply, --watch, or a bare vitest is skipped in favor of a safe alternative, and the ones ignored are reported at the end.

Why TypeScript tools are not fetched

The Python track falls back to uvx when a tool is missing. The TypeScript track deliberately does not, and only uses binaries already in the project's node_modules/.bin. tsc and ESLint need the project's own dependencies and @types packages to say anything true; a freshly downloaded copy running against an uninstalled repo would report thousands of phantom errors rather than real ones. If dependencies are missing, validate says so and skips those gates.

Configuration defaults

If the repository declares [tool.ruff], [tool.basedpyright], [tool.pytest.ini_options], [tool.coverage], pytest.ini, ruff.toml, pyrightconfig.json, eslint.config.*, .prettierrc, biome.json, or tsconfig.json, that config is used untouched. Only when a setting is absent does validate supply its own:

  • Ruff lint — the explicit rule set E4,E7,E9,F,I,UP,B,SIM,ASYNC,S,RUF, with S101 allowed under tests/.
  • Ruff format — no width override. A repo with no ruff config was formatted at ruff's own default width, and imposing a different one would report every file as misformatted. Set VALIDATE_LINE_LENGTH to opt in.
  • basedpyrightrecommended with warnings fatal, minus the diagnostics that describe your dependencies rather than your code: reportMissingTypeStubs, reportAny, reportExplicitAny, reportUnusedCallResult, reportUnannotatedClassAttribute, reportImplicitStringConcatenation, and the reportUnknown* family. Untyped third-party packages are not a defect in the project being validated. A repo wanting pure recommended declares it.
  • Coverage — branch coverage with a floor of 85%, overridable with --min.
  • pytest-ra --strict-markers --strict-config.
  • Prettier fallback — source files only, never ., so lockfiles and generated artifacts are not reported as misformatted in repos without a .prettierignore.

Optional per-repo hooks

Gates that only the repository can define activate when you opt in.

Python startup smoke test. The check that catches the most real breakage is whether the application still imports and wires itself up:

[tool.validate]
smoke = 'python -m your_package --help'

Single quotes are a TOML literal string, so inner double quotes need no escaping. Double-quoted values work too and their \" escapes are resolved. The command should verify that imports succeed, config models construct, and plugin registration completes — without placing calls, contacting providers, mutating databases, or needing production secrets.

Committed generated code. For Convex and anything else that generates committed output, declare a script that regenerates and then fails if the commit was stale:

{
  "scripts": {
    "check:generated": "pnpm exec convex codegen && git diff --exit-code -- convex/_generated"
  }
}

Locally this leaves the fix in your working tree; in CI the dirty diff fails the run. Also give Convex its own typecheck — do not assume the web app's tsconfig.json covers everything under convex/.

Architecture contracts. Declare [tool.importlinter] in pyproject.toml, or a check:architecture script running dependency-cruiser. Worth adding once you have real boundaries: core logic that must not import provider SDKs, renderer code that must not import main-process modules, one company's plugins that must not import another's. Without genuine contracts it is ceremony.

End-to-end. A check:e2e script runs in full mode.

Recommended project config

validate works with no configuration. When you want a repository to own its rules, this is the baseline it assumes.

Python

[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src"]

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "UP", "B", "SIM", "ASYNC", "S", "RUF"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]  # assertions belong in tests

[tool.basedpyright]
include = ["src", "tests"]
typeCheckingMode = "recommended"
failOnWarnings = true
pythonVersion = "3.12"
pythonPlatform = "Linux"

[tool.pytest.ini_options]
minversion = "9.0"
testpaths = ["tests"]
strict = true
xfail_strict = true
addopts = ["-ra"]
filterwarnings = ["error"]

[tool.coverage.run]
branch = true
source = ["your_package"]
relative_files = true

[tool.coverage.report]
show_missing = true
skip_covered = true
fail_under = 85
exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError"]
uv add --dev ruff basedpyright pytest "coverage[toml]" deptry pip-audit

TypeScript

pnpm add -D typescript eslint @eslint/js typescript-eslint \
  eslint-config-prettier prettier globals \
  vitest @vitest/coverage-v8 knip dependency-cruiser

Strict compiler options worth adding beyond strict: true:

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,

    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedSideEffectImports": true,

    "useUnknownInCatchVariables": true,
    "forceConsistentCasingInFileNames": true,

    // Appropriate when Next, Vite, SWC, or esbuild produces the JavaScript.
    "isolatedModules": true
    // For an intentionally ESM-only package, also: "verbatimModuleSyntax": true
  }
}

Use type-aware ESLint. Typed rules catch floating promises, misused promises, unsafe values, and invalid async callbacks that syntax-only linting cannot see:

// eslint.config.mjs
import js from "@eslint/js";
import { defineConfig, globalIgnores } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import tseslint from "typescript-eslint";

export default defineConfig([
  globalIgnores([
    "**/.next/**", "**/.turbo/**", "**/build/**",
    "**/coverage/**", "**/dist/**", "**/out/**",
    "**/convex/_generated/**",
  ]),
  {
    files: ["**/*.{js,mjs,cjs}"],
    extends: [js.configs.recommended],
    languageOptions: { globals: globals.node },
  },
  {
    files: ["**/*.{ts,tsx,mts,cts}"],
    extends: [js.configs.recommended, tseslint.configs.strictTypeChecked],
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
    linterOptions: { reportUnusedDisableDirectives: "error" },
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { prefer: "type-imports", fixStyle: "inline-type-imports" },
      ],
      "@typescript-eslint/switch-exhaustiveness-check": "error",
    },
  },
  eslintConfigPrettier, // must be last
]);

For React and Next.js, add their official configs rather than recreating hook and framework rules by hand. Do not enable every ESLint rule; a strict preset plus a few deliberate policies beats "everything on".

Coverage needs an explicit include, or uncovered files are simply absent from the report and coverage looks healthier than it is:

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    clearMocks: true,
    restoreMocks: true,
    unstubEnvs: true,
    unstubGlobals: true,
    coverage: {
      provider: "v8",
      include: ["src/**/*.{ts,tsx}"],
      exclude: ["**/*.d.ts", "**/*.{test,spec}.{ts,tsx}", "**/__fixtures__/**"],
      reporter: ["text", "html", "json-summary"],
      thresholds: { lines: 85, statements: 85, functions: 85, branches: 80 },
    },
  },
});

Set thresholds to your honest baseline and ratchet upward. Coverage proves execution, not assertion quality; 85–90% with good boundary and failure-path tests beats a superficial 100%. Protect billing, auth, and data-mutation paths with stronger targeted requirements instead.

In a mature monorepo, prefer explicit filters over pnpm -r --if-present, which can silently skip a package that was supposed to expose a check.

In CI and hooks

- run: curl -fsSL https://raw.githubusercontent.com/ricardofelixb/validate/main/install.sh | bash
- run: ~/.local/bin/validate full

As a pre-commit hook in .git/hooks/pre-commit:

#!/usr/bin/env bash
exec validate static

Because it is an executable on PATH rather than a shell alias, it works from hooks, Makefiles, CI, and agent runs — an alias only exists in interactive shells.

A production build should be deterministic. Use placeholder or validation-environment configuration rather than requiring live production secrets, and never let a build in validation mutate real services.

Uninstall

rm ~/.local/bin/validate
rm -rf ~/projects/validate

License

MIT

About

One command that validates any project, from any directory, on any machine.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages