npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

civet-clint

v0.6.0

Published

Compiler-backed style checker and autofixer for Civet codebases

Readme

civet-clint

CI npm version License: MIT

civet-clint is a compiler-backed style checker and autofixer for Civet. The package installs the concise clint command line tool and provides a programmatic Node.js API.

Unlike a text-only regex formatter, civet-clint uses the official @danielx/civet compiler parser. By default, every autofix edit is compiled and verified to produce byte-for-byte identical output to the original source. For opt-in non-byte-identical transforms (such as unquoting single-quoted module paths in style/prefer-terse-imports), fixes are validated against a compiler reference source and bounded by engine-enforced output delta checks. Unsafe or semantics-altering edits are rejected by the safety gate.

Release Status: 0.3.0 published on npm latest. The tool targets @danielx/civet 0.11.15. As a pre-1.0 tool relying on Civet's parser and dialect options, compatibility is pinned to this compiler release. See the Compatibility Matrix.


Features

  • 🛡️ Compiler-Equivalence Verification: Every rule batch is verified against Civet's compilation output. Unsafe or output-altering edits are rejected by the safety gate.
  • Atomic File Rewrites: Changes are written atomically via temporary files, preventing partial writes and preserving line endings (\n vs \r\n).
  • 🎯 Bidirectional Civet Style Rules: 22 built-in rules covering idiomatic Coffee/React style and compiler-safe migration back toward standard Civet.
  • 🧩 Modular Rule Registry & Plugins: Modular RuleRegistry abstraction with plugin contracts, duplicate-rule validation, and runtime-isolated registries.
  • 🗂️ Per-File Configuration Overrides: Support for glob-based overrides in configuration files to tailor rules, presets, and compiler dials per directory or file pattern.
  • ⚙️ Configurable & Extensible: Support for presets (default, coffee-react, coffee-to-standard), granular rule severities (off, warn, error), and integration with project civet.json configs.
  • 🧭 Dial-Aware Capability Checks: Rules declare the compiler options they require (e.g., autoLet, react, coffeeRange). Incompatible rules are skipped rather than emitting invalid autofixes.
  • 📊 Flexible CLI: Rich terminal diagnostics, --check exit codes for CI, --write in-place fixing, machine-readable --format json, parallel linting via --concurrency, and clint --print-config [file] for inspecting workspace and per-file resolved configurations.

Installation

npm install --save-dev civet-clint

# Or with yarn
yarn add -D civet-clint

# Or with pnpm
pnpm add -D civet-clint

CLI Usage

# Check all .civet files in the current workspace (default: --check)
npx clint

# Check specific files or folders
npx clint src/ components/ app.civet

# Automatically fix all safe style violations in place
npx clint --write

# Specify custom configuration file
npx clint --write --config ./config/clint.json

# Output diagnostics in JSON format for CI/CD pipelines
npx clint --check --format json

# Print resolved workspace configuration and compiler dial
npx clint --print-config

# Print effective resolved configuration for a specific file (including overrides)
npx clint --print-config src/components/Button.civet

CLI Flags

| Flag | Description | |---|---| | --check | Lint files and report diagnostics. Exits with code 1 if errors are found, 0 if clean. (Default) | | -w, --write, --fix | Apply autofixes to source files in place after verifying compiler equivalence. | | --rewrite | Rename and convert JS/TS files (.js, .jsx, .ts, .tsx, .mts, .cts) to .civet after verifying they parse, then run the autofix pipeline in place. | | --print-config [file] | Print the resolved preset, compiler options, rules, and skipped/incompatible rules as JSON, then exit. If a target file is passed, resolves matching per-file overrides. | | -j, --concurrency <n> | Number of worker threads used to lint files in parallel. Defaults to the CPU count; 1 lints sequentially in the main process. Results are always reported in sorted file order regardless of this value. | | --verbose | Print the resolved config path, Civet compiler-options path, active preset, compiler options in effect, matching overrides, and file count to stderr before linting. Leaves --format json parseable on stdout. | | -c, --config <path> | Path to a configuration file. Only needed for names outside the auto-discovered list. | | -f, --format <text\|json> | Output format: human-readable text (default) or json. | | -v, --version | Print clint version and exit. | | -h, --help | Show CLI usage help. |


