#repo-local #fact #rules #ai-agent #linting #typescript #cache #javascript #diagnostics #multi-language

build polint

Multi-language repo-local static-analysis rules (Go and TypeScript/JavaScript today)

6 releases

Uses new Rust 2024

new 0.1.12 May 15, 2026
0.1.11 May 12, 2026

#12 in #multi-language

MIT license

1MB
30K SLoC

polint

AI-agent-native, shadcn-style linting for rules you own.

polint turns repo-specific engineering instructions into executable lint rules. AI agents do not always follow prose in CLAUDE.md, AGENTS.md, prompts, or review comments. polint lets you encode the parts that are actually analyzable.

Think shadcn, but for linting: you own the rule code in your repository; polint brings the scaffolding and infrastructure to create, run, test, and ship it.

polint ships no built-in policy rules. It gives you the SDK, parsers, facts, diagnostics, local rule runner, config, cache, and CI output so your repo can own the rules.

Quick Example

Say your frontend must use design tokens instead of raw colors. A polint rule in your repo can catch the violation and tell the AI agent exactly how to fix it:

polint diagnostic for a raw-color literal in Button.tsx

That is the point: the rule does not just fail the code. It injects the missing project context back into the agent at the moment it needs to repair the change.

Try It

Install polint:

cargo install polint --locked

Or from GitHub Releases:

curl -sSfL https://raw.githubusercontent.com/emilwareus/polint/main/scripts/install.sh | bash

Run a self-contained example:

git clone https://github.com/emilwareus/polint.git
cd polint/examples/config-denied-literal
polint check --color always --fail-on none

Expected output:

polint check on examples/config-denied-literal showing a denied literal diagnostic

Use It In Your Repo

polint init
polint add-skill
polint new-rule ts no-raw-colors
polint check

polint init creates .polint.toml, .polint/rules/src/, .polint/cache/, .polint/.gitignore (ignoring cache/), and root rust-toolchain.toml when missing (see Minimum Rust version). polint new-rule <go|ts|js|generic> <name> adds a Rust rule module to your local rule pack. polint check discovers and runs that rule pack.

Rule packs live in your repo:

.polint.toml
.polint/
  rules/
    Cargo.toml
    src/
      main.rs
      no_raw_colors.rs

Rules should use the public SDK (polint::sdk::prelude::*) and runner (polint::runner::run_cli) only. Rule modules use #[polint::rule] functions: the typed fact-view parameters (StringLiterals<'_>, Imports<'_>, GoTests<'_>, and similar) are the facts the rule can read, and polint derives the analysis capabilities from that function signature. Rule functions are plain sync Rust functions with &mut RuleCtx<'_> first and a RuleResult return. RuleCtx is for reporting diagnostics, source paths, rule options, and capability/setup metadata. The fact reference in docs/facts/ describes the raw and derived building blocks available to rule authors: functions, reusable metric signals, imports, branches, Go tests, TS/JS facts, literals, and JSX attributes. Rule-specific TOML fields that are not one of the common shortcuts are available through ctx.options().settings.

use polint::sdk::prelude::*;

#[polint::rule(
    id = "local/no-raw-colors",
    description = "Require design tokens instead of raw color literals.",
    severity = "error"
)]
pub(crate) fn no_raw_colors(
    ctx: &mut RuleCtx<'_>,
    literals: StringLiterals<'_>,
) -> RuleResult {
    for literal in literals.iter() {
        if literal.value.starts_with('#') {
            ctx.report(
                Diagnostic::error(
                    ctx.rule_id(),
                    ctx.file_path(literal.file),
                    literal.span.diagnostic_range(),
                    "Use a design token instead of a raw color literal.",
                )
                .with_evidence("literal", literal.value.clone()),
            );
        }
    }
    Ok(())
}

Profiles are explicit:

  • polint check runs every discovered rule.
  • polint check --profile web runs exactly [profiles.web].
  • Unknown profiles are errors.
  • Profile names are arbitrary. There is no default profile.

Cache

polint keeps local, untracked cache data under .polint/cache by default:

.polint/cache/
  analysis/       compact parser/fact JSON artifacts
  rules-target/   Cargo target dir for repo-local rule hosts
  derived/        reserved for future project-level derived facts

--no-cache disables analysis/fact cache reads and writes for that run. It does not disable the rules-target Cargo cache, because rebuilding the local rule host on every run is usually wasted work. Analysis cache keys include the source path and content, rule/options digest, loaded config, requested capability plan, cache format, and polint version. If a cache artifact cannot be decoded, polint treats it as a miss and removes that artifact.

Useful commands:

