@alint-js/core 0.0.25 → 0.0.27

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/dist/agent.d.mts CHANGED
@@ -1,7 +1,16 @@
1
- import { J as AgentResult, K as AgentAdapter, X as AgentUsage, Y as AgentTool, q as AgentRequest, v as RuleContext } from "./types-CIqhJ96f.mjs";
2
-
1
+ import { J as AgentResult, K as AgentAdapter, X as AgentUsage, Y as AgentTool, q as AgentRequest, v as RuleContext } from "./types-BM_nUu_Z.mjs";
2
+ //#region src/agent/retry.d.ts
3
+ /**
4
+ * Declares that the complete adapter invocation can be safely replayed.
5
+ * Adapters must not throw this after a tool or another externally visible side effect starts.
6
+ */
7
+ declare class RetryableAgentError extends Error {
8
+ constructor(message: string, options?: ErrorOptions);
9
+ }
10
+ declare function isRetryableAgentError(error: unknown): error is RetryableAgentError;
11
+ //#endregion
3
12
  //#region src/agent/index.d.ts
4
13
  declare function defineTool(tool: AgentTool): AgentTool;
5
- declare function requireAgent(context: Pick<RuleContext, 'agent' | 'id'>): AgentAdapter;
14
+ declare function requireAgent(context: Pick<RuleContext, 'agent' | 'id' | 'signal'>): AgentAdapter;
6
15
  //#endregion
7
- export { type AgentAdapter, type AgentRequest, type AgentResult, type AgentTool, type AgentUsage, defineTool, requireAgent };
16
+ export { type AgentAdapter, type AgentRequest, type AgentResult, type AgentTool, type AgentUsage, RetryableAgentError, defineTool, isRetryableAgentError, requireAgent };
package/dist/agent.mjs CHANGED
@@ -1,10 +1,15 @@
1
+ import { n as isRetryableAgentError, t as RetryableAgentError } from "./retry-C4sGjSE5.mjs";
1
2
  //#region src/agent/index.ts
2
3
  function defineTool(tool) {
3
4
  return tool;
4
5
  }
5
6
  function requireAgent(context) {
6
7
  if (!context.agent) throw new TypeError(`Rule "${context.id}" requires an agent, but none is configured. Set "agent" in alint config (e.g. agent: createApeiraAdapter()).`);
7
- return context.agent;
8
+ const agent = context.agent;
9
+ return async (request) => agent({
10
+ ...request,
11
+ signal: request.signal ?? context.signal
12
+ });
8
13
  }
9
14
  //#endregion
