Imported from amritk/mjst (
packages/lint/AGENTS.md). Install upstream withnpx skills add amritk/mjst --skill lint. Copyright stays with the author.
AGENTS.md — @amritk/lint
Contributor guide for AI agents editing this package. Repo-wide rules:
../../AGENTS.md. Consuming the package instead? See
AI.md.
A format-agnostic JSON/YAML style-guide linter: JSON Schema + custom rules,
emitting exact line:column findings.
Commands
bun run --filter='@amritk/lint' test
bun run --filter='@amritk/lint' types:check
Invariants — do not break these
- The core engine is dependency-light by design: it ships no
$refresolver, no built-in ruleset, and no fixers. Those are caller-injected (resolvehook,fixersregistry,extendstargets). Keep that separation — don't bake a resolver into core. - Two severity vocabularies: rulesets author strings
(
error/warn/info/hint/off); findings carry numericDiagnosticSeverity(0–3). Don't unify them. - Ranges are zero-based
{ line, character }. Preserve that; the+1for display is the caller's job. - A finding's
source,pathandrangedescribe one node in one document. A resolved rule matches the dereferenced tree, so its match path is translated back to the authored node before the finding is built (locateincore/runner.ts, overcore/pointers.ts). Don't report a resolved-tree path: it names a node that need not exist, so no range resolves to it and no fixer can edit it.withoutDuplicates(core/order.ts) then relies on that — it keys onpathas well asrange, because a range alone cannot tell three absent siblings apart. - OpenAPI and AsyncAPI support live in the
./rules/openapiand./rules/asyncapisubpaths, layered on top of core — never merge either into the root entry. What the two genuinely share (the Server Object'svariables, tag-name uniqueness) lives in./rules/shared, which neither subpath imports from the other. - A node's members are its own enumerable string keys. Every walk in
core/jsonpath.tsenumerates withObject.keys, so naming a key directly (hasMember, andreadMemberincore/filter.ts) must agree — a plainObject.hasOwnmakesqueryandqueryManyanswer the same expression differently, because the latter seeds from one shared descent. - No
eval, nonew Function, no dynamic code construction anywhere in the engine. A ruleset is data — often YAML written by someone other than the person running the linter — so[?(...)]filters are parsed into an AST and interpreted (core/filter-expression.ts,core/filter.ts). An expression the grammar does not cover must become aCompiledPath.error(whichcreateRulesetthrows on), never a predicate that silently matches nothing. - Regular expressions built from document content must be unambiguous. The
casingpatterns are written so no input can be matched two ways; the ambiguous Spectral originals took minutes on a 46-characteroperationId. Check any new pattern undernode, notbun— JSC caps backtracking at ~1.3 s and hides the problem. - Depth is attacker-controlled. Walkers over document data are iterative, and
the parsers cap nesting (
MAX_NESTING_DEPTH) and report a diagnostic; a malformed document must never throw out oflintDocument. - The OpenAPI and AsyncAPI meta-schemas are generated modules. Edit the
vendored
.json, then runnode scripts/generate-schema-modules.mjs; the build fails on drift. Keep the imports static so the subpaths stay bundler-safe. - The vendored AsyncAPI schemas carry three deliberate regex rewrites. The
upstream patterns nest unbounded quantifiers — one is genuinely exponential —
and
@amritk/runtime-validatorsrefused to compile all three when they were vendored. Each replacement is proven equivalent inrules/asyncapi/schemas/schema.test.ts; re-vendoring a schema without re-applying them fails that test. Never reach forallowUnsafePatternsinstead. The screen has since been relaxed to admit separator-anchored repetitions, so only the exponential one is still refused — the same test pins each pattern's current verdict in both directions, and the other two rewrites stay as equivalent simplifications, not necessities.
Add a changeset for every change (bunx changeset).