polint cache status
polint cache status --format json
polint cache prune --max-size-mb 512
polint cache prune --max-age-days 14 --dry-run
polint cache clean --category analysis
polint cache clean

Set POLINT_CACHE_DIR to move the whole cache root, or POLINT_RULES_TARGET_DIR to move only the repo-local rule-host Cargo target directory.

Comment Ignores

Use comment ignores for intentional, local suppressions:

// polint-ignore-next-line local/no-raw-colors -- legacy fixture
const color = "#ff00aa";

polint-ignore-line, polint-ignore-next-line, polint-ignore-start / polint-ignore-end, and top-of-file polint-ignore-file are supported. Selectors are required and use exact IDs, prefix/*, or *. Ignores suppress policy diagnostics only; parser, internal, capability, and polint/* diagnostics still report.

To inspect ignored debt:

polint ignores --stat --filter local/no-raw-colors

See docs/IGNORE-COMMENTS.md. The checked-in comment-ignores example shows one suppressed finding and one visible finding from the same rule.

For quick human scan summaries during normal checks:

polint check --shortstat
polint check --stat

These flags summarize scanned files, diagnostics, and ignore suppression counts for human output. They do not change JSON or SARIF output.

Baselines

Use a baseline when adopting polint in a repository that already has valid findings. The baseline is always checked in at .polint/baseline.yaml as compact YAML:

version: 1

baseline:
  - "local/backend-context-propagation e337fbb73d44b2b7 backend/app/handler.go"
ignore:
  - "local/no-raw-colors 1b7c9a00e493aa21 frontend/Button.tsx"

Each entry is one string:

<rule_id> <fingerprint> <file>

baseline entries are existing debt: they stay visible in human output but do not fail the process. ignore entries are central accepted exceptions: they are suppressed from output and failure. Baseline matching uses rule_id + fingerprint and refreshes unambiguous moved paths; ignore matching is file-specific to avoid suppressing unrelated findings with the same fingerprint.

polint baseline create
polint check --baseline --new-only
polint baseline update

--new-only emits and fails only on diagnostics not covered by the baseline or central ignore list.

Machine contract (JSON)

Stable JSON reports (polint check --format json) match the schema at docs/schemas/polint-report-v1.json. Emitters also set a top-level schema URL when using current polint. Diagnostics are deduplicated and sorted deterministically; --only-rule and --max-diagnostics apply after that pipeline for emitted reports. --max-diagnostics does not hide failures from --fail-on (see docs/AGENT-PLAYBOOK.md).

Minimum Rust version

Rule packs under .polint/rules are normal Rust crates that depend on the polint library. The published crate declares rust-version = "1.95" (MSRV). Cargo refuses to build the rule pack if the active rustc is older, even when the stub uses edition = "2024".

  • polint init writes rust-toolchain.toml at the repository root only when that file does not already exist, aligning the default toolchain with polint’s MSRV so polint check (which runs cargo with --manifest-path .polint/rules/Cargo.toml) succeeds.
  • If your repo already pins an older toolchain, raise channel in rust-toolchain.toml to 1.95 or newer, or run with an override, for example: RUSTUP_TOOLCHAIN=1.95 polint check

When the rules crate fails for this reason, the CLI adds a short note on top of Cargo’s stderr.

Semver: generated Cargo.toml uses polint = "0.1.x" (caret). Patch updates within 0.1 are accepted automatically; a 0.2 release requires updating that dependency line.

This repository pins Rust 1.95 in rust-toolchain.toml for developing polint itself.

Versions

Item Where it is defined
CLI and polint crate version Workspace version in the repo root Cargo.toml
Published crate polint on crates.io
Minimum supported Rust rust-version in workspace Cargo.toml
Generated rule packs Rust edition 2024 (polint new-rule template)

CI

name: polint

on: [push, pull_request]

jobs:
  polint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: emilwareus/polint@v1
        with:
          version: latest
          args: check --format github

Rules that request Go symbol/reference facts use the embedded Go sidecar by default, which needs Go 1.24 or newer on PATH. polint supports monorepos by inferring Go module roots from discovered files, or from [languages.go].module_roots in .polint.toml; no repository-root go.mod is required. In GitHub Actions, add this before polint check when using those facts:

      - uses: actions/setup-go@v6
        with:
          go-version: "1.24.x"

The action caches .polint/cache by default, including the repo-local rule-host Cargo target directory at .polint/cache/rules-target. A fully cold first run can still pay install, build, and analysis costs; repeat runs with the same relevant inputs should restore those caches. See the GitHub Action guide for inputs, cache keys, and pinning options.

More

License

MIT

Dependencies

~30–58MB
~1M SLoC