10
- export { defineTool, requireAgent };
15
+ export { RetryableAgentError, defineTool, isRetryableAgentError, requireAgent };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { $ as ResolvedModel, A as LanguageContext, B as SourceRuntime, C as RuleSeverity, D as ClassTarget, E as Target, F as ProcessorPostprocessContext, G as SourceText, H as SourceTargetKind, I as SourceFile, L as SourceLocation, M as ProcessedSource, N as ProcessedSourceOrigin, O as FileTarget, P as ProcessorContext, Q as ResolveModelOptions, R as SourcePosition, S as RuleRegistry, T as RuleWithHandler, U as SourceTargetOfKind, V as SourceTarget, W as SourceTargetOrigin, Z as ModelRequirement, _ as RuleConfigEntry, a as AlintLinterOptions, at as RunnerStatsConfig, b as RuleHandlers, c as DiagnosticLocation, d as IgnoreConfig, et as ResolvedProvider, f as LanguageDefinition, g as RuleCacheConfig, h as ProjectTarget, i as AlintConfigItem, it as RunnerConfig, j as LineRange, k as FunctionTarget, l as DirectoryTarget, m as ProcessorDefinition, n as AlintConfigExtends, nt as ProviderDefinition, o as Awaitable, ot as SetupConfig, p as PluginDefinition, r as AlintConfigInput, rt as ProviderType, s as DiagnosticDescriptor, st as SetupModelDefinition, t as AlintConfig, tt as ModelSize, u as EnabledRule, v as RuleContext, w as RuleSpecializedHandlers, x as RuleInferenceUsageRecord, y as RuleDefinition, z as SourceRange } from "./types-CIqhJ96f.mjs";
2
-
1
+ import { $ as ResolvedModel, A as LanguageContext, B as SourceRuntime, C as RuleSeverity, D as ClassTarget, E as Target, F as ProcessorPostprocessContext, G as SourceText, H as SourceTargetKind, I as SourceFile, L as SourceLocation, M as ProcessedSource, N as ProcessedSourceOrigin, O as FileTarget, P as ProcessorContext, Q as ResolveModelOptions, R as SourcePosition, S as RuleRegistry, T as RuleWithHandler, U as SourceTargetOfKind, V as SourceTarget, W as SourceTargetOrigin, Z as ModelRequirement, _ as RuleConfigEntry, a as AlintLinterOptions, at as RunnerStatsConfig, b as RuleHandlers, c as DiagnosticLocation, d as IgnoreConfig, et as ResolvedProvider, f as LanguageDefinition, g as RuleCacheConfig, h as ProjectTarget, i as AlintConfigItem, it as RunnerConfig, j as LineRange, k as FunctionTarget, l as DirectoryTarget, m as ProcessorDefinition, n as AlintConfigExtends, nt as ProviderDefinition, o as Awaitable, ot as SetupConfig, p as PluginDefinition, r as AlintConfigInput, rt as ProviderType, s as DiagnosticDescriptor, st as SetupModelDefinition, t as AlintConfig, tt as ModelSize, u as EnabledRule, v as RuleContext, w as RuleSpecializedHandlers, x as RuleInferenceUsageRecord, y as RuleDefinition, z as SourceRange } from "./types-BM_nUu_Z.mjs";
3
2
  //#region src/config/config-array.d.ts