Configuration

Create a config file in your repository root. These names are discovered automatically, in order: clint.config.json, .clintrc.json, .clint.json. Any other filename works too, but must be passed explicitly with --config.

{
  "preset": "coffee-react",
  "civetConfig": "./civet.json",
  "rules": {
    "style/prefer-word-operators": "error",
    "style/prefer-concise-arrow": "error",
    "style/prefer-bare-assignment": "error",
    "style/no-null-equality": "warn",
    "style/no-is-not": "warn",
    "style/no-trailing-semicolons": "error"
  },
  "overrides": [
    {
      "files": ["test/**/*.civet", "**/*.test.civet"],
      "rules": {
        "style/prefer-word-operators": "off"
      }
    },
    {
      "files": ["src/legacy/**/*.civet"],
      "civetOptions": {
        "coffeeEq": true
      }
    }
  ]
}

Rule Options

A rule entry is normally a bare level ("error"). Rules that accept options also support the array form [level, options]:

{
  "rules": {
    "style/prefer-terse-imports": ["error", { "unquoteSingleQuotes": true }]
  }
}

Options are validated when the config loads: an unknown option key, a value of the wrong type, or options given to a rule that declares none is a hard error rather than a silently ignored setting. clint --print-config prints the effective options for every active rule, including defaults you did not set. Overrides accept the same array form, so options can be scoped to a glob.

style/prefer-terse-imports

| Option | Type | Default | Description | |---|---|---|---| | unquoteSingleQuotes | boolean | false | Also unquote single-quoted module specifiers. | | extraZeroArgCallees | string[] | [] | For style/no-single-param-arrow-without-parens: project-local callees whose callback takes no arguments, exempted from the ambiguity warning. |

By default the rule only unquotes double-quoted specifiers, because that rewrite is byte-identical: Civet echoes the original quote character, and the terse form emits double quotes. Unquoting './x' therefore changes the emitted literal to "./x" — a quote-style change, but still a change, so it stays opt-in.

When enabled, these fixes are not validated against the original compiled output. Two checks replace that one, and a fix must pass both:

  1. Reference source. The rule hands the engine the original file with exactly those specifiers rewritten to double quotes, and nothing else. The engine compiles it and requires the fixed file's output to match byte-for-byte. Because the rewrite is driven by parser-identified specifier spans rather than text matching, a string that merely looks like a path (x := 'plain from ./str') is never touched.
  2. Output-delta bound. The rule also declares how its output may differ — here, quote-style. The engine independently verifies that the two compiled outputs are identical once string-literal quoting is normalized, so the change is provably confined to quote characters and cannot alter identifiers, structure, or string contents.

The second check is what makes the first safe to trust. A reference source is supplied by the rule, so on its own it would let a rule authorize its own rewrite; the engine-owned delta bound is not something a rule can widen. The strict byte-identity check is unchanged for every other rule and for this rule's default path.

Presets

default

The baseline neutral preset that relies on Civet's standard word-operator parsing without enforcing specific framework or dialect styles:

  • style/prefer-word-operators: "error" (fixable)
  • style/prefer-concise-arrow: "error" (fixable)
  • style/no-mixed-interpolation: "warn" (diagnostic)
  • style/no-trailing-semicolons: "error" (fixable)
  • Compiler options: {}

coffee-react

