@alint-js/core
v0.6.0
Published
Public rule and plugin SDK and run engine for alint
Maintainers
Readme
@alint-js/core
The public SDK and run engine for alint.
What it does
This package provides the core SDK and run engine APIs used by plugins, rules, language processors, embedding tools, and agent adapters:
definePluginanddefineRulerunAlint- rule registry and flat config normalization
- source runtime helpers
- built-in JavaScript source extraction, with additional language support for others
- model resolution by size and capability
- diagnostics and progress payload types
- framework-neutral agent contracts under
@alint-js/core/agent - tool-call structured output under
@alint-js/core/structured-output - config DSL and types for advanced SDK consumers
How to use
Write a rule:
import { defineRule } from '@alint-js/core'
export const rule = defineRule({
create: ctx => ({
async onTargetFile(target) {
const model = await ctx.model({ size: 'small' })
const file = await ctx.src.readFile(target.file)
ctx.report({
filePath: target.file.path,
loc: target.loc,
message: `reviewed ${file.lines.length} lines with ${model.id}`,
})
},
}),
})Source and project rules receive compact descriptors. Source handlers should pass target.file
to ctx.src.readFile() so core can detect changes since planning. Read source only when needed:
import { defineRule } from '@alint-js/core'
export const projectRule = defineRule({
create: ctx => ({
async onTargetProject(project) {
for (const entry of project.files) {
const file = await ctx.src.readFile(entry.path)
if (file.text.includes('deprecated-api'))
ctx.report({ filePath: entry.path, message: 'deprecated API used' })
}
},
}),
})PlannedSourceTarget, ProjectFileEntry, and ProjectTargetEntry intentionally omit source text.
Calls to ctx.src.readFile() are explicit and plugin-owned, so a rule controls which source files
it loads and how long it retains them. Cache hits do not invoke the handler and therefore do not
perform its execution-time read.
Use the agent contract for tool-using rules:
import { requireAgent } from '@alint-js/core/agent'
const agent = requireAgent(ctx)Ask a model for one validated, typed result with @alint-js/core/structured-output. It forces
the model to call a single reporting tool whose arguments match a valibot schema, validates
them, and retries with the validation error fed back to the model:
import { generateStructured } from '@alint-js/core/structured-output'
import { array, description, object, pipe } from 'valibot'
const responseSchema = pipe(
object({ findings: array(findingSchema) }),
description('Report findings for this file.'),
)
const { findings } = await generateStructured({
createMessages: retryFeedback => [
{ content: prompt, role: 'system' },
...(retryFeedback ? [{ content: retryFeedback, role: 'user' as const }] : []),
{ content: numberedSource, role: 'user' },
],
logger: ctx.logger,
metering: ctx.metering,
model: await ctx.model(),
operation: 'my-rule-judge',
schema: responseSchema,
})The reporting tool is named reportFindings by default (toolName overrides it) and its
description defaults to the schema's valibot description(...). toolParametersFromSchema,
formatSourceWithLineNumbers, and formatOutputLanguageInstruction are exported for callers
that build their own tools or prompts. Use ctx.agent instead when the model needs to
explore with tools before answering, because a forced tool call is a single shot, not a loop.
Languages
Core parses JavaScript and TypeScript. Everything else is registered by a plugin, so a rule says which languages it can read and core decides what to hand it:
defineRule({
create: () => ({ onTargetFunction: (target) => { /* ... */ } }),
languages: 'any',
})| languages | the rule receives | a named language nothing registered |
| --- | --- | --- |
| omitted | file targets only, never functions or classes | not applicable |
| 'any' | every language except plaintext | never fails |
| ['go', 'rust'] | those languages only | run fails, alint/missing-language |
| { ids: ['go'], skipMissing: true } | those languages only | skipped quietly |
'any' excludes plaintext on purpose. Plain text is what a file falls back to when no language
claims its extension, so a rule that asked for a language would otherwise be handed unparsed text.
When that happens the run reports alint/unregistered-language once per extension — a warning by
default, configurable through linterOptions.reportUnregisteredLanguages.
Declaring a list is the stricter choice. It fails the run when the user has not installed a pack that provides one of them, rather than letting the rule match nothing and look like a pass.
A plugin registers a language by describing how to turn a file into targets:
definePlugin({
languages: {
zig: {
extensions: ['.zig'],
extract: file => [/* SourceTarget[] */],
name: 'zig',
},
},
})Ids are the identifiers editors use — go, python, typescript, plaintext. Registering fails
on a duplicate name or extension, so two plugins can never claim the same language.
Put a FunctionInfo under metadata.function on each function target, and the file's call sites
under metadata.calls on the file target. Those two keys are what let a rule read any language
without a parser of its own.
@alint-js/languages provides Go, Python and Rust this way.
To parse a file the run was not asked to lint, such as when building a workspace-wide index, use
ctx.src.extract(path). It resolves that file's own config and language, and returns nothing for a
file the config ignores rather than throwing.
When to use
- You are writing an
alintplugin or rule package. - You are adding a language processor or source extractor.
- You are embedding
alintin another tool. - You are implementing an
AgentAdapter. - You need project-wide analysis that can consume compact descriptors and load source lazily.
When not to use
- Use
@alint-js/clifor command-line usage and ordinaryalint.config.*files. - Use
@alint-js/configfor setup TOML, config loading, and config-file tooling only. - A plugin that needs a persistent repository database should build and inject that database.
Do not retain every
SourceFilereturned byctx.src.readFile()as a substitute for one.
Memory boundaries
alint bounds concurrent source planning reads and parser work. Planning releases rich extractor
values as soon as compact jobs are admitted; queued jobs do not retain source text. It cannot bound
source files retained by plugin code after ctx.src.readFile() returns.
A valid cache written by the same alint version is still read from one monolithic JSON document.
An extremely large cache may therefore exhaust available memory.
After each cacheable rule job completes, alint writes a cache checkpoint before releasing its
scheduler slot. Each checkpoint atomically replaces the monolithic cache file. An interrupted run
can therefore reuse every result that had already become durable, but large caches can cause
substantial disk writes during runs with many cache misses. Cache hits, skipped jobs, failed jobs,
and rules that opt out of caching do not add checkpoint writes. A checkpoint or final cache write
error is fatal and causes the run to fail.
