@alint-js/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ This package provides the core SDK and run engine APIs used by plugins, rules, l
10
10
  - `runAlint`
11
11
  - rule registry and flat config normalization
12
12
  - source runtime helpers
13
- - built-in JavaScript source extraction
13
+ - built-in JavaScript source extraction, with additional language support for others
14
14
  - model resolution by size and capability
15
15
  - diagnostics and progress payload types
16
16
  - framework-neutral agent contracts under `@alint-js/core/agent`
@@ -105,6 +105,60 @@ description defaults to the schema's valibot `description(...)`. `toolParameters
105
105
  that build their own tools or prompts. Use `ctx.agent` instead when the model needs to
106
106
  explore with tools before answering, because a forced tool call is a single shot, not a loop.
107
107
 
108
+ ### Languages
109
+
110
+ Core parses JavaScript and TypeScript. Everything else is registered by a plugin, so a rule says
111
+ which languages it can read and core decides what to hand it:
112
+
113
+ ```ts
114
+ defineRule({
115
+ create: () => ({ onTargetFunction: (target) => { /* ... */ } }),
116
+ languages: 'any',
117
+ })
118
+ ```
119
+
120
+ | `languages` | the rule receives | a named language nothing registered |
121
+ | --- | --- | --- |
122
+ | omitted | file targets only, never functions or classes | not applicable |
123
+ | `'any'` | every language except `plaintext` | never fails |
124
+ | `['go', 'rust']` | those languages only | run fails, `alint/missing-language` |
125
+ | `{ ids: ['go'], skipMissing: true }` | those languages only | skipped quietly |
126
+
127
+ `'any'` excludes `plaintext` on purpose. Plain text is what a file falls back to when no language
128
+ claims its extension, so a rule that asked for a language would otherwise be handed unparsed text.
129
+ When that happens the run reports `alint/unregistered-language` once per extension — a warning by
130
+ default, configurable through `linterOptions.reportUnregisteredLanguages`.
131
+
132
+ Declaring a list is the stricter choice. It fails the run when the user has not installed a pack
133
+ that provides one of them, rather than letting the rule match nothing and look like a pass.
134
+
135
+ A plugin registers a language by describing how to turn a file into targets:
136
+
137
+ ```ts
138
+ definePlugin({
139
+ languages: {
140
+ zig: {
141
+ extensions: ['.zig'],
142
+ extract: file => [/* SourceTarget[] */],
143
+ name: 'zig',
144
+ },
145
+ },
146
+ })
147
+ ```
148
+
149
+ Ids are the identifiers editors use — `go`, `python`, `typescript`, `plaintext`. Registering fails
150
+ on a duplicate name or extension, so two plugins can never claim the same language.
151
+
152
+ Put a `FunctionInfo` under `metadata.function` on each function target, and the file's call sites
153
+ under `metadata.calls` on the file target. Those two keys are what let a rule read any language
154
+ without a parser of its own.
155
+
156
+ `@alint-js/languages` provides Go, Python and Rust this way.
157
+
158
+ To parse a file the run was not asked to lint, such as when building a workspace-wide index, use
159
+ `ctx.src.extract(path)`. It resolves that file's own config and language, and returns nothing for a
160
+ file the config ignores rather than throwing.
161
+
108
162
  ## When to use
109
163
 
110
164
  - You are writing an `alint` plugin or rule package.
@@ -128,3 +182,10 @@ source files retained by plugin code after `ctx.src.readFile()` returns.
128
182
 
129
183
  A valid cache written by the same `alint` version is still read from one monolithic JSON document.
130
184
  An extremely large cache may therefore exhaust available memory.
185
+
186
+ After each cacheable rule job completes, `alint` writes a cache checkpoint before releasing its
187
+ scheduler slot. Each checkpoint atomically replaces the monolithic cache file. An interrupted run
188
+ can therefore reuse every result that had already become durable, but large caches can cause
189
+ substantial disk writes during runs with many cache misses. Cache hits, skipped jobs, failed jobs,
190
+ and rules that opt out of caching do not add checkpoint writes. A checkpoint or final cache write
191
+ error is fatal and causes the run to fail.
package/dist/agent.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as AgentTool, I as AgentUsage, M as AgentAdapter, N as AgentRequest, P as AgentResult, b as RuleContext } from "./types-deBCDA4_.mjs";
1
+ import { F as AgentAdapter, I as AgentRequest, L as AgentResult, R as AgentTool, x as RuleContext, z as AgentUsage } from "./types-Cm5Z7Fll.mjs";
2
2
  //#region src/agent/retry.d.ts
3
3
  /**
4
4
  * Declares that the complete adapter invocation can be safely replayed.
@@ -1,4 +1,4 @@
1
- import { E as RuleOptionsSchema, i as AlintConfigItem, p as PluginDefinition, t as AlintConfig, w as RuleOptionsInput, x as RuleDefinition, y as RuleConfigEntry } from "./types-deBCDA4_.mjs";
1
+ import { D as RuleOptionsSchema, S as RuleDefinition, T as RuleOptionsInput, b as RuleConfigEntry, i as AlintConfigItem, m as PluginDefinition, t as AlintConfig } from "./types-Cm5Z7Fll.mjs";
2
2
  //#region src/dsl/define.d.ts
3
3
  type AnyRuleDefinition = RuleDefinition<any>;
4
4
  type ConfigItemWithPluginRules<Plugins, Rules> = Omit<AlintConfigItem, 'plugins' | 'rules'> & {
package/dist/dsl.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { A as RuleWithHandler, C as RuleInferenceUsageRecord, D as RuleRegistry, E as RuleOptionsSchema, O as RuleSeverity, S as RuleHandlers, T as RuleOptionsOutput, _ as ProjectTargetEntry, a as AlintLinterOptions, b as RuleContext, c as DiagnosticLocation, d as IgnoreConfig, f as LanguageDefinition, g as ProjectTarget, h as ProjectFileEntry, i as AlintConfigItem, j as Target, k as RuleSpecializedHandlers, l as DirectoryTarget, m as ProcessorDefinition, n as AlintConfigExtends, o as Awaitable, p as PluginDefinition, r as AlintConfigInput, s as DiagnosticDescriptor, t as AlintConfig, u as EnabledRule, v as RuleCacheConfig, w as RuleOptionsInput, x as RuleDefinition, y as RuleConfigEntry } from "./types-deBCDA4_.mjs";
2
- import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-CJ2QJPZ8.mjs";
3
- export { type AlintConfig, type AlintConfigExtends, type AlintConfigInput, type AlintConfigItem, type AlintLinterOptions, type Awaitable, type DiagnosticDescriptor, type DiagnosticLocation, type DirectoryTarget, type EnabledRule, type IgnoreConfig, type LanguageDefinition, type PluginDefinition, type ProcessorDefinition, type ProjectFileEntry, type ProjectTarget, type ProjectTargetEntry, type RuleCacheConfig, type RuleConfigEntry, type RuleContext, type RuleDefinition, type RuleHandlers, type RuleInferenceUsageRecord, type RuleOptionsInput, type RuleOptionsOutput, type RuleOptionsSchema, type RuleRegistry, type RuleSeverity, type RuleSpecializedHandlers, type RuleWithHandler, type Target, defineConfig, definePlugin, defineRule };
1
+ import { A as RuleSpecializedHandlers, C as RuleHandlers, D as RuleOptionsSchema, E as RuleOptionsOutput, M as StopGateConfig, N as StopGateTarget, O as RuleRegistry, P as Target, S as RuleDefinition, T as RuleOptionsInput, _ as ProjectTarget, a as AlintLinterOptions, b as RuleConfigEntry, c as DiagnosticLocation, d as IgnoreConfig, f as IntegrationsConfig, g as ProjectFileEntry, h as ProcessorDefinition, i as AlintConfigItem, j as RuleWithHandler, k as RuleSeverity, l as DirectoryTarget, m as PluginDefinition, n as AlintConfigExtends, o as Awaitable, p as LanguageDefinition, r as AlintConfigInput, s as DiagnosticDescriptor, t as AlintConfig, u as EnabledRule, v as ProjectTargetEntry, w as RuleInferenceUsageRecord, x as RuleContext, y as RuleCacheConfig } from "./types-Cm5Z7Fll.mjs";
2
+ import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-DO36VJ4W.mjs";
3
+ export { type AlintConfig, type AlintConfigExtends, type AlintConfigInput, type AlintConfigItem, type AlintLinterOptions, type Awaitable, type DiagnosticDescriptor, type DiagnosticLocation, type DirectoryTarget, type EnabledRule, type IgnoreConfig, type IntegrationsConfig, type LanguageDefinition, type PluginDefinition, type ProcessorDefinition, type ProjectFileEntry, type ProjectTarget, type ProjectTargetEntry, type RuleCacheConfig, type RuleConfigEntry, type RuleContext, type RuleDefinition, type RuleHandlers, type RuleInferenceUsageRecord, type RuleOptionsInput, type RuleOptionsOutput, type RuleOptionsSchema, type RuleRegistry, type RuleSeverity, type RuleSpecializedHandlers, type RuleWithHandler, type StopGateConfig, type StopGateTarget, type Target, defineConfig, definePlugin, defineRule };
@@ -1,7 +1,7 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { extname } from "node:path";
3
4
  import { clamp } from "es-toolkit";
4
- import { createHash } from "node:crypto";
5
5
  import { parseSync } from "oxc-parser";
6
6
  //#region src/core/hash.ts
7
7
  function createStableHasher() {
@@ -173,11 +173,11 @@ function targetIdentity(kind, name, range) {
173
173
  /**
174
174
  * Swaps a target's positional identity for its name, where that name is unique among these targets.
175
175
  *
176
- * Identities key caches, so a function that only moved should keep the identity it had. Two targets
177
- * sharing a name cannot be told apart that way, so both keep the positional form.
176
+ * Caches are keyed by identity, so a function that only moved should keep the identity it had. Two
177
+ * targets sharing a name cannot be told apart by name, so those keep the positional form.
178
178
  *
179
- * Run every function and class target through this. Skip it in one producer and the same function
180
- * ends up with a different identity depending on which language read it.
179
+ * Run every function and class target through this. Miss it in one language and the same function
180
+ * gets a different identity depending on which language read it.
181
181
  */