Tailored for idiomatic Civet + React codebases:

  • style/prefer-word-operators: "error" (fixable)
  • style/prefer-concise-arrow: "error" (fixable)
  • style/prefer-jsx-shorthand: "error" (fixable, requires react)
  • style/prefer-bare-assignment: "error" (fixable, requires autoLet)
  • style/prefer-terse-imports: "error" (fixable)
  • style/prefer-bare-jsx-values: "error" (fixable, requires react)
  • style/prefer-hash-comments: "error" (fixable, requires coffeeComment)
  • style/no-trailing-semicolons: "error" (fixable)
  • style/prefer-existential-check: "warn" (diagnostic)
  • style/prefer-jsx-attr-shorthand: "warn" (diagnostic, requires react)
  • style/prefer-ampersand-shorthand: "warn" (diagnostic)
  • style/no-single-param-arrow-without-parens: "warn" (diagnostic)
  • style/prefer-named-export-default: "warn" (diagnostic)
  • style/no-thin-arrow: "warn" (diagnostic)
  • style/no-pipe-operator: "error" (diagnostic)
  • style/prefer-range-operator: "warn" (diagnostic, requires coffeeRange)
  • style/no-null-equality: "warn" (diagnostic)
  • style/no-is-not: "warn" (diagnostic)
  • style/no-mixed-interpolation: "warn" (diagnostic)
  • Compiler options: { "autoLet": true, "coffeeComment": true, "coffeeIsnt": true, "coffeeRange": true, "react": true }

coffee-to-standard

A transition preset for legacy CoffeeScript-compatible source. It parses with the same compiler options as coffee-react, but enables the safe inverse rules instead of their Coffee-style counterparts:

  • style/prefer-slash-comments: "error" (#//)
  • style/prefer-is-not: "error" (isntis not)
  • style/prefer-explicit-declarations: "error" (:= and exported auto-bindings)
  • The three dialect-independent default rules remain enabled. The existing word-operator rule stays out because it selects isnt while coffeeIsnt is active; the transition preset must converge directly on is not.
  • Compiler options: { "autoLet": true, "coffeeComment": true, "coffeeIsnt": true, "coffeeRange": true, "react": true }

Use it as a staged migration rather than turning compiler options off immediately:

  1. Select "preset": "coffee-to-standard" while keeping the existing Civet dial.
  2. Run clint --print-config and inspect skippedRules. In particular, style/prefer-is-not is skipped while coffeeNot is enabled because that option changes the meaning of is not.
  3. Disable coffeeNot only after compiling/testing the project without it, then run clint --write, review the diff, and run it again to verify a no-op.
  4. Repeat until clint --check is clean; unsupported cases remain untouched.
  5. Disable migrated Coffee options in civet.json and switch to "preset": "default".
  6. Compile and test the application after each compiler-option removal.

Opposing rules cannot be enabled together. Clint rejects those configurations before linting, preventing repeated autofix runs from oscillating between styles.

Per-file Configuration Overrides

The overrides array allows configuring rules, presets, compiler options, or separate civet.json configurations for subsets of files matching glob patterns. Overrides apply in declaration order on top of the base configuration.

{
  "overrides": [
    {
      "files": "src/components/**/*.civet",
      "civetOptions": { "react": true },
      "rules": {
        "style/prefer-jsx-shorthand": "error",
        "style/prefer-jsx-attr-shorthand": "error"
      }
    }
  ]
}

Compiler Dial & Rule Capabilities

Rules declare required compiler options (e.g., autoLet, react, coffeeRange) via meta.capabilities. When the active dial does not enable a rule's requirements, the rule is automatically skipped rather than executing and emitting diagnostics or fixes that are invalid under the active dial.


Rules Catalog

civet-clint currently provides 22 built-in style, correctness, and migration rules.

Fixable Rules

| Rule ID | Description | Required Dial | |---|---|---| | style/prefer-word-operators | Convert ===, !==, &&, ||, ! to is, isnt, and, or, not. | — | | style/prefer-concise-arrow | Convert parameterless () => to concise =>. | — | | style/no-trailing-semicolons | Phase cleanup. Disallow unnecessary trailing semicolons at statement ends. Keeps any semicolon that suppresses an implicit return — see below. Verified via semicolon-style output delta. | — | | style/prefer-jsx-shorthand | Convert className="btn" and id="main" to .btn and #main shorthands. Only where the shorthand lowers in place — see below. | react | | style/prefer-bare-assignment | Prefer bare x = 1 for let and := for CONST_CASE bindings. | autoLet | | style/prefer-walrus-declarations | Convert const x = … to x := …, including destructuring patterns. Byte-identical output, so unlike bare = it needs no delta. Conflicts with prefer-bare-assignment. | autoLet | | style/prefer-implicit-block-call | Drop the call parens on multi-line describe/it/test blocks and hooks so indentation closes them, removing stacked ))) closers. | — | | style/prefer-implicit-call-args | Drop call parens on a trailing matcher (expect(a).toBe 'x') or a render(<JSX/>) call, letting the argument list close the line. Single-line, statement-ending calls only; an empty argument list keeps its parens. | — | | style/prefer-implicit-arrow-arg | Drop call parens when the sole argument is a zero-parameter arrow (vi.fn => x, lazy => import(…)). Never fires on an object property followed by more properties — the arrow would absorb them. | — | | style/prefer-terse-imports | Omit the optional import keyword and unquote safe module paths ({ t } from ../i18n). Accepts unquoteSingleQuotes. | — | | style/prefer-jsx-attr-shorthand | Convert prop={prop} to {prop}. The prop={true} form is reported but not fixed — see below. | react | | style/prefer-bare-jsx-values | Convert braced values attr={value} to bare values attr=value for identifiers, member expressions, and non-string literals. | react | | style/prefer-hash-comments | Convert // line comments to CoffeeScript # comments. | coffeeComment | | style/prefer-slash-comments | Convert CoffeeScript # comments to standard Civet // comments while preserving directives, shebangs, block comments, and JSX text. | coffeeComment | | style/prefer-is-not | Convert CoffeeScript isnt to standard Civet is not. | coffeeIsnt | | style/prefer-explicit-declarations | Convert := and exported auto-bindings to explicit const/let declarations. Bare autoLet requires scope/hoisting analysis and remains untouched. | autoLet | | style/no-trailing-commas | Remove a comma before a closing bracket, brace or paren — object literals, arrays, argument lists, destructuring patterns and import clauses. Never edits regex literals, array elisions, or a comma after a rest element. Verified via trailing-comma-style output delta. | — | | style/prefer-indented-object | Drop the braces from a multi-line object literal bound to a declaration, letting indentation delimit it. | — | | style/prefer-indented-blocks | Drop the braces and head parens from a JS-style statement block (if / for / while / switch / try / catch / finally), letting indentation delimit the body. Verified via whitespace-style output delta. Two shapes are reported without a fix — see below. | — | | style/no-braced-arrow-body | Phase repair. De-brace a => { ... } body that Civet parses as an object literal. Applied by --rewrite; not by --write. | — | | style/no-discarded-arrow-return | Phase repair. Remove a trailing ; that collapses a concise arrow into a block discarding its return value. Applied by --rewrite; not by --write. | — |

style/prefer-indented-blocks — the two shapes reported without a fix

Both are cases where the before side of the equivalence check is itself the bug, so autofixing would be verifying a repair against broken output.

A single-statement body ending in ; parses as an object with a method definition — if (a) { g(); } becomes if (a) ({ g() {; } }), and g() never runs.

A block in expression position (the last statement of a function) already mis-parses into a returned object literal:

afterEach =>
  if (original) {
    Object.defineProperty(proto, 'scrollTo', original)
  } else {
    delete proto.scrollTo
  }
// what Civet actually emits — both branches become returned objects
if (original) { return ({
  defineProperty: Object.defineProperty(proto, 'scrollTo', original)
})} else return ({ scrollTo: delete proto.scrollTo })

De-bracing repairs this, so the emitted output legitimately changes and the gate correctly rejects the fix. Repair it by hand, adding a trailing ; where the block should return nothing.

Blocks nested inside a braced => { … } arrow body are skipped entirely: de-bracing the inner block while the arrow keeps its braces breaks compilation. Run style/no-braced-arrow-body first, and this rule sees them on a later pass.

style/prefer-jsx-attr-shorthand — why only one of the two forms is fixed

prop={prop}{prop} re-expands to exactly prop={prop}, including after a {...spread}, so compiled output is unchanged and the fix is applied.

prop={true}prop is reported without a fix. Civet emits the bare attribute as prop, so the compiled output genuinely differs. React treats both as true, but that is a render-equivalence claim, and the gate only accepts byte-identical output. Note the two forms are not interchangeable in source either: a bare prop is the boolean shorthand, so writing it in place of prop={prop} would change meaning.

style/no-trailing-semicolons — the semicolons it will not remove

In Civet a trailing ; is not always cosmetic: inside a function body it is one of the sanctioned ways to suppress the implicit return of the last statement.

useEffect =>
  setCount 5;        # without the `;` this becomes `return setCount(5)`, and React
                     # treats a non-function return value as a cleanup callback.

The rule verifies each candidate by compiling with and without the semicolon and comparing normalized output, so it reports only removals that provably do not change the emitted program. Candidates are bisected rather than tested one at a time, which keeps a file with hundreds of semicolons to a handful of compiles.

Note this is the opposite of style/no-discarded-arrow-return, where the semicolon must go. The two never overlap: that rule fires only when the body is not a real block.

style/prefer-jsx-shorthand — what it will and won't rewrite

Civet lowers the .class/#id shorthand to the front of the tag, on the tag-name line, wherever it was written. The rule therefore only rewrites attributes that are already there — the leading run of className/id on the tag line:

<div className="a" id="b" onClick={f}>   ✅  → <div .a #b onClick={f}>
<div className="a" {...props}>           ✅  → <div .a {...props}>
<Icon size={16} className="i" />         ❌  would emit className before size
<div {...props} className="a">           ❌  would invert spread precedence
<button                                  ❌  would collapse the line break
  className="a"
>

The last two matter beyond formatting: moving className ahead of a {...spread} changes which value wins. Skipped sites are still reported, so they surface for review.

Diagnostic Rules

| Rule ID | Description | Required Dial | |---|---|---| | style/prefer-existential-check | Prefer existential postfix (x?, not x?) over null equality comparisons. | — | | style/prefer-jsx-attr-shorthand | Report prop={true}, which lowers to prop and so is not byte-identical. The fixable prop={prop} form is listed above. | react | | style/prefer-ampersand-shorthand | Prefer & block shorthand for single-parameter callbacks (.map &.id). | — | | style/no-single-param-arrow-without-parens | Require parentheses around single arrow function parameters (x) => .... | — | | style/prefer-named-export-default | Prefer named default exports (export default MyComp = ...). | — | | style/no-thin-arrow | Disallow thin arrows -> in favor of fat arrows =>. | — | | style/no-pipe-operator | Disallow pipe operator \|>. | — | | style/prefer-range-operator | Prefer [0...N].map range loops over Array.from({ length: N }, ...). | coffeeRange | | style/no-null-equality | Disallow direct comparisons with null. | — | | style/no-is-not | Disallow is not in favor of isnt. | coffeeIsnt or coffeeNot | | style/no-mixed-interpolation | Disallow mixing ${...} and #{...} within the same file. | — |

Style-Guide Coverage

Clint automates the mechanical conventions of a Civet style guide — the ones with a deterministic, compiler-verifiable rewrite. Several conventions are deliberately out of scope; their absence is a design boundary, not a missing feature.

| Convention | Status | Notes | |---|---|---| | Word operators, existential checks, terse declarations/exports, terse imports, JSX shorthands, arrow style, range loops | Automated | See the rule tables above. | | JSX class/id shorthand where the attribute is not already first on the tag line | Partially automated | The shorthand lowers to the front of the tag, so rewriting elsewhere reorders emitted attributes — or changes precedence against a {...spread}. Reported, not autofixed. | | Side-effect import ordering | Not automated | Reordering imports can change evaluation order, so it is not compiler-equivalent. | | Single-quoted module paths | Automated, opt-in | Off by default, because unquoting './x' changes the emitted quote character. Set unquoteSingleQuotes on style/prefer-terse-imports to enable it; the fix is then verified against a reference compile so the change is provably confined to specifier quote style. | | Removing unused or default React imports | Not automated | Requires whole-program binding analysis; deleting a binding is not an equivalence-preserving edit. | | Comment quality, naming, file/layer organization, architectural policy (i18n via t(), no fetch in components) | Not automated | Qualitative judgments with no mechanical rewrite. Enforce in review. |


Migrating a JS/TS Codebase to Civet

Because Civet is a superset of JS/TSX, migrating codebases to Civet does not require a separate decompiler or manual line-by-line translation. In empirical testing against real-world projects, over 97% of JS/TSX files (133 of 137 in one real-world React codebase) parse and compile cleanly as Civet with zero manual source edits.

Use clint --rewrite to convert and format files in one pass:

# Rewrite all JS/TS files in a directory
npx clint --rewrite src/components

# Rewrite specific files or test glob
npx clint --rewrite src/**/*.test.jsx

--rewrite operates safely:

  1. Verifies that the source parses cleanly under the project's resolved Civet dial (civet.json / clint.config.json).
  2. Checks that the destination .civet file does not already exist (never clobbers).
  3. Renames the file in place via fs.rename (Git records an R100 clean rename).
  4. Runs the autofix pipeline in phase order (see below).
  5. Skips .d.ts, .d.mts, .d.cts declaration files and .cjs files.

Why renaming alone is not enough

A JS file that parses as Civet does not necessarily mean the same thing. Two constructs change behaviour silently the moment the extension changes:

// 1. A braced arrow body becomes an OBJECT LITERAL, not a statement block.
it('x', () => {
  expect(a).toBe(1)      //  compiles to:  it('x', () =>( { toBe: expect(a).toBe(1) }))
})                       //  side effects still run, so the test passes — but the
                         //  arrow now returns an object instead of the last value.

// 2. A concise arrow ending in `;` collapses into a block that discards its value.
const make = () => new QueryClient({ ... });
                         //  compiles to:  () => { new QueryClient({...}); }
                         //  make() now returns undefined.

Both compile cleanly and neither is reported by a lint pass over the resulting .civet, which is why --rewrite repairs them during conversion rather than leaving them to be found later. The rules are style/no-braced-arrow-body and style/no-discarded-arrow-return; both run in the repair phase.

Rule phases

Rules declare a phase, and --rewrite runs them in order, re-parsing between each so a later phase sees the text earlier phases produced:

| phase | purpose | gate | | --- | --- | --- | | repair | Fixes a mis-compilation. Emitted output changes by design. | The targeted defect must be present before and absent after. | | idiom | The default. Output-preserving style fixes. | Emitted output must be byte-identical (modulo a declared delta). | | cleanup | Fixes that only become correct once earlier phases have run. | Same as idiom. |

Ordering is load-bearing, not cosmetic. In a braced arrow body the trailing semicolon is what stops Civet reparsing the block as an object literal, so style/no-trailing-semicolons (phase cleanup) must not judge the body until style/no-braced-arrow-body (phase repair) has de-braced it. Running them in one pass would have each rule judging text the other is about to replace.

Because a repair changes emitted output on purpose, the byte-equality gate cannot verify it. Its gate is defect-specific instead — the mis-compilation must be present before and gone after — so a repair rule cannot use the phase as a licence to make arbitrary edits. Behaviour is ultimately verified by your own test suite: run it after --rewrite.

What to expect

Compiling cleanly is not the same as passing. Budget for review:

  • Files whose arrow bodies could not be repaired mechanically (a body declaring const/let is already a real block; act(=> ...) changes meaning if it gains an implicit return) are reported, not rewritten.
  • A handful of files may need a Prettier pass first: a wrapped arrow argument followed by a trailing comma (f((id) =>\n g(id),\n)) does not parse as Civet.
  • Run --rewrite, then run your test suite, then review the diff. Do not assume a clean clint run means the conversion was semantically neutral.

Programmatic API & Plugins

civet-clint exports a typed ESM API:

import {
  lintSource,
  lintFile,
  loadConfig,
  resolveConfigForFile,
  RuleRegistry,
  createDefaultRuleRegistry
} from 'civet-clint';

const registry = createDefaultRuleRegistry();
registry.register({
  id: 'custom/no-debugger',
  meta: {
    description: 'Disallow debugger statements',
    fixable: false,
    defaultSeverity: 'error'
  },
  check(context) {
    if (context.source.includes('debugger')) {
      context.report({
        ruleId: 'custom/no-debugger',
        message: 'Avoid debugger statements in production code'
      });
    }
  }
});

const config = loadConfig();
const result = lintSource('fn := () => a === b', {
  config,
  registry,
  fix: true
});

console.log(result.isEquivalencePreserved); // true
console.log(result.fixedSource);            // "fn := => a is b"

Fixes that intentionally rewrite comment text must declare meta.allowFixesInsideComments: true; comment edits from every other built-in or plugin rule are rejected before the compiler-equivalence gate.


Architecture & Equivalence Engine

flowchart TD
    A[Source File] --> B[Parse Raw AST via @danielx/civet]
    A --> C[Baseline Compilation]
    B --> D[Execute Active Rules via RuleRegistry]
    D --> E[Collect Non-overlapping TextEdits]
    E --> F[Apply Candidate Autofixes]
    F --> G[Compile Fixed Candidate]
    C --> H{Verify Byte-Identical Output}
    G --> H
    H -- Match --> I[Approved: Atomic Write]
    H -- Mismatch --> K{Rule declared a reference source?}
    K -- No --> J[Rejected: Retain Original Source]
    K -- Yes --> L[Compile Reference Source]
    L --> M{Byte-Identical to Reference?}
    M -- No --> J
    M -- Yes --> N{Within Declared Output Delta?}
    N -- No --> J
    N -- Yes --> I

Fixes are validated per rule, one batch per file, so a rejected rule never discards another rule's approved edits.

The reference-source branch is the single, opt-in exception to comparing against the original file's output, currently used only by unquoteSingleQuotes. It does not relax the gate, because it is paired with an engine-owned bound on the emitted difference. A rule supplies the reference source; the engine defines what each delta kind permits and verifies it separately, so a rule cannot widen its own allowance or launder an arbitrary rewrite through a broad reference. A rule never inspects, normalizes, or approves compiled output. Rules that declare no reference — every rule today by default — are governed solely by the strict check against the original.

For design details regarding AST constraints, compiler dials, and upstream Civet integration, see docs/upstream.md.


Documentation

Releasing, in one line

Publishing is triggered by pushing a v<version> tag — never by merging to main.

npm version <version> --no-git-tag-version   # bump package.json + lock
# update CHANGELOG.md, commit, push main
git tag -a v<version> -m "Release v<version>" && git push origin v<version>

CI then runs npm run release:check and publishes via npm Trusted Publishing (OIDC) — there is no npm token, and npm login / npm whoami are irrelevant. A local npm publish is neither needed nor expected. The workflow refuses to run from any non-tag ref by design. Full detail in the Release Guide.


License

MIT © shogi-dojo