4
3
  interface EffectiveAlintConfig {
5
4
  agent?: AlintConfigItem['agent'];
@@ -126,7 +125,11 @@ interface RuleEndPayload {
126
125
  endedAt?: number;
127
126
  path: ProgressPath;
128
127
  startedAt?: number;
129
- state: 'completed' | 'errored';
128
+ /**
129
+ * `skipped` only occurs under {@link RunOptions.cacheOnly}: the rule missed the cache and was
130
+ * never executed, so it produced no diagnostics. `cache` is still `'miss'` in that case.
131
+ */
132
+ state: 'completed' | 'errored' | 'skipped';
130
133
  }
131
134
  interface RuleStartPayload {
132
135
  path: ProgressPath;
@@ -139,6 +142,8 @@ interface RunEndPayload {
139
142
  endedAt?: number;
140
143
  errored: number;
141
144
  planned: number;
145
+ /** Rules left unexecuted because they missed the cache under {@link RunOptions.cacheOnly}. Always 0 otherwise. */
146
+ skipped: number;
142
147
  startedAt?: number;
143
148
  usage: RunUsage;
144
149
  }
@@ -147,9 +152,22 @@ interface RunExecution {
147
152
  completed: number;
148
153
  errored: number;
149
154
  planned: number;
155
+ /** Rules left unexecuted because they missed the cache under {@link RunOptions.cacheOnly}. Always 0 otherwise. */
156
+ skipped: number;
150
157
  }
151
158
  type RunnerOptions = RunnerConfig;
152
159
  interface RunOptions {
160
+ /**
161
+ * Return only diagnostics that are already cached, without calling any model.
162
+ *
163
+ * Rules that miss the cache are skipped instead of executed, and the run leaves the cache
164
+ * file untouched. Read {@link RunExecution.skipped} to see how many rules a full run would
165
+ * still have to execute.
166
+ *
167
+ * Intended for callers that want to show known results for free, such as an editor
168
+ * displaying diagnostics on file open.
169
+ */
170
+ cacheOnly?: boolean;
153
171
  config?: AlintConfig;
154
172
  cwd?: string;
155
173
  directories?: string[];
@@ -159,6 +177,15 @@ interface RunOptions {
159
177
  progress?: ProgressReporter;
160
178
  runner?: RunnerOptions;
161
179
  setupConfig?: SetupConfig;
180
+ /**
181
+ * Cancels the run. Aborting stops the engine from starting further rules and cancels the
182
+ * in-flight model call, so a cancelled run stops spending tokens.
183
+ *
184
+ * `runAlint` rejects with {@link AlintAbortError}, which carries the diagnostics gathered
185
+ * before the abort. Rules that already finished keep their cache entries: cancelling never
186
+ * throws away work you already paid for.
187
+ */
188
+ signal?: AbortSignal;
162
189
  }
163
190
  interface RunResult {
164
191
  diagnostics: Diagnostic[];
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { r as withAgentRetry } from "./retry-C4sGjSE5.mjs";
1
2
  import { minimatch } from "minimatch";
2
3
  import { isAbsolute, relative, resolve } from "pathe";
3
4
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
@@ -967,7 +968,7 @@ function createDirectoryExecutionPlans(directories, fileOffset) {
967
968
  }
968
969
  //#endregion
969
970
  //#region package.json
970
- var version = "0.0.25";
971
+ var version = "0.0.27";
971
972
  //#endregion
972
973
  //#region src/core/targets/execution.ts
973
974
  var AlintRuleExecutionError = class extends Error {
@@ -1048,7 +1049,8 @@ function emitErroredRuleEnd(progress, clock, path, startedAt, cache) {
1048
1049
  } catch {}
1049
1050
  }
1050
1051
  async function executeRule(execution, path, target, options) {
1051
- const { cache, clock, counters, diagnostics, progress, usage } = options;
1052
+ const { cache, clock, counters, diagnostics, progress, signal, usage } = options;
1053
+ signal?.throwIfAborted();
1052
1054
  const startedAt = clock();
1053
1055
  const cacheKey = createExecutionCacheKey(execution.runtime, target, path, cache);
1054
1056
  const cachedEntry = cacheKey ? cache.store.get(cacheKey) : void 0;
@@ -1075,6 +1077,17 @@ async function executeRule(execution, path, target, options) {
1075
1077
  });
1076
1078
  return;
1077
1079
  }
1080
+ if (options.cacheOnly) {
1081
+ counters.skip();
1082
+ progress?.onRuleEnd?.({
1083
+ cache: "miss",
1084
+ endedAt: clock(),
1085
+ path,
1086
+ startedAt,
1087
+ state: "skipped"
1088
+ });
1089
+ return;
1090
+ }
1078
1091
  let handlerError;
1079
1092
  let handlerFailed = false;
1080
1093
  const cacheDiagnostics = cacheKey ? [] : void 0;
@@ -1091,6 +1104,7 @@ async function executeRule(execution, path, target, options) {
1091
1104
  handlerFailed = true;
1092
1105
  }
1093
1106
  if (handlerFailed) {
1107
+ signal?.throwIfAborted();
1094
1108
  counters.error();
1095
1109
  emitErroredRuleEnd(progress, clock, path, startedAt, "miss");
1096
1110
  throw new AlintRuleExecutionError(handlerError, path);
@@ -1397,6 +1411,18 @@ var AlintRunError = class extends Error {
1397
1411
  this.result = result;
1398
1412
  }
1399
1413
  };
1414
+ /**
1415
+ * Thrown when {@link RunOptions.signal} aborts a run.
1416
+ *
1417
+ * Extends {@link AlintRunError} so existing handlers still receive the partial `result`, while
1418
+ * callers that treat cancellation differently from failure can test for this type.
1419
+ */
1420
+ var AlintAbortError = class extends AlintRunError {
1421
+ constructor(result, options = {}) {
1422
+ super("alint run was aborted", result, { cause: options.cause });
1423
+ this.name = "AlintAbortError";
1424
+ }
1425
+ };
1400
1426
  async function runAlint(options = {}) {
1401
1427
  const cwd$1 = options.cwd ?? cwd();
1402
1428
  const config = options.config ?? [];
@@ -1511,6 +1537,7 @@ async function runAlint(options = {}) {
1511
1537
  let runStartedAt;
1512
1538
  let runError;
1513
1539
  try {
1540
+ options.signal?.throwIfAborted();
1514
1541
  filePlans = createSourceExecutionPlans(files, cwd$1);
1515
1542
  directoryPlans = createDirectoryExecutionPlans(directories, filePlans.length);
1516
1543
  projectPlan = resolvedProjectConfig.ignored ? void 0 : createProjectExecutionPlan({
@@ -1535,38 +1562,44 @@ async function runAlint(options = {}) {
1535
1562
  });
1536
1563
  await runConcurrently(filePlans.filter((filePlan) => filePlan.targets.length > 0), resolveFileConcurrency(options.runner?.fileConcurrency), (filePlan) => executeTargetPlans({
1537
1564
  cache: cacheContext,
1565
+ cacheOnly: options.cacheOnly,
1538
1566
  clock,
1539
1567
  counters,
1540
1568
  diagnostics,
1541
1569
  filesTotal: inputsTotal,
1542
1570
  plans: [filePlan],
1543
1571
  progress: options.progress,
1572
+ signal: options.signal,
1544
1573
  usage
1545
1574
  }));
1546
1575
  await runConcurrently(activeDirectoryPlans, resolveFileConcurrency(options.runner?.fileConcurrency), (directoryPlan) => executeTargetPlans({
1547
1576
  cache: cacheContext,
1577
+ cacheOnly: options.cacheOnly,
1548
1578
  clock,
1549
1579
  counters,
1550
1580
  diagnostics,
1551
1581
  filesTotal: inputsTotal,
1552
1582
  plans: [directoryPlan],
1553
1583
  progress: options.progress,
1584
+ signal: options.signal,
1554
1585
  usage
1555
1586
  }));
1556
1587
  if (projectPlan) await executeTargetPlans({
1557
1588
  cache: cacheContext,
1589
+ cacheOnly: options.cacheOnly,
1558
1590
  clock,
1559
1591
  counters,
1560
1592
  diagnostics,
1561
1593
  filesTotal: inputsTotal,
1562
1594
  plans: [projectPlan],
1563
1595
  progress: options.progress,
1596
+ signal: options.signal,
1564
1597
  usage
1565
1598
  });
1566
1599
  } catch (error) {
1567
1600
  runError = error;
1568
1601
  } finally {
1569
- await reconcileCache(filePlans, cacheContext);
1602
+ if (!options.cacheOnly) await reconcileCache(filePlans, cacheContext);
1570
1603
  try {
1571
1604
  options.progress?.onRunEnd?.({
1572
1605
  ...counters.snapshot(planned),
@@ -1584,7 +1617,10 @@ async function runAlint(options = {}) {
1584
1617
  execution: counters.snapshot(planned),
1585
1618
  usage: usage.toJSON()
1586
1619
  };
1587
- if (runError) throw createAlintRunError(runError, result);
1620
+ if (runError) {
1621
+ if (options.signal?.aborted) throw new AlintAbortError(result, { cause: runError });
1622
+ throw createAlintRunError(runError, result);
1623
+ }
1588
1624
  return result;
1589
1625
  }
1590
1626
  function addTokenCount(base, value) {
@@ -1612,6 +1648,7 @@ function createRuleEndCounters() {
1612
1648
  let cached = 0;
1613
1649
  let completed = 0;
1614
1650
  let errored = 0;
1651
+ let skipped = 0;
1615
1652
  return {
1616
1653
  cache() {
1617
1654
  cached += 1;
@@ -1622,21 +1659,26 @@ function createRuleEndCounters() {
1622
1659
  error() {
1623
1660
  errored += 1;
1624
1661
  },
1662
+ skip() {
1663
+ skipped += 1;
1664
+ },
1625
1665
  snapshot(planned) {
1626
1666
  return {
1627
1667
  cached,
1628
1668
  completed,
1629
1669
  errored,
1630
- planned
1670
+ planned,
1671
+ skipped
1631
1672
  };
1632
1673
  }
1633
1674
  };
1634
1675
  }
1635
1676
  function createRuleRuntimes(options) {
1677
+ const agent = options.effectiveAgent ? withAgentRetry(options.effectiveAgent, options.options.runner?.agentRetries) : void 0;
1636
1678
  return options.registry.enabledRules.map((enabledRule) => {
1637
1679
  const executionState = new AsyncLocalStorage();
1638
1680
  const context = {
1639
- agent: options.effectiveAgent,
1681
+ agent,
1640
1682
  cwd: options.cwd,
1641
1683
  id: enabledRule.id,
1642
1684
  localId: enabledRule.localId,
@@ -1691,6 +1733,7 @@ function createRuleRuntimes(options) {
1691
1733
  });
1692
1734
  },
1693
1735
  settings: options.effectiveSettings,
1736
+ signal: options.options.signal,
1694
1737
  src: options.src
1695
1738
  };
1696
1739
  return {
@@ -0,0 +1,50 @@
1
+ import { withRetry } from "@moeru/std/with-retry";
2
+ //#region src/agent/retry.ts
3
+ /**
4
+ * Declares that the complete adapter invocation can be safely replayed.
5
+ * Adapters must not throw this after a tool or another externally visible side effect starts.
6
+ */
7
+ var RetryableAgentError = class extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = "RetryableAgentError";
11
+ }
12
+ };
13
+ function isRetryableAgentError(error) {
14
+ return error instanceof RetryableAgentError;
15
+ }
16
+ function withAgentRetry(adapter, retries = 2) {
17
+ const invoke = withRetry(async (request) => {
18
+ try {
19
+ request.signal?.throwIfAborted();
20
+ const result = await adapter(request);
21
+ request.signal?.throwIfAborted();
22
+ return {
23
+ ok: true,
24
+ result
25
+ };
26
+ } catch (error) {
27
+ if (request.signal?.aborted) return {
28
+ error: request.signal.reason,
29
+ ok: false
30
+ };
31
+ if (isRetryableAgentError(error)) throw error;
32
+ return {
33
+ error,
34
+ ok: false
35
+ };
36
+ }
37
+ }, {
38
+ retry: retries,
39
+ retryDelay: 500,
40
+ retryDelayFactor: 2,
41
+ retryDelayMax: 3e4
42
+ });
43
+ return async (request) => {
44
+ const attempt = await invoke(request);
45
+ if (!attempt.ok) throw attempt.error;
46
+ return attempt.result;
47
+ };
48
+ }
49
+ //#endregion
50
+ export { isRetryableAgentError as n, withAgentRetry as r, RetryableAgentError as t };
@@ -1,8 +1,7 @@
1
- import { $ as ResolvedModel, v as RuleContext } from "./types-CIqhJ96f.mjs";
1
+ import { $ as ResolvedModel, v as RuleContext } from "./types-BM_nUu_Z.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";
5
-
6
5
  //#region src/structuredOutput/index.d.ts
7
6
  interface GenerateStructuredOptions<Schema extends GenericSchema> {
8
7
  /**
@@ -21,6 +20,8 @@ interface GenerateStructuredOptions<Schema extends GenericSchema> {
21
20
  /** Milliseconds to wait before the given (1-based) attempt is retried. */
22
21
  retryDelay?: (attempt: number) => number;
23
22
  schema: Schema;
23
+ /** Cancels the model call and any pending retry backoff. Pass `ctx.signal` from a rule. */
24
+ signal?: AbortSignal;
24
25
  temperature?: number;
25
26
  /** Shown to the model as the tool description. Defaults to the schema's valibot description. */
26
27
  toolDescription?: string;
@@ -1,6 +1,6 @@
1
1
  import { errorMessageFrom } from "@moeru/std/error";
2
2
  import { getDescription, parse } from "valibot";
3
- import { sleep } from "@moeru/std/sleep";
3
+ import { setTimeout } from "node:timers/promises";
4
4
  import { toJsonSchema } from "@valibot/to-json-schema";
5
5
  import { generateText } from "@xsai/generate-text";
6
6
  import { rawTool } from "@xsai/tool";
@@ -41,9 +41,11 @@ async function generateStructured(options) {
41
41
  });
42
42
  let previousError;
43
43
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
44
+ options.signal?.throwIfAborted();
44
45
  let response;
45
46
  try {
46
47
  response = await generateText({
48
+ abortSignal: options.signal,
47
49
  baseURL: options.model.provider.endpoint,
48
50
  headers: options.model.provider.headers,
49
51
  messages: options.createMessages(previousError ? retryFeedbackFrom(toolName, previousError) : void 0),
@@ -57,11 +59,12 @@ async function generateStructured(options) {
57
59
  tools: [tool]
58
60
  });
59
61
  } catch (error) {
62
+ options.signal?.throwIfAborted();
60
63
  const callError = `Tool call failed before validation: ${errorMessageFrom(error) ?? String(error)}`;
61
64
  previousError = isRetriableHttpError(error) ? void 0 : callError;
62
65
  options.logger?.debug(`${options.operation} attempt ${attempt} failed while calling the model: ${callError}`);
63
66
  if (!isRetriableCallError(error) || attempt === maxAttempts) throw error;
64
- await sleep(retryDelay(attempt));
67
+ await setTimeout(retryDelay(attempt), void 0, { signal: options.signal });
65
68
  continue;
66
69
  }
67
70
  recordAttemptUsage(options, response);
@@ -70,7 +73,7 @@ async function generateStructured(options) {
70
73
  previousError = result.error;
71
74
  options.logger?.debug(`${options.operation} attempt ${attempt} returned an invalid structured result: ${previousError}`);
72
75
  if (!result.retriable || attempt === maxAttempts) throw new InvalidStructuredOutputError(`Invalid structured model response: ${previousError}`);
73
- await sleep(retryDelay(attempt));
76
+ await setTimeout(retryDelay(attempt), void 0, { signal: options.signal });
74
77
  }
75
78
  throw new InvalidStructuredOutputError("Model did not return a valid structured result");
76
79
  }
@@ -13,6 +13,8 @@ interface RunnerCacheConfig {
13
13
  location?: string;
14
14
  }
15
15
  interface RunnerConfig {
16
+ /** Retries after the initial attempt for replay-safe agent failures. @default 2 */
17
+ agentRetries?: number;
16
18
  cache?: boolean | RunnerCacheConfig;
17
19
  fileConcurrency?: number;
18
20
  ruleConcurrency?: number;
@@ -275,6 +277,13 @@ interface RuleContext {
275
277
  outputLanguage?: string;
276
278
  report: (diagnostic: DiagnosticDescriptor) => void;
277
279
  settings: Record<string, unknown>;
280
+ /**
281
+ * Cancels the run. Forward it to anything long-running a rule starts, so cancelling stops
282
+ * the work instead of letting it finish and bill.
283
+ *
284
+ * `ctx.agent` already injects it, and `generateStructured` accepts it as `signal`.
285
+ */
286
+ signal?: AbortSignal;
278
287
  src: SourceRuntime;
279
288
  }
280
289
  interface RuleDefinition {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/core",
3
3
  "type": "module",
4
- "version": "0.0.25",
4
+ "version": "0.0.27",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -28,13 +28,13 @@
28
28
  "@xsai/tool": "^0.5.0-beta.7",
29
29
  "es-toolkit": "^1.49.0",
30
30
  "minimatch": "^10.2.5",
31
- "oxc-parser": "^0.137.0",
31
+ "oxc-parser": "^0.140.0",
32
32
  "pathe": "^2.0.3",
33
- "valibot": "^1.4.1"
33
+ "valibot": "^1.4.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "effect": "^3.22.0",
37
- "sury": "^10.0.4"
37
+ "sury": "^11.0.0-alpha.10"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsdown",