Portable, project-authored ast-grep YAML lint rules enforcing house JSDoc/ESM JS conventions
ast-grep

Files
README.md

@voxpelli/ast-grep-rules #

EXPERIMENTAL staged extraction of the portable, project-authored ast-grep JSDoc/ESM lint rules.

npm version npm downloads Ask DeepWiki human-directed, AI-built

Important

Issues and pull requests live on Tangled — not on the GitHub mirror, where issues are disabled and only collaborators can open pull requests.

The rules encode house conventions shared across the voxpelli siblings: JSDoc-typed JavaScript, ESM-only, prefer unknown over any. They are the narrow, cross-repo-agreed slice.

Also ships an optional Claude Code workflow under workflows/ (an example script, not gated — see Applying these rules with an AI agent).

Rules #

id severity catches auto-fixable
no-jsdoc-object-typedef warning @typedef {object} Name followed by @property tags no — a one-token manual edit (delete {object} ) fully repairs it; the transform/fix auto-fix was lost to an ast-grep 0.45.0 regression (see callout below)
no-jsdoc-any-type warning @param/@returns/@type/@property {any} no — the correct narrower type (unknown, a named type, a union) is context-specific; a machine can't pick it
no-inline-jsdoc-import warning inline import('module').Type at a use-site inside a JSDoc block (/** … */ only — // and plain /* … */ prose are ignored). Excludes @typedef {import(...)} (a type ALIAS / cross-module re-export — the inline import IS the definition) and a x is import(...).T type-predicate position (the hoisted @import doesn't resolve there on the TS/JSDoc toolchain) no — a correct fix must BOTH rewrite the inline ref → bare Type AND add a top-level /** @import { Type } from 'module' */; ast-grep's fix is node-local (matched comment only) and can't insert the second edit, so rewriting alone would leave Type undefined. Hoist by hand

Why the split matters: no-jsdoc-object-typedef is a single-location edit (delete one token), so it would be the natural --update-all candidate — but its auto-fix is currently suspended (see callout). The other two flag drift that needs a human decision (which narrower type) or a second, non-local edit (the hoisted @import) that ast-grep can't perform safely — so they stay report-only.

Warning

Known limitation — ast-grep 0.45.0 regression. ast-grep 0.45.0 deliberately made Smart strictness (the default for pattern:) skip comment/extra nodes, which broke binding a metavariable to a comment node via pattern: $C + kind: comment. All three rules relied on that for transform-based message interpolation ($TAG/$NAME/$MOD/$TYP), and no-jsdoc-object-typedef additionally relied on it for its fix:. Detection still works — all three still flag their patterns as warnings — but messages are now static and the {object} auto-fix is gone. The full root-cause analysis and the revert path (restore the transform/fix form once upstream fixes comment-metavariable binding) live in COMPAT-FIX-0-45.md. The sweep workflow's bulk-fix path is suspended until then.

All three are severity: warning heuristics (house SHOULDs), so ast-grep scan exits 0 on findings — they surface, they do not fail CI on their own.

Diagnostic structure — message / note / metadata #

Each rule's diagnostic is authored as an agent-executable instruction, split across three ast-grep fields (rule config reference) with distinct jobs:

  • message — one line: what is wrong, rendered as the diagnostic headline. The rules are designed to interpolate the concrete captured tokens here ($MOD/$TYP, $TAG, $NAME) via transform — a regex named capture can't reach message directly, so it goes through transform. Currently static (see the 0.45.0 callout): ast-grep 0.45.0 broke comment-metavariable binding, so $-interpolation is suspended and messages carry the what without the which token. The transform-based form is documented in COMPAT-FIX-0-45.md for when upstream restores it.
  • note — multi-line markdown: the ordered how-to-repair (or, for the any rule, a decision tree). Rendered as the = … block beneath the message. Per the docs, note "can contain markdown syntax, but it cannot reference meta-variables" — so everything concrete stays in message; the note is the reusable procedure.
  • metadata — arbitrary machine-readable key/values, so a scout→apply harness can route a rule to a strategy without parsing the prose. ast-grep emits it in JSON output via ast-grep scan --json --include-metadata, so a harness reads the routing fields below straight from the scan result.

metadata fields #

Every rule carries the same five keys. Values are the options in use across this package; the schema is open (ast-grep treats metadata as free-form), so a consumer can add its own keys, but these five are the contract this package documents:

field type options meaning
autofixable boolean true · false The rule has a fix: that fully repairs the finding node-locally, so ast-grep scan --update-all (-U) is safe to run unattended. Currently none are true — no-jsdoc-object-typedef was true until ast-grep 0.45.0 broke its fix: (see the callout).
edits number | string 1 · 2 · context-dependent How many source edits a correct fix takes. 2 means the second edit lands in a different location than the match — which is exactly why ast-grep can't autofix it. context-dependent = the count depends on how the flagged value is used.
applyMode enum bulk · agent · judgment How to apply the fix. bulk = blind --update-all. agent = an LLM applies the deterministic recipe in note, then the gate verifies. judgment = needs genuine type judgment; the rule over-fires on legitimate cases the model must recognise and skip.
cheapModelSafe boolean true · false Whether a cheap model (e.g. Haiku) applies the fix reliably when gated by gate. Calibrated from the pilot below — a measured value, not an aspiration.
gate string tsc --noEmit The deterministic check that must pass after applying. This — not agent agreement — is what decides correctness; a wrong edit fails the gate.

Per-rule values:

rule autofixable edits applyMode cheapModelSafe gate
no-jsdoc-object-typedef false 1 manual true tsc --noEmit
no-inline-jsdoc-import false 2 agent true tsc --noEmit
no-jsdoc-any-type false context-dependent judgment false tsc --noEmit

Applying these rules with an AI agent #

These rules are designed so a cheap model can apply the fix from the diagnostic alone, made safe by the tsc gate rather than by agent consensus. In ESLint's vocabulary: all three are currently suggestion-class — agent-in-the-loop or manual, never a blind --fix. no-jsdoc-object-typedef was a fix (mechanically safe, blind --update-all) until ast-grep 0.45.0 broke its fix: (see the callout); it is now a manual one-token edit (delete {object} ), still cheap-model-safe.

The tsc gate is the semantic backstop. "Auto-fixable" is a syntactic property: a structural detector (ast-grep, Semgrep) validates the shape of an edit, not its meaning. unknown + narrow, gated by tsc --noEmit, is the mechanism that makes cheap-model application safe — a semantically wrong edit fails the gate even when it is syntactically valid.

Two design laws the notes follow (learned from applying the rules at scale):

  1. Tell the model that a downstream error can be the EXPECTED next step. Hoisting an import, or swapping any→unknown, deliberately produces a tsc error at the next site — that error is the signal to add the second edit / the narrower, not to revert. Each note says so in as many words.
  2. Name the legitimate exceptions the rule over-fires on, or the model mis-applies. no-jsdoc-any-type's note names the two "leave it" cases: an untyped 3rd-party boundary value, and the (...args: any[]) => any top-function constraint (where unknown[] => unknown would reject typed handlers by contravariance).

Calibrated cheap-model safety (pilot: 211 findings, 17 groupings) #

rule raw cheap-model success apply as
no-jsdoc-object-typedef 100% manual delete of {object} (auto-fix suspended by the 0.45.0 regression)
no-inline-jsdoc-import ~99% agent + tsc gate — the single miss was a duplicate @import (a TS2300), now pre-empted by the note's dedup instruction
no-jsdoc-any-type ~4/9 agent + gate, expect escalation to a stronger model — it is a judgment call, not a mechanical one

The cliff falls exactly at the single-step → multi-step / judgment boundary: a one-location mechanical fix is bulk-safe; a two-location but deterministic fix needs an agent plus the gate; a fix that requires knowing what a value is needs judgment and will need escalation.

Detection-only is forced by ast-grep, not a defect. ast-grep's fix edits a single node range, so a repair that requires a second edit elsewhere (the hoisted top-level @import) simply can't be expressed as a fix — hence no-inline-jsdoc-import is report-only by construction, and the note carries the recipe a human or agent runs instead.

Reusable sweep workflow #

workflows/haiku-jsdoc-sweep.js packages the above as a runnable Claude Code Workflow script: scout → apply → recover → escalate → verify, one agent per grouping (a module, or a file-batched slice of a monolith split by an inline-import cap). A cheap-model fleet applies the fixes from the diagnostics; a stronger model recovers stragglers; the repo's own gate is the deterministic cross-check; the run returns a capability scorecard. It is a documented example, not gated by this package's ast-grep test — pass a repo's root / moduleGlobs / gate via the Workflow tool's args (see the header of the file for the full arg reference). Its bulk-fix step (the --update-all pass over no-jsdoc-object-typedef) is suspended until ast-grep restores comment-metavariable binding; the report and agent-applied paths still work.

Consuming the rules #

Point an sgconfig.yml's ruleDirs at this package's rules/ directory (or copy/vendor the individual .yml files). A rule's own language: field scopes it to matching files automatically, so a single ast-grep scan over your source runs each rule only against files of its declared language.

# sgconfig.yml
ruleDirs:
  - node_modules/@voxpelli/ast-grep-rules/rules

The package's bundled sgconfig.yml + rule-tests/ are what ast-grep test (the check / test:node scripts) uses to prove each rule fires on a planted violation and stays silent on the correct form — the ast-grep-native equivalent of a detector self-test. The testConfigs in sgconfig.yml wires rule-tests/ and its __snapshots__/ together.

ast-grep documentation #