@alint-js/core 0.5.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
@@ -182,3 +182,10 @@ source files retained by plugin code after `ctx.src.readFile()` returns.
182
182
 
183
183
  A valid cache written by the same `alint` version is still read from one monolithic JSON document.
184
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-BK5CADYK.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-BK5CADYK.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-BK5CADYK.mjs";
2
- import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-CNfm3wZW.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 };
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-BK5CADYK.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-CNfm3wZW.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;
@@ -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-CLXRRiJW.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: [
@@ -605,13 +638,16 @@ function recordMissingLanguages(into, rules, registry, path) {
605
638
  }
606
639
  //#endregion
607
640
  //#region package.json
608
- var version = "0.5.0";
641
+ var version = "0.6.0";
609
642
  //#endregion
610
643
  //#region src/core/cache/types.ts
611
644
  const CACHE_MAGIC = "ALINT_CACHE";
612
645
  //#endregion
613
646
  //#region src/core/cache/store.ts
614
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;
615
651
  const FiniteNumberSchema = pipe(number(), finite());
616
652
  const PositionSchema = object({
617
653
  column: FiniteNumberSchema,
@@ -671,18 +707,19 @@ const CacheEntrySchema = object({
671
707
  }),
672
708
  usage: array(UsageSchema)
673
709
  });
710
+ const CachedOwnerSchema = object({
711
+ contentHash: optional(string()),
712
+ kind: union([literal("file"), literal("project")]),
713
+ path: string(),
714
+ slots: array(string())
715
+ });
674
716
  function objectRecord(value) {
675
717
  return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
676
718
  }
677
719
  const CacheFileBodySchema = pipe(object({
678
720
  createdAt: string(),
679
721
  entries: objectRecord(CacheEntrySchema),
680
- owners: objectRecord(object({
681
- contentHash: optional(string()),
682
- kind: union([literal("file"), literal("project")]),
683
- path: string(),
684
- slots: array(string())
685
- })),
722
+ owners: objectRecord(CachedOwnerSchema),
686
723
  updatedAt: string()
687
724
  }), check((body) => {
688
725
  const entryKeys = Object.keys(body.entries);
@@ -690,22 +727,68 @@ const CacheFileBodySchema = pipe(object({
690
727
  const uniqueSlots = new Set(referencedSlots);
691
728
  return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
692
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
+ ]);
693
766
  async function createCacheStore(options) {
694
767
  const location = resolveCacheLocation(options.cwd, options.location);
695
768
  if (!options.enabled) return createNoopCacheStore(location);
696
769
  const alintVersion = options.alintVersion ?? version;
697
- const header = `${CACHE_MAGIC} 2 ${alintVersion}`;
698
- const body = await loadCacheBody(location, header, options.readOnly === true);
699
- 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);
700
779
  const fileExists = options.fileExists ?? defaultFileExists;
701
- const writeFile$1 = options.writeFile ?? writeFile;
702
780
  return {
703
- beginOwner: (owner) => beginOwner(body, owner, options.cwd),
781
+ beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
782
+ flush: () => writer.flush(false),
704
783
  location,
705
784
  reconcile: async () => {
706
- await collectMissingFileOwners(body, options.cwd, fileExists);
707
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
708
- 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);
709
792
  }
710
793
  };
711
794
  }
@@ -757,7 +840,50 @@ function resolveCacheLocation(cwd, location) {
757
840
  } catch {}
758
841
  return resolved;
759
842
  }
760
- 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 = {}) {
761
887
  const normalizedOwner = {
762
888
  kind: owner.kind,
763
889
  path: normalizeCachePath(cwd, owner.path)
@@ -765,22 +891,64 @@ function beginOwner(body, owner, cwd) {
765
891
  const key = ownerKey(normalizedOwner, cwd);
766
892
  const previousOwner = body.owners[key];
767
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
+ };
768
923
  return {
769
- commit: (metadata = {}) => {
770
- const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
771
- for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
772
- const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
773
- for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
774
- for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
775
- body.owners[key] = {
776
- contentHash: metadata.contentHash,
924
+ checkpoint: async () => {
925
+ const cachedOwner = {
926
+ contentHash: ownerMetadata.contentHash,
777
927
  kind: normalizedOwner.kind,
778
928
  path: normalizedOwner.path,
779
- slots: [...committedEntries.keys()].sort()
929
+ slots: []
780
930
  };
781
- 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);
782
951
  },
783
- discard: (cacheSlot) => nextEntries.delete(slotKey(normalizedOwner, cacheSlot, cwd)),
784
952
  lookup: (cacheSlot, fingerprint) => {
785
953
  const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
786
954
  const cached = body.entries[entryKey];
@@ -788,15 +956,21 @@ function beginOwner(body, owner, cwd) {
788
956
  nextEntries.set(entryKey, cached);
789
957
  return cached;
790
958
  },
791
- 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
+ }
792
964
  };
793
965
  }
794
966
  async function collectMissingFileOwners(body, cwd, fileExists) {
967
+ const removed = [];
795
968
  for (const [key, owner] of Object.entries(body.owners)) {
796
969
  if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
797
- for (const entryKey of owner.slots) delete body.entries[entryKey];
798
- delete body.owners[key];
970
+ removeOwner(body, key);
971
+ removed.push(key);
799
972
  }
973
+ return removed;
800
974
  }
801
975
  function createBaseTargetIdentity(target) {
802
976
  if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
@@ -805,17 +979,53 @@ function createBaseTargetIdentity(target) {
805
979
  if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
806
980
  return target.kind;
807
981
  }
808
- function createEmptyCacheBody() {
809
- const now = (/* @__PURE__ */ new Date()).toISOString();
982
+ function createEmptyCacheBody(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
810
983
  return {
811
- createdAt: now,
984
+ createdAt,
812
985
  entries: {},
813
986
  owners: {},
814
- 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
+ }
815
1024
  };
816
1025
  }
817
1026
  function createNoopCacheStore(location) {
818
1027
  const transaction = {
1028
+ checkpoint: async () => {},
819
1029
  commit: () => {},
820
1030
  discard: () => {},
821
1031
  lookup: () => void 0,
@@ -823,22 +1033,24 @@ function createNoopCacheStore(location) {
823
1033
  };
824
1034
  return {
825
1035
  beginOwner: () => transaction,
1036
+ flush: async () => {},
826
1037
  location,
827
1038
  reconcile: async () => {}
828
1039
  };
829
1040
  }
830
1041
  function createReadOnlyCacheStore(location, body, cwd) {
831
- const transaction = (owner) => ({
832
- commit: () => {},
833
- discard: () => {},
834
- lookup: (slot, fingerprint) => {
835
- const cached = body.entries[slotKey(owner, slot, cwd)];
836
- return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
837
- },
838
- put: () => {}
839
- });
840
1042
  return {
841
- 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 () => {},
842
1054
  location,
843
1055
  reconcile: async () => {}
844
1056
  };
@@ -852,9 +1064,38 @@ async function defaultFileExists(path) {
852
1064
  throw error;
853
1065
  }
854
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
+ }
855
1088
  function fingerprintsEqual(left, right) {
856
1089
  return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
857
1090
  }
1091
+ function getCacheLockOptions(retries) {
1092
+ return {
1093
+ realpath: false,
1094
+ retries,
1095
+ stale: LOCK_STALE_MS,
1096
+ update: LOCK_UPDATE_MS
1097
+ };
1098
+ }
858
1099
  function hasJsonArrayProperties(input, ancestors) {
859
1100
  let arrayLength;
860
1101
  let indexCount = 0;
@@ -866,7 +1107,8 @@ function hasJsonArrayProperties(input, ancestors) {
866
1107
  arrayLength = descriptor.value;
867
1108
  continue;
868
1109
  }
869
- 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;
870
1112
  if (!isJsonValueAt(descriptor.value, ancestors)) return false;
871
1113
  indexCount += 1;
872
1114
  }
@@ -881,10 +1123,6 @@ function hasJsonObjectProperties(input, ancestors) {
881
1123
  }
882
1124
  return true;
883
1125
  }
884
- function isCanonicalArrayIndex(key) {
885
- const index = Number(key);
886
- return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
887
- }
888
1126
  function isJsonValue(input) {
889
1127
  return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
890
1128
  }
@@ -910,27 +1148,14 @@ function isJsonValueAt(input, ancestors) {
910
1148
  function isMissingFileError(error) {
911
1149
  return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
912
1150
  }
913
- function isNodeErrorCode(error, code) {
914
- return error instanceof Error && "code" in error && error.code === code;
915
- }
916
- async function loadCacheBody(location, expectedHeader, readOnly) {
917
- let handle;
1151
+ async function loadCacheLog(location, alintVersion) {
918
1152
  try {
919
- handle = await open(location, "r");
920
- const header = await readHeader(handle);
921
- await handle.close();
922
- handle = void 0;
923
- if (header !== expectedHeader) {
924
- if (!readOnly) await rm(location, { force: true });
925
- return createEmptyCacheBody();
926
- }
927
- const text = await readFile(location, "utf8");
928
- 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);
929
1156
  } catch (error) {
930
- await handle?.close().catch(() => {});
931
- if (isMissingFileError(error)) return createEmptyCacheBody();
932
- if (!readOnly) await rm(location, { force: true }).catch(() => {});
933
- return createEmptyCacheBody();
1157
+ if (isMissingFileError(error)) return emptyLoadedLog(alintVersion);
1158
+ throw error;
934
1159
  }
935
1160
  }
936
1161
  function ownerEntries(body, owner) {
@@ -947,27 +1172,72 @@ function ownerKey(owner, cwd) {
947
1172
  path: normalizeCachePath(cwd, owner.path)
948
1173
  });
949
1174
  }
950
- function parseCacheBody(text) {
951
- const value = JSON.parse(text);
952
- 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
+ }
953
1185
  }
954
- async function persistCacheBody(location, header, body, writeFile) {
955
- const validatedBody = parse(CacheFileBodySchema, body);
956
- 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("");
957
1200
  const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
958
1201
  try {
959
- await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
1202
+ await write(tempPath, contents);
960
1203
  await rename(tempPath, location);
961
1204
  } catch (error) {
962
1205
  await rm(tempPath, { force: true }).catch(() => {});
963
1206
  throw error;
964
1207
  }
965
1208
  }
966
- async function readHeader(handle) {
967
- const buffer = Buffer.alloc(256);
968
- const { bytesRead } = await handle.read(buffer, 0, 256, 0);
969
- const newline = buffer.subarray(0, bytesRead).indexOf(10);
970
- 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
+ };
971
1241
  }
972
1242
  function slotKey(owner, slot, cwd) {
973
1243
  return stableHash({
@@ -975,6 +1245,43 @@ function slotKey(owner, slot, cwd) {
975
1245
  ...slot
976
1246
  });
977
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
+ }
978
1285
  //#endregion
979
1286
  //#region src/core/execution/records.ts
980
1287
  function snapshotDiagnostic(diagnostic) {
@@ -1112,9 +1419,10 @@ async function executeRuleJob(job, options) {
1112
1419
  failure: failure(handlerCause, job, handlerCause instanceof SourceChangedError ? "source-changed" : "handler"),
1113
1420
  state: "failed"
1114
1421
  });
1115
- if (slot && fingerprint) try {
1422
+ if (slot && fingerprint) {
1116
1423
  job.target.cacheOwner?.put(slot, createCacheEntry(job, fingerprint, bucket));
1117
- } catch {}
1424
+ await job.target.cacheOwner?.checkpoint();
1425
+ }
1118
1426
  return finish(job, bucket, {
1119
1427
  cache: "miss",
1120
1428
  state: "completed"
@@ -2097,7 +2405,7 @@ async function planSource(input, options) {
2097
2405
  cacheOwner = options.cacheStore.beginOwner({
2098
2406
  kind: "file",
2099
2407
  path: file.path
2100
- });
2408
+ }, { contentHash });
2101
2409
  const baseFile = {
2102
2410
  contentHash,
2103
2411
  language: file.language,
@@ -2112,10 +2420,7 @@ async function planSource(input, options) {
2112
2420
  };
2113
2421
  }
2114
2422
  if (options.signal?.aborted) {
2115
- cacheOwner.commit({
2116
- contentHash,
2117
- mode: "merge"
2118
- });
2423
+ cacheOwner.commit({ mode: "merge" });
2119
2424
  return {
2120
2425
  outcomes: Promise.resolve([]),
2121
2426
  project
@@ -2124,11 +2429,9 @@ async function planSource(input, options) {
2124
2429
  const batch = options.scheduler.schedule(jobs);
2125
2430
  reportFilePlanningComplete(input, batch.jobsAdded, options);
2126
2431
  return {
2127
- outcomes: batch.outcomes.then((settled) => {
2128
- cacheOwner.commit(options.signal?.aborted ? {
2129
- contentHash,
2130
- mode: "merge"
2131
- } : { contentHash });
2432
+ outcomes: batch.outcomes.then(async (settled) => {
2433
+ cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
2434
+ await options.cacheStore.flush();
2132
2435
  return settled;
2133
2436
  }),
2134
2437
  project
@@ -2405,6 +2708,14 @@ var AlintAbortError = class extends AlintRunCancelledError {
2405
2708
  this.name = "AlintAbortError";
2406
2709
  }
2407
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
+ };
2408
2719
  var AlintRunError = class extends Error {
2409
2720
  failures;
2410
2721
  result;
@@ -2482,6 +2793,7 @@ async function runAlint(options = {}) {
2482
2793
  const outcomes = [];
2483
2794
  const fileFailures = [];
2484
2795
  let infrastructureError;
2796
+ let cachePersistenceError;
2485
2797
  let infrastructureFailed = false;
2486
2798
  let admissionFailed = false;
2487
2799
  try {
@@ -2545,7 +2857,9 @@ async function runAlint(options = {}) {
2545
2857
  }
2546
2858
  if (!admissionFailed && !options.cacheOnly) try {
2547
2859
  await cacheStore.reconcile();
2548
- } catch {}
2860
+ } catch (error) {
2861
+ cachePersistenceError = error;
2862
+ }
2549
2863
  }
2550
2864
  runProgress.finalize();
2551
2865
  if (admissionFailed) throw infrastructureError;
@@ -2574,6 +2888,7 @@ async function runAlint(options = {}) {
2574
2888
  usage: snapshotRunUsage(result.usage)
2575
2889
  });
2576
2890
  if (infrastructureFailed) throw infrastructureError;
2891
+ if (cachePersistenceError !== void 0) throw new AlintCachePersistenceError(result, cachePersistenceError);
2577
2892
  if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
2578
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, {
2579
2894
  cause: new AggregateError(failures.map((failure) => new Error(failure.message)), "Alint execution failures."),
@@ -2930,4 +3245,4 @@ function sampleCorpus(seed) {
2930
3245
  ].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
2931
3246
  }
2932
3247
  //#endregion
2933
- 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-CLXRRiJW.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-BK5CADYK.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";
@@ -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[]>;
@@ -305,6 +309,12 @@ interface RuleWithHandler {
305
309
  onTargetProject?: never;
306
310
  onTargetWith: (target: Target) => Awaitable<void>;
307
311
  }
312
+ interface StopGateConfig {
313
+ enabled?: boolean;
314
+ target?: StopGateTarget;
315
+ timeoutMs?: number;
316
+ }
317
+ type StopGateTarget = 'all' | 'dirty-files';
308
318
  type Target = DirectoryTarget | PlannedSourceTarget | ProjectTarget;
309
319
  //#endregion
310
- 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.5.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",
@@ -39,9 +54,12 @@
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
  },
@@ -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() {