182
182
  function withStableIdentities(targets) {
183
183
  const nameCounts = /* @__PURE__ */ new Map();
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as RuleWithHandler, B as ResolvedProvider, C as RuleInferenceUsageRecord, D as RuleRegistry, G as RunnerStatsConfig, H as ProviderDefinition, K as SetupConfig, L as ModelRequirement, O as RuleSeverity, R as ResolveModelOptions, S as RuleHandlers, U as ProviderType, V as ModelSize, W as RunnerConfig, _ as ProjectTargetEntry, a as AlintLinterOptions, b as RuleContext, c as DiagnosticLocation, d as IgnoreConfig, f as LanguageDefinition, g as ProjectTarget, h as ProjectFileEntry, i as AlintConfigItem, j as Target, k as RuleSpecializedHandlers, l as DirectoryTarget, m as ProcessorDefinition, n as AlintConfigExtends, o as Awaitable, p as PluginDefinition, q as SetupModelDefinition, r as AlintConfigInput, s as DiagnosticDescriptor, t as AlintConfig, u as EnabledRule, v as RuleCacheConfig, x as RuleDefinition, y as RuleConfigEntry, z as ResolvedModel } from "./types-deBCDA4_.mjs";
1
+ import { A as RuleSpecializedHandlers, B as ModelRequirement, C as RuleHandlers, G as ProviderDefinition, H as ResolvedModel, J as RunnerStatsConfig, K as ProviderType, M as StopGateConfig, N as StopGateTarget, O as RuleRegistry, P as Target, S as RuleDefinition, U as ResolvedProvider, V as ResolveModelOptions, W as ModelSize, X as SetupModelDefinition, Y as SetupConfig, _ as ProjectTarget, a as AlintLinterOptions, b as RuleConfigEntry, c as DiagnosticLocation, d as IgnoreConfig, f as IntegrationsConfig, g as ProjectFileEntry, h as ProcessorDefinition, i as AlintConfigItem, j as RuleWithHandler, k as RuleSeverity, l as DirectoryTarget, m as PluginDefinition, n as AlintConfigExtends, o as Awaitable, p as LanguageDefinition, q as RunnerConfig, r as AlintConfigInput, s as DiagnosticDescriptor, t as AlintConfig, u as EnabledRule, v as ProjectTargetEntry, w as RuleInferenceUsageRecord, x as RuleContext, y as RuleCacheConfig } from "./types-Cm5Z7Fll.mjs";
2
2
  import { C as SourceTarget, D as SourceTargetOrigin, E as SourceTargetOfKind, O as SourceText, S as SourceRuntime, T as SourceTargetMetadata, _ as SourceLocation, a as FunctionInfo, b as SourcePosition, c as LineRange, d as ProcessedSource, f as ProcessedSourceOrigin, g as SourceFile, h as SourceExtractOptions, i as FileTarget, l as PlannedSourceTarget, m as ProcessorPostprocessContext, n as CallSite, o as FunctionTarget, p as ProcessorContext, r as ClassTarget, s as LanguageContext, t as BaseSourceFile, u as PlannedSourceTargetOfKind, v as SourceMetadataObject, w as SourceTargetKind, x as SourceRange, y as SourceMetadataValue } from "./types-DM4idR4w.mjs";
3
- import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-CJ2QJPZ8.mjs";
3
+ import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-DO36VJ4W.mjs";
4
4
  //#region src/config/config-array.d.ts
5
5
  interface EffectiveAlintConfig {
6
6
  agent?: AlintConfigItem['agent'];
@@ -32,6 +32,19 @@ declare function resolveConfigForDirectory(directoryPath: string, input: AlintCo
32
32
  declare function resolveConfigForFile(filePath: string, input: AlintConfig, options: ResolveConfigOptions): ResolveConfigResult;
33
33
  declare function resolveConfigForProject(projectPath: string, input: AlintConfig, options: ResolveConfigOptions): ResolveConfigResult;
34
34
  //#endregion
35
+ //#region src/config/stop-gate.d.ts
36
+ interface ResolvedStopGateConfig {
37
+ enabled: boolean;
38
+ target: StopGateTarget;
39
+ timeoutMs: number;
40
+ }
41
+ declare const maximumStopGateTimeoutMs: number;
42
+ declare const defaultStopGateConfig: Readonly<ResolvedStopGateConfig>;
43
+ declare function resolveStopGateConfig(config: AlintConfig, cwd: string): ResolvedStopGateConfig;
44
+ //#endregion
45
+ //#region src/core/hash.d.ts
46
+ declare function stableHash(value: unknown): string;
47
+ //#endregion
35
48
  //#region src/core/languages/registry.d.ts
36
49
  interface LanguageRegistry {
37
50
  byExtension: Map<string, string>;
@@ -270,6 +283,10 @@ declare class AlintAbortError extends AlintRunCancelledError {
270
283
  cause?: unknown;
271
284
  });
272
285
  }
286
+ declare class AlintCachePersistenceError extends Error {
287
+ readonly result: RunResult;
288
+ constructor(result: RunResult, cause: unknown);
289
+ }
273
290
  declare class AlintRunError extends Error {
274
291
  readonly failures: AlintRunFailure[];
275
292
  readonly result: RunResult;
@@ -286,11 +303,11 @@ declare function targetIdentity(kind: SourceTargetKind, name: string | undefined
286
303
  /**
287
304
  * Swaps a target's positional identity for its name, where that name is unique among these targets.
288
305
  *
289
- * Identities key caches, so a function that only moved should keep the identity it had. Two targets
290
- * sharing a name cannot be told apart that way, so both keep the positional form.
306
+ * Caches are keyed by identity, so a function that only moved should keep the identity it had. Two
307
+ * targets sharing a name cannot be told apart by name, so those keep the positional form.
291
308
  *
292
- * Run every function and class target through this. Skip it in one producer and the same function
293
- * ends up with a different identity depending on which language read it.
309
+ * Run every function and class target through this. Miss it in one language and the same function
310
+ * gets a different identity depending on which language read it.
294
311
  */
295
312
  declare function withStableIdentities(targets: readonly SourceTarget[]): SourceTarget[];
296
313
  //#endregion
@@ -380,4 +397,4 @@ declare function benchmarkModels(config: SetupConfig, options?: BenchmarkModelsO
380
397
  //#region src/models/resolve.d.ts
381
398
  declare function resolveModel(registry: SetupConfig, options?: ResolveModelOptions): ResolvedModel;
382
399
  //#endregion
383
- export { AlintAbortError, type AlintConfig, type AlintConfigExtends, type AlintConfigInput, type AlintConfigItem, type AlintFailureCause, type AlintFileFailure, type AlintLinterOptions, type AlintRuleFailure, AlintRunCancelledError, AlintRunError, type AlintRunFailure, type Awaitable, type BaseSourceFile, type BenchmarkModelsOptions, type CallSite, type ClassTarget, type Diagnostic, type DiagnosticDescriptor, type DiagnosticLocation, type DiagnosticProgressPayload, type DirectoryTarget, type EffectiveAlintConfig, type EnabledRule, type ExecutionCounts, type ExecutionProgressPayload, type FileReadyPayload, type FileTarget, type FunctionInfo, type FunctionTarget, type IgnoreConfig, type InferenceUsageRecord, type JobEndPayload, type JobQueuedPayload, type JobRetryPayload, type JobStartPayload, type LanguageContext, type LanguageDefinition, type LanguageRegistry, type LineRange, type ModelBenchmarkActive, type ModelBenchmarkMeasurement, type ModelBenchmarkProgress, type ModelBenchmarkRequest, type ModelBenchmarkResult, type ModelRequirement, type ModelSize, type PlannedSourceTarget, type PlannedSourceTargetOfKind, type PluginDefinition, type PrepareEndPayload, type PrepareStartPayload, type ProcessedSource, type ProcessedSourceOrigin, type ProcessorContext, type ProcessorDefinition, type ProcessorPostprocessContext, type ProgressJobRef, type ProgressReporter, type ProgressSnapshot, type ProgressTargetKind, type ProjectFileEntry, type ProjectTarget, type ProjectTargetEntry, type ProviderDefinition, type ProviderType, type ResolveConfigResult, type ResolveLanguageOptions, type ResolveModelOptions, type ResolvedModel, type ResolvedProvider, type RuleCacheConfig, type RuleConfigEntry, type RuleContext, type RuleDefinition, type RuleHandlers, type RuleInferenceUsageRecord, type RuleRegistry, type RuleSeverity, type RuleSpecializedHandlers, type RuleWithHandler, type RunEndPayload, type RunExecution, type RunOptions, type RunResult, type RunUsage, type RunUsageTotals, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, type SourceExtractOptions, type SourceFile, type SourceLocation, type SourceMetadataObject, type SourceMetadataValue, type SourcePosition, type SourceRange, type SourceRuntime, type SourceRuntimeOptions, type SourceTarget, type SourceTargetKind, type SourceTargetMetadata, type SourceTargetOfKind, type SourceTargetOrigin, type SourceText, type Target, type UsageProgressPayload, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange, targetIdentity, withLanguage, withStableIdentities };
400
+ export { AlintAbortError, AlintCachePersistenceError, type AlintConfig, type AlintConfigExtends, type AlintConfigInput, type AlintConfigItem, type AlintFailureCause, type AlintFileFailure, type AlintLinterOptions, type AlintRuleFailure, AlintRunCancelledError, AlintRunError, type AlintRunFailure, type Awaitable, type BaseSourceFile, type BenchmarkModelsOptions, type CallSite, type ClassTarget, type Diagnostic, type DiagnosticDescriptor, type DiagnosticLocation, type DiagnosticProgressPayload, type DirectoryTarget, type EffectiveAlintConfig, type EnabledRule, type ExecutionCounts, type ExecutionProgressPayload, type FileReadyPayload, type FileTarget, type FunctionInfo, type FunctionTarget, type IgnoreConfig, type InferenceUsageRecord, type IntegrationsConfig, type JobEndPayload, type JobQueuedPayload, type JobRetryPayload, type JobStartPayload, type LanguageContext, type LanguageDefinition, type LanguageRegistry, type LineRange, type ModelBenchmarkActive, type ModelBenchmarkMeasurement, type ModelBenchmarkProgress, type ModelBenchmarkRequest, type ModelBenchmarkResult, type ModelRequirement, type ModelSize, type PlannedSourceTarget, type PlannedSourceTargetOfKind, type PluginDefinition, type PrepareEndPayload, type PrepareStartPayload, type ProcessedSource, type ProcessedSourceOrigin, type ProcessorContext, type ProcessorDefinition, type ProcessorPostprocessContext, type ProgressJobRef, type ProgressReporter, type ProgressSnapshot, type ProgressTargetKind, type ProjectFileEntry, type ProjectTarget, type ProjectTargetEntry, type ProviderDefinition, type ProviderType, type ResolveConfigResult, type ResolveLanguageOptions, type ResolveModelOptions, type ResolvedModel, type ResolvedProvider, type ResolvedStopGateConfig, type RuleCacheConfig, type RuleConfigEntry, type RuleContext, type RuleDefinition, type RuleHandlers, type RuleInferenceUsageRecord, type RuleRegistry, type RuleSeverity, type RuleSpecializedHandlers, type RuleWithHandler, type RunEndPayload, type RunExecution, type RunOptions, type RunResult, type RunUsage, type RunUsageTotals, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, type SourceExtractOptions, type SourceFile, type SourceLocation, type SourceMetadataObject, type SourceMetadataValue, type SourcePosition, type SourceRange, type SourceRuntime, type SourceRuntimeOptions, type SourceTarget, type SourceTargetKind, type SourceTargetMetadata, type SourceTargetOfKind, type SourceTargetOrigin, type SourceText, type StopGateConfig, type StopGateTarget, type Target, type UsageProgressPayload, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defaultStopGateConfig, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, resolveStopGateConfig, runAlint, sliceLines, sliceRange, stableHash, targetIdentity, withLanguage, withStableIdentities };
package/dist/index.mjs CHANGED
@@ -1,15 +1,17 @@
1
1
  import { o as withAgentRetry, t as combineAbortSignals } from "./agent-B6GG68NX.mjs";
2
2
  import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-Da342Fcx.mjs";
3
- import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-DoHeLdG1.mjs";
3
+ import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-DhLFgRej.mjs";
4
4
  import { minimatch } from "minimatch";
5
5
  import { basename, extname, isAbsolute, relative, resolve } from "pathe";
6
- import { access, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
7
- import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
6
+ import { array, boolean, check, custom, finite, getDotPath, integer, literal, looseObject, maxValue, minValue, number, object, optional, parse, picklist, pipe, record, safeParse, string, union, unknown } from "valibot";
8
7
  import { randomUUID } from "node:crypto";
8
+ import { access, appendFile, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
9
+ import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
9
10
  import process, { cwd } from "node:process";
10
- import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
11
11
  import { Buffer } from "node:buffer";
12
12
  import { statSync } from "node:fs";
13
+ import lockfile from "proper-lockfile";
14
+ import { isNodeErrorCode } from "@alint-js/utils/node";
13
15
  import { errorMessageFrom } from "@moeru/std/error";
14
16
  import { AsyncLocalStorage } from "node:async_hooks";
15
17
  import { streamText } from "@xsai/stream-text";
@@ -236,6 +238,37 @@ function withExpansionMetadata(item, inheritedMatcherScopes, effectiveBasePath)
236
238
  return item;
237
239
  }
238
240
  //#endregion
241
+ //#region src/config/stop-gate.ts
242
+ const maximumStopGateTimeoutMs = 861e5;
243
+ const defaultStopGateConfig = {
244
+ enabled: false,
245
+ target: "dirty-files",
246
+ timeoutMs: 9e5
247
+ };
248
+ const stopGateConfigSchema = looseObject({
249
+ enabled: optional(boolean("integrations.stopGate.enabled must be a boolean.")),
250
+ target: optional(picklist(["all", "dirty-files"], "integrations.stopGate.target must be \"all\" or \"dirty-files\".")),
251
+ timeoutMs: optional(pipe(number(`integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), integer(`integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), minValue(1, `integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), maxValue(maximumStopGateTimeoutMs, `integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`)))
252
+ }, "integrations.stopGate must be an object.");
253
+ function resolveStopGateConfig(config, cwd) {
254
+ const project = resolveConfigForProject(cwd, config, { cwd });
255
+ for (const skipped of project.skipped) if (skipped.item.integrations?.stopGate !== void 0) throw new Error("integrations.stopGate must be declared in a global config item without files, directories, or ignores.");
256
+ return project.matched.reduce((resolved, item) => mergeStopGateConfig(resolved, item), { ...defaultStopGateConfig });
257
+ }
258
+ function assertStopGateConfig(value) {
259
+ parse(stopGateConfigSchema, value);
260
+ }
261
+ function mergeStopGateConfig(resolved, item) {
262
+ const stopGate = item.integrations?.stopGate;
263
+ if (stopGate === void 0) return resolved;
264
+ assertStopGateConfig(stopGate);
265
+ return {
266
+ enabled: stopGate.enabled ?? resolved.enabled,
267
+ target: stopGate.target ?? resolved.target,
268
+ timeoutMs: stopGate.timeoutMs ?? resolved.timeoutMs
269
+ };
270
+ }
271
+ //#endregion
239
272
  //#region src/core/languages/js/index.ts
240
273
  const javascriptLanguage = {
241
274
  extensions: [
@@ -299,7 +332,10 @@ function resolveLanguage(file, registry, options) {
299
332
  function resolveLanguageForPath(filePath, registry, options) {
300
333
  const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
301
334
  const language = registry.languages.get(languageName);
302
- if (!language) throw new Error(`Unknown language "${languageName}".`);
335
+ if (!language) {
336
+ const registered = [...registry.languages.keys()].sort().join(", ");
337
+ throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
338
+ }
303
339
  return language;
304
340
  }
305
341
  //#endregion
@@ -602,13 +638,16 @@ function recordMissingLanguages(into, rules, registry, path) {
602
638
  }
603
639
  //#endregion
604
640
  //#region package.json
605
- var version = "0.4.0";
641
+ var version = "0.6.0";
606
642
  //#endregion
607
643
  //#region src/core/cache/types.ts
608
644
  const CACHE_MAGIC = "ALINT_CACHE";
609
645
  //#endregion
610
646
  //#region src/core/cache/store.ts
611
647
  const DEFAULT_CACHE_FILE_NAME = ".alintcache";
648
+ const LOCK_STALE_MS = 2e4;
649
+ const LOCK_UPDATE_MS = 3e3;
650
+ const METADATA_READ_LIMIT = 4096;
612
651
  const FiniteNumberSchema = pipe(number(), finite());
613
652
  const PositionSchema = object({
614
653
  column: FiniteNumberSchema,
@@ -668,18 +707,19 @@ const CacheEntrySchema = object({
668
707
  }),
669
708
  usage: array(UsageSchema)
670
709
  });
710
+ const CachedOwnerSchema = object({
711
+ contentHash: optional(string()),
712
+ kind: union([literal("file"), literal("project")]),
713
+ path: string(),
714
+ slots: array(string())
715
+ });
671
716
  function objectRecord(value) {
672
717
  return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
673
718
  }
674
719
  const CacheFileBodySchema = pipe(object({
675
720
  createdAt: string(),
676
721
  entries: objectRecord(CacheEntrySchema),
677
- owners: objectRecord(object({
678
- contentHash: optional(string()),
679
- kind: union([literal("file"), literal("project")]),
680
- path: string(),
681
- slots: array(string())
682
- })),
722
+ owners: objectRecord(CachedOwnerSchema),
683
723
  updatedAt: string()
684
724
  }), check((body) => {
685
725
  const entryKeys = Object.keys(body.entries);
@@ -687,22 +727,68 @@ const CacheFileBodySchema = pipe(object({
687
727
  const uniqueSlots = new Set(referencedSlots);
688
728
  return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
689
729
  }));
730
+ const CacheMetadataSchema = object({
731
+ alintVersion: string(),
732
+ createdAt: string(),
733
+ magic: literal(CACHE_MAGIC),
734
+ schemaVersion: literal(2),
735
+ type: literal("metadata")
736
+ });
737
+ const PutEventSchema = object({
738
+ at: string(),
739
+ entry: CacheEntrySchema,
740
+ owner: CachedOwnerSchema,
741
+ ownerKey: string(),
742
+ slotKey: string(),
743
+ type: literal("put")
744
+ });
745
+ const ReplaceOwnerEventSchema = object({
746
+ at: string(),
747
+ entries: array(object({
748
+ entry: CacheEntrySchema,
749
+ slotKey: string()
750
+ })),
751
+ mode: union([literal("merge"), literal("replace")]),
752
+ owner: CachedOwnerSchema,
753
+ ownerKey: string(),
754
+ type: literal("replace-owner")
755
+ });
756
+ const RemoveOwnerEventSchema = object({
757
+ at: string(),
758
+ ownerKey: string(),
759
+ type: literal("remove-owner")
760
+ });
761
+ const CacheEventSchema = union([
762
+ PutEventSchema,
763
+ ReplaceOwnerEventSchema,
764
+ RemoveOwnerEventSchema
765
+ ]);
690
766
  async function createCacheStore(options) {
691
767
  const location = resolveCacheLocation(options.cwd, options.location);
692
768
  if (!options.enabled) return createNoopCacheStore(location);
693
769
  const alintVersion = options.alintVersion ?? version;
694
- const header = `${CACHE_MAGIC} 2 ${alintVersion}`;
695
- const body = await loadCacheBody(location, header, options.readOnly === true);
696
- if (options.readOnly) return createReadOnlyCacheStore(location, body, options.cwd);
770
+ const initial = await loadCacheLog(location, alintVersion);
771
+ if (options.readOnly) return createReadOnlyCacheStore(location, initial.body, options.cwd);
772
+ const dependencies = {
773
+ append: options.appendFile ?? appendFile,
774
+ lock: options.lock ?? { acquire: lockfile.lock },
775
+ write: options.writeFile ?? writeFile
776
+ };
777
+ const trimmed = await trimOnce(location, alintVersion, initial, dependencies);
778
+ const writer = createEventWriter(location, trimmed.metadata, dependencies);
697
779
  const fileExists = options.fileExists ?? defaultFileExists;
698
- const writeFile$1 = options.writeFile ?? writeFile;
699
780
  return {
700
- beginOwner: (owner) => beginOwner(body, owner, options.cwd),
781
+ beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
782
+ flush: () => writer.flush(false),
701
783
  location,
702
784
  reconcile: async () => {
703
- await collectMissingFileOwners(body, options.cwd, fileExists);
704
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
705
- await persistCacheBody(location, header, body, writeFile$1);
785
+ const removedOwners = await collectMissingFileOwners(trimmed.body, options.cwd, fileExists);
786
+ for (const ownerKey of removedOwners) writer.enqueue({
787
+ at: (/* @__PURE__ */ new Date()).toISOString(),
788
+ ownerKey,
789
+ type: "remove-owner"
790
+ });
791
+ await writer.flush(true);
706
792
  }
707
793
  };
708
794
  }
@@ -754,7 +840,50 @@ function resolveCacheLocation(cwd, location) {
754
840
  } catch {}
755
841
  return resolved;
756
842
  }
757
- function beginOwner(body, owner, cwd) {
843
+ async function appendBatch(location, payload, append) {
844
+ const handle = await open(location, "a+");
845
+ try {
846
+ const { size } = await handle.stat();
847
+ let separator = "";
848
+ if (size > 0) {
849
+ const finalByte = Buffer.alloc(1);
850
+ await handle.read(finalByte, 0, 1, size - 1);
851
+ separator = finalByte[0] === 10 ? "" : "\n";
852
+ }
853
+ await append(location, `${separator}${payload}`, "utf8");
854
+ } finally {
855
+ await handle.close();
856
+ }
857
+ }
858
+ function applyEvent(body, event) {
859
+ if (event.type === "remove-owner") {
860
+ removeOwner(body, event.ownerKey);
861
+ body.updatedAt = event.at;
862
+ return;
863
+ }
864
+ if (event.type === "put") {
865
+ const current = body.owners[event.ownerKey];
866
+ const slots = new Set(current?.slots ?? []);
867
+ slots.add(event.slotKey);
868
+ body.entries[event.slotKey] = event.entry;
869
+ body.owners[event.ownerKey] = {
870
+ ...event.owner,
871
+ slots: [...slots].sort()
872
+ };
873
+ body.updatedAt = event.at;
874
+ return;
875
+ }
876
+ const entries = event.mode === "merge" ? ownerEntries(body, body.owners[event.ownerKey]) : /* @__PURE__ */ new Map();
877
+ for (const item of event.entries) entries.set(item.slotKey, item.entry);
878
+ for (const slot of body.owners[event.ownerKey]?.slots ?? []) delete body.entries[slot];
879
+ for (const [slotKey, entry] of entries) body.entries[slotKey] = entry;
880
+ body.owners[event.ownerKey] = {
881
+ ...event.owner,
882
+ slots: [...entries.keys()].sort()
883
+ };
884
+ body.updatedAt = event.at;
885
+ }
886
+ function beginOwner(body, owner, cwd, writer, ownerMetadata = {}) {
758
887
  const normalizedOwner = {
759
888
  kind: owner.kind,
760
889
  path: normalizeCachePath(cwd, owner.path)
@@ -762,22 +891,64 @@ function beginOwner(body, owner, cwd) {
762
891
  const key = ownerKey(normalizedOwner, cwd);
763
892
  const previousOwner = body.owners[key];
764
893
  const nextEntries = /* @__PURE__ */ new Map();
894
+ const checkpointEntries = /* @__PURE__ */ new Map();
895
+ const commit = (metadata = {}) => {
896
+ const contentHash = metadata.contentHash ?? ownerMetadata.contentHash;
897
+ const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
898
+ for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
899
+ const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
900
+ for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
901
+ for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
902
+ const cachedOwner = {
903
+ contentHash,
904
+ kind: normalizedOwner.kind,
905
+ path: normalizedOwner.path,
906
+ slots: [...committedEntries.keys()].sort()
907
+ };
908
+ body.owners[key] = cachedOwner;
909
+ body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
910
+ writer.enqueue({
911
+ at: body.updatedAt,
912
+ entries: [...committedEntries].map(([slotKey, entry]) => ({
913
+ entry,
914
+ slotKey
915
+ })),
916
+ mode: metadata.mode ?? "replace",
917
+ owner: cachedOwner,
918
+ ownerKey: key,
919
+ type: "replace-owner"
920
+ });
921
+ checkpointEntries.clear();
922
+ };
765
923
  return {
766
- commit: (metadata = {}) => {
767
- const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
768
- for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
769
- const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
770
- for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
771
- for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
772
- body.owners[key] = {
773
- contentHash: metadata.contentHash,
924
+ checkpoint: async () => {
925
+ const cachedOwner = {
926
+ contentHash: ownerMetadata.contentHash,
774
927
  kind: normalizedOwner.kind,
775
928
  path: normalizedOwner.path,
776
- slots: [...committedEntries.keys()].sort()
929
+ slots: []
777
930
  };
778
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
931
+ for (const [entryKey, cacheEntry] of checkpointEntries) {
932
+ const event = {
933
+ at: (/* @__PURE__ */ new Date()).toISOString(),
934
+ entry: cacheEntry,
935
+ owner: cachedOwner,
936
+ ownerKey: key,
937
+ slotKey: entryKey,
938
+ type: "put"
939
+ };
940
+ applyEvent(body, event);
941
+ writer.enqueue(event);
942
+ }
943
+ checkpointEntries.clear();
944
+ await writer.flush(false);
945
+ },
946
+ commit,
947
+ discard: (cacheSlot) => {
948
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
949
+ nextEntries.delete(entryKey);
950
+ checkpointEntries.delete(entryKey);
779
951
  },
780
- discard: (cacheSlot) => nextEntries.delete(slotKey(normalizedOwner, cacheSlot, cwd)),
781
952
  lookup: (cacheSlot, fingerprint) => {
782
953
  const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
783
954
  const cached = body.entries[entryKey];
@@ -785,15 +956,21 @@ function beginOwner(body, owner, cwd) {
785
956
  nextEntries.set(entryKey, cached);
786
957
  return cached;
787
958
  },
788
- put: (cacheSlot, cacheEntry) => nextEntries.set(slotKey(normalizedOwner, cacheSlot, cwd), cacheEntry)
959
+ put: (cacheSlot, cacheEntry) => {
960
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
961
+ nextEntries.set(entryKey, cacheEntry);
962
+ checkpointEntries.set(entryKey, cacheEntry);
963
+ }
789
964
  };
790
965
  }
791
966
  async function collectMissingFileOwners(body, cwd, fileExists) {
967
+ const removed = [];
792
968
  for (const [key, owner] of Object.entries(body.owners)) {
793
969
  if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
794
- for (const entryKey of owner.slots) delete body.entries[entryKey];
795
- delete body.owners[key];
970
+ removeOwner(body, key);
971
+ removed.push(key);
796
972
  }
973
+ return removed;
797
974
  }
798
975
  function createBaseTargetIdentity(target) {
799
976
  if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
@@ -802,17 +979,53 @@ function createBaseTargetIdentity(target) {
802
979
  if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
803
980
  return target.kind;
804
981
  }
805
- function createEmptyCacheBody() {
806
- const now = (/* @__PURE__ */ new Date()).toISOString();
982
+ function createEmptyCacheBody(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
807
983
  return {
808
- createdAt: now,
984
+ createdAt,
809
985
  entries: {},
810
986
  owners: {},
811
- updatedAt: now
987
+ updatedAt: createdAt
988
+ };
989
+ }
990
+ function createEventWriter(location, metadata, dependencies) {
991
+ const backlog = [];
992
+ let validationError;
993
+ let previousWrite = Promise.resolve();
994
+ const persistBacklog = async () => {
995
+ if (validationError) throw validationError;
996
+ if (backlog.length === 0) return;
997
+ await mkdir(dirname(location), { recursive: true });
998
+ await withCacheLock(location, dependencies.lock, getCacheLockOptions({
999
+ factor: 2,
1000
+ maxTimeout: 16e3,
1001
+ minTimeout: 2e3,
1002
+ retries: 4
1003
+ }), async () => {
1004
+ await ensureCompatibleMetadata(location, metadata, dependencies.write);
1005
+ const batchSize = backlog.length;
1006
+ await appendBatch(location, backlog.slice(0, batchSize).map((event) => `${JSON.stringify(event)}\n`).join(""), dependencies.append);
1007
+ backlog.splice(0, batchSize);
1008
+ });
1009
+ validationError = void 0;
1010
+ };
1011
+ return {
1012
+ enqueue: (event) => {
1013
+ try {
1014
+ backlog.push(parse(CacheEventSchema, event));
1015
+ } catch (error) {
1016
+ validationError ??= error;
1017
+ }
1018
+ },
1019
+ flush: (finalAttempt) => {
1020
+ const write = previousWrite.then(persistBacklog);
1021
+ previousWrite = write.catch(() => {});
1022
+ return finalAttempt ? write : write.catch(() => {});
1023
+ }
812
1024
  };
813
1025
  }
814
1026
  function createNoopCacheStore(location) {
815
1027
  const transaction = {
1028
+ checkpoint: async () => {},
816
1029
  commit: () => {},
817
1030
  discard: () => {},
818
1031
  lookup: () => void 0,
@@ -820,22 +1033,24 @@ function createNoopCacheStore(location) {
820
1033
  };
821
1034
  return {
822
1035
  beginOwner: () => transaction,
1036
+ flush: async () => {},
823
1037
  location,
824
1038
  reconcile: async () => {}
825
1039
  };
826
1040
  }
827
1041
  function createReadOnlyCacheStore(location, body, cwd) {
828
- const transaction = (owner) => ({
829
- commit: () => {},
830
- discard: () => {},
831
- lookup: (slot, fingerprint) => {
832
- const cached = body.entries[slotKey(owner, slot, cwd)];
833
- return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
834
- },
835
- put: () => {}
836
- });
837
1042
  return {
838
- beginOwner: transaction,
1043
+ beginOwner: (owner) => ({
1044
+ checkpoint: async () => {},
1045
+ commit: () => {},
1046
+ discard: () => {},
1047
+ lookup: (slot, fingerprint) => {
1048
+ const cached = body.entries[slotKey(owner, slot, cwd)];
1049
+ return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
1050
+ },
1051
+ put: () => {}
1052
+ }),
1053
+ flush: async () => {},
839
1054
  location,
840
1055
  reconcile: async () => {}
841
1056
  };
@@ -849,9 +1064,38 @@ async function defaultFileExists(path) {
849
1064
  throw error;
850
1065
  }
851
1066
  }
1067
+ function emptyLoadedLog(alintVersion) {
1068
+ const metadata = {
1069
+ alintVersion,
1070
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1071
+ magic: CACHE_MAGIC,
1072
+ schemaVersion: 2,
1073
+ type: "metadata"
1074
+ };
1075
+ return {
1076
+ body: createEmptyCacheBody(metadata.createdAt),
1077
+ metadata
1078
+ };
1079
+ }
1080
+ async function ensureCompatibleMetadata(location, metadata, write) {
1081
+ try {
1082
+ if (parseMetadataLine(await readMetadataLine(location), metadata.alintVersion)) return;
1083
+ } catch (error) {
1084
+ if (!isMissingFileError(error)) throw error;
1085
+ }
1086
+ await write(location, `${JSON.stringify(metadata)}\n`);
1087
+ }
852
1088
  function fingerprintsEqual(left, right) {
853
1089
  return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
854
1090
  }
1091
+ function getCacheLockOptions(retries) {
1092
+ return {
1093
+ realpath: false,
1094
+ retries,
1095
+ stale: LOCK_STALE_MS,
1096
+ update: LOCK_UPDATE_MS
1097
+ };
1098
+ }
855
1099
  function hasJsonArrayProperties(input, ancestors) {
856
1100
  let arrayLength;
857
1101
  let indexCount = 0;
@@ -863,7 +1107,8 @@ function hasJsonArrayProperties(input, ancestors) {
863
1107
  arrayLength = descriptor.value;
864
1108
  continue;
865
1109
  }
866
- if (typeof key !== "string" || !isCanonicalArrayIndex(key) || !descriptor.enumerable) return false;
1110
+ const index = typeof key === "string" ? Number(key) : NaN;
1111
+ if (typeof key !== "string" || !Number.isInteger(index) || index < 0 || index >= 2 ** 32 - 1 || String(index) !== key || !descriptor.enumerable) return false;
867
1112
  if (!isJsonValueAt(descriptor.value, ancestors)) return false;
868
1113
  indexCount += 1;
869
1114
  }
@@ -878,10 +1123,6 @@ function hasJsonObjectProperties(input, ancestors) {
878
1123
  }
879
1124
  return true;
880
1125
  }
881
- function isCanonicalArrayIndex(key) {
882
- const index = Number(key);
883
- return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
884
- }
885
1126
  function isJsonValue(input) {
886
1127
  return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
887
1128
  }
@@ -907,27 +1148,14 @@ function isJsonValueAt(input, ancestors) {
907
1148
  function isMissingFileError(error) {
908
1149
  return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
909
1150
  }
910
- function isNodeErrorCode(error, code) {
911
- return error instanceof Error && "code" in error && error.code === code;
912
- }
913
- async function loadCacheBody(location, expectedHeader, readOnly) {
914
- let handle;
1151
+ async function loadCacheLog(location, alintVersion) {
915
1152
  try {
916
- handle = await open(location, "r");
917
- const header = await readHeader(handle);
918
- await handle.close();
919
- handle = void 0;
920
- if (header !== expectedHeader) {
921
- if (!readOnly) await rm(location, { force: true });
922
- return createEmptyCacheBody();
923
- }
924
- const text = await readFile(location, "utf8");
925
- return parseCacheBody(text.slice(text.indexOf("\n") + 1));
1153
+ const metadata = parseMetadataLine(await readMetadataLine(location), alintVersion);
1154
+ if (!metadata) return emptyLoadedLog(alintVersion);
1155
+ return replayLog(await readFile(location, "utf8"), metadata);
926
1156
  } catch (error) {
927
- await handle?.close().catch(() => {});
928
- if (isMissingFileError(error)) return createEmptyCacheBody();
929
- if (!readOnly) await rm(location, { force: true }).catch(() => {});
930
- return createEmptyCacheBody();
1157
+ if (isMissingFileError(error)) return emptyLoadedLog(alintVersion);
1158
+ throw error;
931
1159
  }
932
1160
  }
933
1161
  function ownerEntries(body, owner) {
@@ -944,27 +1172,72 @@ function ownerKey(owner, cwd) {
944
1172
  path: normalizeCachePath(cwd, owner.path)
945
1173
  });
946
1174
  }
947
- function parseCacheBody(text) {
948
- const value = JSON.parse(text);
949
- return parse(CacheFileBodySchema, value);
1175
+ function parseMetadataLine(line, expectedAlintVersion) {
1176
+ if (!line) return void 0;
1177
+ try {
1178
+ const value = JSON.parse(line);
1179
+ const metadata = parse(CacheMetadataSchema, value);
1180
+ if (expectedAlintVersion !== void 0 && metadata.alintVersion !== expectedAlintVersion) return void 0;
1181
+ return metadata;
1182
+ } catch {
1183
+ return;
1184
+ }
950
1185
  }
951
- async function persistCacheBody(location, header, body, writeFile) {
952
- const validatedBody = parse(CacheFileBodySchema, body);
953
- await mkdir(dirname(location), { recursive: true });
1186
+ async function persistCompactedLog(location, loaded, write) {
1187
+ const metadata = parse(CacheMetadataSchema, loaded.metadata);
1188
+ const events = Object.entries(loaded.body.owners).map(([ownerKey, owner]) => ({
1189
+ at: loaded.body.updatedAt,
1190
+ entries: [...ownerEntries(loaded.body, owner)].map(([slotKey, entry]) => ({
1191
+ entry,
1192
+ slotKey
1193
+ })),
1194
+ mode: "replace",
1195
+ owner,
1196
+ ownerKey,
1197
+ type: "replace-owner"
1198
+ })).map((event) => parse(CacheEventSchema, event));
1199
+ const contents = [`${JSON.stringify(metadata)}\n`, ...events.map((event) => `${JSON.stringify(event)}\n`)].join("");
954
1200
  const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
955
1201
  try {
956
- await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
1202
+ await write(tempPath, contents);
957
1203
  await rename(tempPath, location);
958
1204
  } catch (error) {
959
1205
  await rm(tempPath, { force: true }).catch(() => {});
960
1206
  throw error;
961
1207
  }
962
1208
  }
963
- async function readHeader(handle) {
964
- const buffer = Buffer.alloc(256);
965
- const { bytesRead } = await handle.read(buffer, 0, 256, 0);
966
- const newline = buffer.subarray(0, bytesRead).indexOf(10);
967
- return newline < 0 ? void 0 : buffer.subarray(0, newline).toString("utf8");
1209
+ async function readMetadataLine(location) {
1210
+ const handle = await open(location, "r");
1211
+ try {
1212
+ const buffer = Buffer.alloc(METADATA_READ_LIMIT);
1213
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
1214
+ const newlineIndex = buffer.subarray(0, bytesRead).indexOf(10);
1215
+ if (newlineIndex < 0) return void 0;
1216
+ const lineEnd = newlineIndex > 0 && buffer[newlineIndex - 1] === 13 ? newlineIndex - 1 : newlineIndex;
1217
+ return buffer.toString("utf8", 0, lineEnd);
1218
+ } finally {
1219
+ await handle.close();
1220
+ }
1221
+ }
1222
+ function removeOwner(body, key) {
1223
+ for (const slot of body.owners[key]?.slots ?? []) delete body.entries[slot];
1224
+ delete body.owners[key];
1225
+ }
1226
+ function replayLog(text, metadata) {
1227
+ const body = createEmptyCacheBody(metadata.createdAt);
1228
+ const lines = text.split(/\r?\n/);
1229
+ for (let index = 1; index < lines.length; index += 1) {
1230
+ const line = lines[index];
1231
+ if (!line) continue;
1232
+ try {
1233
+ const value = JSON.parse(line);
1234
+ applyEvent(body, parse(CacheEventSchema, value));
1235
+ } catch {}
1236
+ }
1237
+ return {
1238
+ body: parse(CacheFileBodySchema, body),
1239
+ metadata
1240
+ };
968
1241
  }
969
1242
  function slotKey(owner, slot, cwd) {
970
1243
  return stableHash({
@@ -972,6 +1245,43 @@ function slotKey(owner, slot, cwd) {
972
1245
  ...slot
973
1246
  });
974
1247
  }
1248
+ async function trimOnce(location, alintVersion, fallback, dependencies) {
1249
+ await mkdir(dirname(location), { recursive: true });
1250
+ try {
1251
+ return await withCacheLock(location, dependencies.lock, getCacheLockOptions(0), async () => {
1252
+ const latest = await loadCacheLog(location, alintVersion);
1253
+ await persistCompactedLog(location, latest, dependencies.write);
1254
+ return latest;
1255
+ });
1256
+ } catch (error) {
1257
+ if (isNodeErrorCode(error, "ELOCKED")) return fallback;
1258
+ throw error;
1259
+ }
1260
+ }
1261
+ async function withCacheLock(location, lock, options, operation) {
1262
+ let compromised;
1263
+ const release = await lock.acquire(location, {
1264
+ ...options,
1265
+ onCompromised: (error) => {
1266
+ compromised = error;
1267
+ }
1268
+ });
1269
+ let operationError;
1270
+ try {
1271
+ const result = await operation();
1272
+ if (compromised) throw compromised;
1273
+ return result;
1274
+ } catch (error) {
1275
+ operationError = error;
1276
+ throw error;
1277
+ } finally {
1278
+ try {
1279
+ await release();
1280
+ } catch (error) {
1281
+ if (operationError === void 0) throw error;
1282
+ }
1283
+ }
1284
+ }
975
1285
  //#endregion
976
1286
  //#region src/core/execution/records.ts
977
1287
  function snapshotDiagnostic(diagnostic) {
@@ -1109,9 +1419,10 @@ async function executeRuleJob(job, options) {
1109
1419
  failure: failure(handlerCause, job, handlerCause instanceof SourceChangedError ? "source-changed" : "handler"),
1110
1420
  state: "failed"
1111
1421
  });
1112
- if (slot && fingerprint) try {
1422
+ if (slot && fingerprint) {
1113
1423
  job.target.cacheOwner?.put(slot, createCacheEntry(job, fingerprint, bucket));
1114
- } catch {}
1424
+ await job.target.cacheOwner?.checkpoint();
1425
+ }
1115
1426
  return finish(job, bucket, {
1116
1427
  cache: "miss",
1117
1428
  state: "completed"
@@ -2094,7 +2405,7 @@ async function planSource(input, options) {
2094
2405
  cacheOwner = options.cacheStore.beginOwner({
2095
2406
  kind: "file",
2096
2407
  path: file.path
2097
- });
2408
+ }, { contentHash });
2098
2409
  const baseFile = {
2099
2410
  contentHash,
2100
2411
  language: file.language,
@@ -2109,10 +2420,7 @@ async function planSource(input, options) {
2109
2420
  };
2110
2421
  }
2111
2422
  if (options.signal?.aborted) {
2112
- cacheOwner.commit({
2113
- contentHash,
2114
- mode: "merge"
2115
- });
2423
+ cacheOwner.commit({ mode: "merge" });
2116
2424
  return {
2117
2425
  outcomes: Promise.resolve([]),
2118
2426
  project
@@ -2121,11 +2429,9 @@ async function planSource(input, options) {
2121
2429
  const batch = options.scheduler.schedule(jobs);
2122
2430
  reportFilePlanningComplete(input, batch.jobsAdded, options);
2123
2431
  return {
2124
- outcomes: batch.outcomes.then((settled) => {
2125
- cacheOwner.commit(options.signal?.aborted ? {
2126
- contentHash,
2127
- mode: "merge"
2128
- } : { contentHash });
2432
+ outcomes: batch.outcomes.then(async (settled) => {
2433
+ cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
2434
+ await options.cacheStore.flush();
2129
2435
  return settled;
2130
2436
  }),
2131
2437
  project
@@ -2402,6 +2708,14 @@ var AlintAbortError = class extends AlintRunCancelledError {
2402
2708
  this.name = "AlintAbortError";
2403
2709
  }
2404
2710
  };
2711
+ var AlintCachePersistenceError = class extends Error {
2712
+ result;
2713
+ constructor(result, cause) {
2714
+ super("Failed to persist the alint cache.", { cause });
2715
+ this.name = "AlintCachePersistenceError";
2716
+ this.result = result;
2717
+ }
2718
+ };
2405
2719
  var AlintRunError = class extends Error {
2406
2720
  failures;
2407
2721
  result;
@@ -2479,6 +2793,7 @@ async function runAlint(options = {}) {
2479
2793
  const outcomes = [];
2480
2794
  const fileFailures = [];
2481
2795
  let infrastructureError;
2796
+ let cachePersistenceError;
2482
2797
  let infrastructureFailed = false;
2483
2798
  let admissionFailed = false;
2484
2799
  try {
@@ -2542,7 +2857,9 @@ async function runAlint(options = {}) {
2542
2857
  }
2543
2858
  if (!admissionFailed && !options.cacheOnly) try {
2544
2859
  await cacheStore.reconcile();
2545
- } catch {}
2860
+ } catch (error) {
2861
+ cachePersistenceError = error;
2862
+ }
2546
2863
  }
2547
2864
  runProgress.finalize();
2548
2865
  if (admissionFailed) throw infrastructureError;
@@ -2571,6 +2888,7 @@ async function runAlint(options = {}) {
2571
2888
  usage: snapshotRunUsage(result.usage)
2572
2889
  });
2573
2890
  if (infrastructureFailed) throw infrastructureError;
2891
+ if (cachePersistenceError !== void 0) throw new AlintCachePersistenceError(result, cachePersistenceError);
2574
2892
  if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
2575
2893
  if (failures.length > 0) throw new AlintRunError(fileFailures.length > 0 ? `${failures.length} alint execution${failures.length === 1 ? "" : "s"} failed.` : `${failures.length} rule execution${failures.length === 1 ? "" : "s"} failed.`, result, {
2576
2894
  cause: new AggregateError(failures.map((failure) => new Error(failure.message)), "Alint execution failures."),
@@ -2927,4 +3245,4 @@ function sampleCorpus(seed) {
2927
3245
  ].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
2928
3246
  }
2929
3247
  //#endregion
2930
- export { AlintAbortError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange, targetIdentity, withLanguage, withStableIdentities };
3248
+ export { AlintAbortError, AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defaultStopGateConfig, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, resolveStopGateConfig, runAlint, sliceLines, sliceRange, stableHash, targetIdentity, withLanguage, withStableIdentities };
@@ -1,2 +1,2 @@
1
- import { t as extractJsSourceTargets } from "./extract-DoHeLdG1.mjs";
1
+ import { t as extractJsSourceTargets } from "./extract-DhLFgRej.mjs";
2
2
  export { extractJsSourceTargets };
@@ -1,4 +1,4 @@
1
- import { b as RuleContext, z as ResolvedModel } from "./types-deBCDA4_.mjs";
1
+ import { H as ResolvedModel, x as RuleContext } from "./types-Cm5Z7Fll.mjs";
2
2
  import { GenericSchema, InferOutput } from "valibot";
3
3
  import { JsonSchema } from "@valibot/to-json-schema";
4
4
  import { Message } from "@xsai/shared-chat";
@@ -152,20 +152,32 @@ function parseStructuredResponse(schema, toolName, response) {
152
152
  retriable: true
153
153
  };
154
154
  const toolResults = response.toolResults.filter((result) => result.toolName === toolName);
155
- if (toolResults.length === 0) return {
155
+ const toolCalls = response.toolCalls.filter((call) => call.toolName === toolName);
156
+ const matches = toolResults.length + toolCalls.length;
157
+ if (matches === 0) return {
156
158
  error: `Missing ${toolName} tool result; finishReason=${response.finishReason}`,
157
159
  ok: false,
158
160
  retriable: true
159
161
  };
160
- if (toolResults.length > 1) return {
161
- error: `Expected one ${toolName} tool result, received ${toolResults.length}`,
162
+ if (matches > 1) return {
163
+ error: `Expected one ${toolName} tool result, received ${matches}`,
162
164
  ok: false,
163
165
  retriable: true
164
166
  };
167
+ let value;
168
+ try {
169
+ value = toolResults[0] !== void 0 ? toolResults[0].result : JSON.parse(toolCalls[0].args);
170
+ } catch (error) {
171
+ return {
172
+ error: `Tool call failed before validation: ${errorMessageFrom(error) ?? String(error)}`,
173
+ ok: false,
174
+ retriable: true
175
+ };
176
+ }
165
177
  try {
166
178
  return {
167
179
  ok: true,
168
- value: parse(schema, toolResults[0].result)
180
+ value: parse(schema, value)
169
181
  };
170
182
  } catch (error) {
171
183
  return {
@@ -109,6 +109,7 @@ interface AlintConfigItem {
109
109
  files?: readonly (readonly string[] | string)[];
110
110
  ignore?: IgnoreConfig;
111
111
  ignores?: readonly string[];
112
+ integrations?: IntegrationsConfig;
112
113
  language?: string;
113
114
  languageOptions?: Record<string, unknown>;
114
115
  linterOptions?: AlintLinterOptions;
@@ -160,6 +161,9 @@ interface EnabledRule {
160
161
  interface IgnoreConfig {
161
162
  gitignore?: boolean;
162
163
  }
164
+ interface IntegrationsConfig {
165
+ stopGate?: StopGateConfig;
166
+ }
163
167
  interface LanguageDefinition {
164
168
  extensions?: readonly string[];
165
169
  extract: (file: SourceFile, context: LanguageContext) => Awaitable<SourceTarget[]>;
@@ -263,8 +267,10 @@ interface RuleInferenceUsageRecord {
263
267
  totalTokens?: number;
264
268
  }
265
269
  /**
266
- * - `'any'` — every registered language, and never a failure. A rule that works from `FunctionInfo`
267
- * alone wants this: a language pack the user installs later is covered without a new release.
270
+ * - `'any'` — every language except `plaintext`, and never a failure. Plain text is what a file
271
+ * falls back to when no language claims it, so a rule asking for any language is asking for a
272
+ * real one. Rules that work from `FunctionInfo` alone want this: install another language pack
273
+ * and they cover it too, unchanged.
268
274
  * - A list of language ids — `LanguageDefinition.name` values such as `go` or `typescript`, never
269
275
  * file extensions. The rule handles exactly these. Files of other languages are skipped
270
276
  * rather than failed, so one plugin can carry rules for several languages behind one `files:`
@@ -303,6 +309,12 @@ interface RuleWithHandler {
303
309
  onTargetProject?: never;
304
310
  onTargetWith: (target: Target) => Awaitable<void>;
305
311
  }
312
+ interface StopGateConfig {
313
+ enabled?: boolean;
314
+ target?: StopGateTarget;
315
+ timeoutMs?: number;
316
+ }
317
+ type StopGateTarget = 'all' | 'dirty-files';
306
318
  type Target = DirectoryTarget | PlannedSourceTarget | ProjectTarget;
307
319
  //#endregion
308
- export { RuleWithHandler as A, ResolvedProvider as B, RuleInferenceUsageRecord as C, RuleRegistry as D, RuleOptionsSchema as E, AgentTool as F, RunnerStatsConfig as G, ProviderDefinition as H, AgentUsage as I, SetupConfig as K, ModelRequirement as L, AgentAdapter as M, AgentRequest as N, RuleSeverity as O, AgentResult as P, ResolveModelOptions as R, RuleHandlers as S, RuleOptionsOutput as T, ProviderType as U, ModelSize as V, RunnerConfig as W, ProjectTargetEntry as _, AlintLinterOptions as a, RuleContext as b, DiagnosticLocation as c, IgnoreConfig as d, LanguageDefinition as f, ProjectTarget as g, ProjectFileEntry as h, AlintConfigItem as i, Target as j, RuleSpecializedHandlers as k, DirectoryTarget as l, ProcessorDefinition as m, AlintConfigExtends as n, Awaitable as o, PluginDefinition as p, SetupModelDefinition as q, AlintConfigInput as r, DiagnosticDescriptor as s, AlintConfig as t, EnabledRule as u, RuleCacheConfig as v, RuleOptionsInput as w, RuleDefinition as x, RuleConfigEntry as y, ResolvedModel as z };
320
+ export { RuleSpecializedHandlers as A, ModelRequirement as B, RuleHandlers as C, RuleOptionsSchema as D, RuleOptionsOutput as E, AgentAdapter as F, ProviderDefinition as G, ResolvedModel as H, AgentRequest as I, RunnerStatsConfig as J, ProviderType as K, AgentResult as L, StopGateConfig as M, StopGateTarget as N, RuleRegistry as O, Target as P, AgentTool as R, RuleDefinition as S, RuleOptionsInput as T, ResolvedProvider as U, ResolveModelOptions as V, ModelSize as W, SetupModelDefinition as X, SetupConfig as Y, ProjectTarget as _, AlintLinterOptions as a, RuleConfigEntry as b, DiagnosticLocation as c, IgnoreConfig as d, IntegrationsConfig as f, ProjectFileEntry as g, ProcessorDefinition as h, AlintConfigItem as i, RuleWithHandler as j, RuleSeverity as k, DirectoryTarget as l, PluginDefinition as m, AlintConfigExtends as n, Awaitable as o, LanguageDefinition as p, RunnerConfig as q, AlintConfigInput as r, DiagnosticDescriptor as s, AlintConfig as t, EnabledRule as u, ProjectTargetEntry as v, RuleInferenceUsageRecord as w, RuleContext as x, RuleCacheConfig as y, AgentUsage as z };
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
2
  "name": "@alint-js/core",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.6.0",
5
+ "description": "Public rule and plugin SDK and run engine for alint",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/moeru-ai/alint.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "keywords": [
13
+ "alint",
14
+ "lint",
15
+ "linter",
16
+ "ai",
17
+ "code-analysis",
18
+ "sdk"
19
+ ],
5
20
  "exports": {
6
21
  ".": {
7
22
  "types": "./dist/index.d.mts",
@@ -31,17 +46,20 @@
31
46
  "dependencies": {
32
47
  "@moeru/std": "^0.1.0-beta.20",
33
48
  "@valibot/to-json-schema": "^1.7.1",
34
- "@xsai/generate-text": "^0.5.0-beta.7",
35
- "@xsai/shared-chat": "^0.5.0-beta.7",
36
- "@xsai/stream-text": "^0.5.0-beta.7",
37
- "@xsai/tool": "^0.5.0-beta.7",
49
+ "@xsai/generate-text": "^0.5.0-beta.8",
50
+ "@xsai/shared-chat": "^0.5.0-beta.8",
51
+ "@xsai/stream-text": "^0.5.0-beta.8",
52
+ "@xsai/tool": "^0.5.0-beta.8",
38
53
  "es-toolkit": "^1.50.0",
39
54
  "minimatch": "^10.2.6",
40
55
  "oxc-parser": "^0.142.0",
41
56
  "pathe": "^2.0.3",
42
- "valibot": "^1.4.2"
57
+ "proper-lockfile": "4.1.2",
58
+ "valibot": "^1.4.2",
59
+ "@alint-js/utils": "0.6.0"
43
60
  },
44
61
  "devDependencies": {
62
+ "@types/proper-lockfile": "4.1.4",
45
63
  "effect": "^3.22.1",
46
64
  "sury": "^11.0.0-rc.0"
47
65
  },