@alint-js/config 0.0.21 → 0.0.23

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/index.d.mts CHANGED
@@ -69,6 +69,98 @@ declare function getProjectPluginStorePath(cwd: string): string;
69
69
  declare function getProjectSetupConfigPath(cwd: string): string;
70
70
  declare function getStatsDir(env?: NodeJS.ProcessEnv, options?: GlobalSetupConfigPathOptions): string;
71
71
  //#endregion
72
+ //#region src/plugins/declarative/types.d.ts
73
+ declare const builtInAgentNames: readonly ["basic-structured", "basic-coding-agent"];
74
+ declare const declarativeRuleFilePattern = "**/rule.alint.{toml,yaml,yml,json,jsonc,json5}";
75
+ type BuiltInAgentName = typeof builtInAgentNames[number];
76
+ interface DeclarativeFinding {
77
+ confidence?: 'high' | 'low' | 'medium';
78
+ filePath?: string;
79
+ line: number;
80
+ message: string;
81
+ suggestion?: string;
82
+ }
83
+ interface DeclarativeFindingResponse {
84
+ findings: DeclarativeFinding[];
85
+ }
86
+ declare const declarativeFindingSchema: import("valibot").SchemaWithPipe<readonly [import("valibot").ObjectSchema<{
87
+ readonly confidence: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").PicklistSchema<["high", "medium", "low"], undefined>, import("valibot").DescriptionAction<"high" | "medium" | "low", "Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known.">]>, undefined>;
88
+ readonly filePath: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file.">]>, undefined>;
89
+ readonly line: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").DescriptionAction<number, "Use the left-column line number from the numbered source block.">]>;
90
+ readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the issue.">]>;
91
+ readonly suggestion: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Concrete remediation direction for the finding.">]>, undefined>;
92
+ }, undefined>, import("valibot").DescriptionAction<{
93
+ confidence?: "high" | "medium" | "low" | undefined;
94
+ filePath?: string | undefined;
95
+ line: number;
96
+ message: string;
97
+ suggestion?: string | undefined;
98
+ }, "One declarative rule finding.">]>;
99
+ declare const declarativeFindingResponseSchema: import("valibot").SchemaWithPipe<readonly [import("valibot").ObjectSchema<{
100
+ readonly findings: import("valibot").SchemaWithPipe<readonly [import("valibot").ArraySchema<import("valibot").SchemaWithPipe<readonly [import("valibot").ObjectSchema<{
101
+ readonly confidence: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").PicklistSchema<["high", "medium", "low"], undefined>, import("valibot").DescriptionAction<"high" | "medium" | "low", "Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known.">]>, undefined>;
102
+ readonly filePath: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file.">]>, undefined>;
103
+ readonly line: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").DescriptionAction<number, "Use the left-column line number from the numbered source block.">]>;
104
+ readonly message: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Human-readable diagnostic message describing the issue.">]>;
105
+ readonly suggestion: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "Concrete remediation direction for the finding.">]>, undefined>;
106
+ }, undefined>, import("valibot").DescriptionAction<{
107
+ confidence?: "high" | "medium" | "low" | undefined;
108
+ filePath?: string | undefined;
109
+ line: number;
110
+ message: string;
111
+ suggestion?: string | undefined;
112
+ }, "One declarative rule finding.">]>, undefined>, import("valibot").DescriptionAction<{
113
+ confidence?: "high" | "medium" | "low" | undefined;
114
+ filePath?: string | undefined;
115
+ line: number;
116
+ message: string;
117
+ suggestion?: string | undefined;
118
+ }[], "All findings for the declarative rule. Return an empty array when there are no issues.">]>;
119
+ }, undefined>, import("valibot").DescriptionAction<{
120
+ findings: {
121
+ confidence?: "high" | "medium" | "low" | undefined;
122
+ filePath?: string | undefined;
123
+ line: number;
124
+ message: string;
125
+ suggestion?: string | undefined;
126
+ }[];
127
+ }, "Structured declarative rule findings.">]>;
128
+ interface DeclarativeRuleDefinition {
129
+ builtInAgent: BuiltInAgentName;
130
+ excludeFiles: string[];
131
+ filePath: string;
132
+ includeFiles?: string[];
133
+ instruction: string;
134
+ name: string;
135
+ }
136
+ declare function isBuiltInAgentName(value: unknown): value is BuiltInAgentName;
137
+ //#endregion
138
+ //#region src/plugins/declarative/parse.d.ts
139
+ interface LoadDeclarativeRulesOptions {
140
+ alias: string;
141
+ root: string;
142
+ }
143
+ declare function hasDeclarativeRuleFiles(root: string): Promise<boolean>;
144
+ declare function loadDeclarativeRules(options: LoadDeclarativeRulesOptions): Promise<DeclarativeRuleDefinition[]>;
145
+ //#endregion
146
+ //#region src/plugins/declarative/plugin.d.ts
147
+ interface CreateDeclarativePluginOptions {
148
+ rules: readonly DeclarativeRuleDefinition[];
149
+ }
150
+ declare function createDeclarativePlugin(options: CreateDeclarativePluginOptions): PluginDefinition;
151
+ //#endregion
152
+ //#region src/plugins/declarative/scope.d.ts
153
+ interface CreateReportScopeOptions {
154
+ cwd: string;
155
+ excludeFiles: readonly string[];
156
+ includeFiles?: readonly string[];
157
+ targetFilePath?: string;
158
+ }
159
+ interface ReportScope {
160
+ canReport: (filePath: string | undefined) => boolean;
161
+ }
162
+ declare function createReportScope(options: CreateReportScopeOptions): ReportScope;
163
+ //#endregion
72
164
  //#region src/plugins/types.d.ts
73
165
  interface DirectoryPluginLockEntry {
74
166
  alias: string;
@@ -153,4 +245,4 @@ declare function stringifySetupConfigToml(config: SetupConfig$1): string;
153
245
  //#region src/setup/write.d.ts
154
246
  declare function writeSetupConfig(filePath: string, config: SetupConfig$1): Promise<void>;
155
247
  //#endregion
156
- export { type AlintConfig, type DirectoryPluginLockEntry, type GlobalSetupConfigPathOptions, type ModelSize, type ParseStaticConfigOptions, type ParsedDirectoryPluginLockEntry, type ParsedPluginLockEntry, type ParsedPluginLockFile, type ParsedPluginSpecifier, type ParsedRegistryPluginLockEntry, type ParsedStaticConfig, type ParsedStaticConfigGroup, type PluginLockEntry, type PluginLockFile, type ProviderDefinition, type ProviderType, type RegistryPluginLockEntry, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, type StaticConfigInput, type StaticConfigItem, type StaticPluginInstallOptions, type StaticPluginInstallResult, type StaticPluginReference, type StaticPluginResolver, type ToAlintConfigOptions, createEmptyPluginLockFile, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, listMissing, listUnresolved, loadAlintConfig, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
248
+ export { type AlintConfig, BuiltInAgentName, CreateDeclarativePluginOptions, CreateReportScopeOptions, DeclarativeFinding, DeclarativeFindingResponse, DeclarativeRuleDefinition, type DirectoryPluginLockEntry, type GlobalSetupConfigPathOptions, LoadDeclarativeRulesOptions, type ModelSize, type ParseStaticConfigOptions, type ParsedDirectoryPluginLockEntry, type ParsedPluginLockEntry, type ParsedPluginLockFile, type ParsedPluginSpecifier, type ParsedRegistryPluginLockEntry, type ParsedStaticConfig, type ParsedStaticConfigGroup, type PluginLockEntry, type PluginLockFile, type ProviderDefinition, type ProviderType, type RegistryPluginLockEntry, ReportScope, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, type StaticConfigInput, type StaticConfigItem, type StaticPluginInstallOptions, type StaticPluginInstallResult, type StaticPluginReference, type StaticPluginResolver, type ToAlintConfigOptions, builtInAgentNames, createDeclarativePlugin, createEmptyPluginLockFile, createReportScope, declarativeFindingResponseSchema, declarativeFindingSchema, declarativeRuleFilePattern, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, hasDeclarativeRuleFiles, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, isBuiltInAgentName, listMissing, listUnresolved, loadAlintConfig, loadDeclarativeRules, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
package/dist/index.mjs CHANGED
@@ -1,16 +1,20 @@
1
- import { dirname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
1
+ import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
2
2
  import { loadConfig } from "c12";
3
3
  import { createJiti } from "jiti/static";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
- import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
6
- import { array, boolean, literal, looseObject, optional, parse, picklist, record, strictObject, string, tuple, union, unknown, variant } from "valibot";
5
+ import { access, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { array, boolean, check, description, literal, looseObject, number, object, optional, parse, picklist, pipe, record, strictObject, string, tuple, union, unknown, variant } from "valibot";
7
7
  import process from "node:process";
8
8
  import { homedir } from "node:os";
9
- import { dirname as dirname$1, extname, join as join$1 } from "pathe";
9
+ import { dirname as dirname$1, extname, join as join$1, normalize as normalize$1 } from "pathe";
10
10
  import { xdgConfig } from "xdg-basedir";
11
- import { isError } from "@moeru/std/error";
12
- import { exports } from "resolve.exports";
11
+ import { errorMessageFrom, isError } from "@moeru/std/error";
12
+ import { formatOutputLanguageInstruction, formatSourceWithLineNumbers, generateStructured, toolParametersFromSchema } from "@alint-js/core/structured-output";
13
+ import { createTools, listFiles } from "@alint-js/tools-fs";
14
+ import { chat, hasToolCall, or, rawTool, stepCountAtLeast, user } from "apeira";
13
15
  import { Buffer } from "node:buffer";
16
+ import { minimatch } from "minimatch";
17
+ import { exports } from "resolve.exports";
14
18
  import { createWriteStream } from "node:fs";
15
19
  import { Readable } from "node:stream";
16
20
  import { pipeline } from "node:stream/promises";
@@ -88,6 +92,386 @@ function parseIntegrity(integrity, specifier) {
88
92
  };
89
93
  }
90
94
  //#endregion
95
+ //#region src/plugins/declarative/types.ts
96
+ const builtInAgentNames = ["basic-structured", "basic-coding-agent"];
97
+ const declarativeRuleFilePattern = "**/rule.alint.{toml,yaml,yml,json,jsonc,json5}";
98
+ const declarativeFindingSchema = pipe(object({
99
+ confidence: optional(pipe(picklist([
100
+ "high",
101
+ "medium",
102
+ "low"
103
+ ]), description("Confidence in this finding. Use exactly \"high\", \"medium\", or \"low\" when confidence is known."))),
104
+ filePath: optional(pipe(string(), description("Path to the file that owns this finding. Omit when the finding belongs to the reviewed target file."))),
105
+ line: pipe(number(), description("Use the left-column line number from the numbered source block.")),
106
+ message: pipe(string(), description("Human-readable diagnostic message describing the issue.")),
107
+ suggestion: optional(pipe(string(), description("Concrete remediation direction for the finding.")))
108
+ }), description("One declarative rule finding."));
109
+ const declarativeFindingResponseSchema = pipe(object({ findings: pipe(array(declarativeFindingSchema), description("All findings for the declarative rule. Return an empty array when there are no issues.")) }), description("Structured declarative rule findings."));
110
+ function isBuiltInAgentName(value) {
111
+ return builtInAgentNames.includes(value);
112
+ }
113
+ //#endregion
114
+ //#region src/plugins/declarative/parse.ts
115
+ const declarativeRuleFileNamePattern = /^rule\.alint\.(?:toml|ya?ml|json|jsonc|json5)$/;
116
+ const declarativeRuleNamePattern = /^[\w.-]+$/;
117
+ const nonEmptyStringSchema = pipe(string(), check((value) => value.trim() !== "", "must be a non-empty string"));
118
+ const declarativeRuleFileSchema = object({
119
+ builtInAgent: picklist(builtInAgentNames),
120
+ excludeFiles: optional(array(string())),
121
+ includeFiles: optional(array(string())),
122
+ instruction: nonEmptyStringSchema,
123
+ name: pipe(nonEmptyStringSchema, check((value) => declarativeRuleNamePattern.test(value), "Declarative rule \"name\" must contain only letters, numbers, dots, underscores, and hyphens."))
124
+ });
125
+ async function hasDeclarativeRuleFiles(root) {
126
+ return (await findDeclarativeRuleFiles(root)).length > 0;
127
+ }
128
+ async function loadDeclarativeRules(options) {
129
+ const files = await findDeclarativeRuleFiles(options.root);
130
+ if (files.length === 0) throw new Error(`Directory plugin "${options.alias}" must contain package.json or rule.alint.* files.`);
131
+ const rules = [];
132
+ const filesByName = /* @__PURE__ */ new Map();
133
+ for (const file of files) {
134
+ const rule = await loadDeclarativeRuleFile(file);
135
+ const duplicateFile = filesByName.get(rule.name);
136
+ if (duplicateFile !== void 0) throw new Error(`Duplicate declarative rule name "${rule.name}" in ${file} and ${duplicateFile}.`);
137
+ filesByName.set(rule.name, file);
138
+ rules.push(rule);
139
+ }
140
+ return rules;
141
+ }
142
+ async function collectDeclarativeRuleFiles(directory, files) {
143
+ const entries = await readdir(directory, { withFileTypes: true });
144
+ await Promise.all(entries.map(async (entry) => {
145
+ const path = join(directory, entry.name);
146
+ if (entry.isDirectory()) {
147
+ await collectDeclarativeRuleFiles(path, files);
148
+ return;
149
+ }
150
+ if (entry.isFile() && declarativeRuleFileNamePattern.test(entry.name)) files.push(path);
151
+ }));
152
+ }
153
+ async function findDeclarativeRuleFiles(root) {
154
+ const files = [];
155
+ await collectDeclarativeRuleFiles(root, files);
156
+ return files.sort((left, right) => left.localeCompare(right));
157
+ }
158
+ function isPlainObject$3(value) {
159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
160
+ }
161
+ async function loadDeclarativeRuleFile(filePath) {
162
+ const input = await loadDeclarativeRuleFileContent(filePath);
163
+ if (!isPlainObject$3(input)) throw new Error(`Declarative rule file ${filePath} must contain an object.`);
164
+ const parsed = parseDeclarativeRuleFileInput(input, filePath);
165
+ return {
166
+ builtInAgent: parsed.builtInAgent,
167
+ excludeFiles: parsed.excludeFiles ?? [],
168
+ filePath,
169
+ includeFiles: parsed.includeFiles,
170
+ instruction: parsed.instruction,
171
+ name: parsed.name
172
+ };
173
+ }
174
+ async function loadDeclarativeRuleFileContent(filePath) {
175
+ try {
176
+ return (await loadConfig({
177
+ configFile: basename(filePath),
178
+ cwd: dirname(filePath),
179
+ envName: false,
180
+ extend: false,
181
+ packageJson: false,
182
+ rcFile: false
183
+ })).config;
184
+ } catch (error) {
185
+ throw new Error(`Could not parse declarative rule file ${filePath}: ${errorMessageFrom(error) ?? "Unknown error"}`, { cause: error });
186
+ }
187
+ }
188
+ function parseDeclarativeRuleFileInput(input, filePath) {
189
+ try {
190
+ return parse(declarativeRuleFileSchema, input);
191
+ } catch (error) {
192
+ if (typeof input.builtInAgent === "string" && !isBuiltInAgentName(input.builtInAgent)) throw new Error(`Unknown builtInAgent "${input.builtInAgent}" in ${filePath}.`, { cause: error });
193
+ throw new Error(`Invalid declarative rule file ${filePath}: ${errorMessageFrom(error) ?? "Unknown error"}`, { cause: error });
194
+ }
195
+ }
196
+ //#endregion
197
+ //#region src/plugins/declarative/scope.ts
198
+ function createReportScope(options) {
199
+ const includeFiles = options.includeFiles;
200
+ const excludeFiles = options.excludeFiles;
201
+ const rawTargetFilePath = options.targetFilePath;
202
+ const hasTargetFilePath = rawTargetFilePath !== void 0;
203
+ const targetFilePath = rawTargetFilePath === void 0 ? void 0 : toScopedRelativePath(options.cwd, rawTargetFilePath);
204
+ return { canReport(filePath) {
205
+ if (filePath === void 0) return !hasTargetFilePath && includeFiles === void 0;
206
+ const relativePath = toScopedRelativePath(options.cwd, filePath);
207
+ if (relativePath === void 0) return false;
208
+ if (!(includeFiles === void 0 ? targetFilePath !== void 0 && relativePath === targetFilePath : includeFiles.some((pattern) => minimatch(relativePath, pattern, { dot: true })))) return false;
209
+ return !excludeFiles.some((pattern) => minimatch(relativePath, pattern, { dot: true }));
210
+ } };
211
+ }
212
+ function isOutsideScopePath(path) {
213
+ return path === ".." || path.startsWith("../") || isAbsolute(path) || win32.isAbsolute(path) || /^[A-Za-z]:/u.test(path);
214
+ }
215
+ function normalizePath(path) {
216
+ return normalize$1(path).replace(/^\.\//u, "");
217
+ }
218
+ function toScopedRelativePath(cwd, filePath) {
219
+ const path = normalizePath(relative(cwd, filePath));
220
+ if (isOutsideScopePath(path)) return;
221
+ return path;
222
+ }
223
+ //#endregion
224
+ //#region src/agents/presets/basic-structured.ts
225
+ const maxSupplementalFileBytes = 64 * 1024;
226
+ const maxSupplementalFiles = 12;
227
+ function createBasicStructuredRule(rule) {
228
+ return {
229
+ cache: rule.includeFiles === void 0 || rule.includeFiles.length === 0,
230
+ create: (ctx) => ({ async onTargetFile(target) {
231
+ const model = await ctx.model();
232
+ const supplementalFiles = await collectSupplementalFiles({
233
+ cwd: ctx.cwd,
234
+ includeFiles: rule.includeFiles,
235
+ logger: ctx.logger,
236
+ targetFilePath: target.file.path
237
+ });
238
+ const { findings } = await generateStructured({
239
+ createMessages: (retryFeedback) => createStructuredMessagesSync({
240
+ cwd: ctx.cwd,
241
+ includeFiles: rule.includeFiles,
242
+ instruction: rule.instruction,
243
+ outputLanguage: ctx.outputLanguage,
244
+ retryFeedback,
245
+ ruleFilePath: rule.filePath,
246
+ sourceText: target.file.text,
247
+ supplementalFiles,
248
+ targetFilePath: target.file.path
249
+ }),
250
+ logger: ctx.logger,
251
+ metering: ctx.metering,
252
+ model,
253
+ operation: `declarative-${rule.name}-structured`,
254
+ schema: declarativeFindingResponseSchema
255
+ });
256
+ reportDeclarativeFindings({
257
+ ctx,
258
+ excludeFiles: rule.excludeFiles,
259
+ findings,
260
+ includeFiles: rule.includeFiles,
261
+ targetFilePath: target.file.path
262
+ });
263
+ } })
264
+ };
265
+ }
266
+ function createStructuredMessagesSync(options) {
267
+ return [
268
+ {
269
+ content: options.instruction,
270
+ role: "system"
271
+ },
272
+ ...options.retryFeedback ? [{
273
+ content: options.retryFeedback,
274
+ role: "user"
275
+ }] : [],
276
+ {
277
+ content: [
278
+ formatOutputLanguageInstruction(options.outputLanguage),
279
+ `Rule file path: ${options.ruleFilePath}`,
280
+ `Reviewed target file path: ${options.targetFilePath}`,
281
+ formatSupplementalFiles(options.cwd, options.supplementalFiles ?? []),
282
+ `Reviewed target source with line numbers:\n\n${formatSourceWithLineNumbers(options.sourceText)}`
283
+ ].filter(Boolean).join("\n\n"),
284
+ role: "user"
285
+ }
286
+ ];
287
+ }
288
+ function reportDeclarativeFindings(options) {
289
+ const scope = createReportScope({
290
+ cwd: options.ctx.cwd,
291
+ excludeFiles: options.excludeFiles,
292
+ includeFiles: options.includeFiles,
293
+ targetFilePath: options.targetFilePath
294
+ });
295
+ for (const finding of options.findings) {
296
+ const filePath = resolveFindingFilePath(options.ctx.cwd, options.targetFilePath, finding.filePath);
297
+ if (!scope.canReport(filePath)) {
298
+ options.ctx.logger.debug("Ignoring out-of-scope declarative finding", {
299
+ filePath,
300
+ message: finding.message
301
+ });
302
+ continue;
303
+ }
304
+ const evidence = createFindingEvidence(finding);
305
+ options.ctx.report({
306
+ ...evidence ? { evidence } : {},
307
+ filePath,
308
+ loc: { start: {
309
+ column: 0,
310
+ line: finding.line
311
+ } },
312
+ message: finding.message
313
+ });
314
+ }
315
+ }
316
+ async function collectSupplementalFiles(options) {
317
+ if (options.includeFiles === void 0 || options.includeFiles.length === 0) return [];
318
+ const targetFilePath = resolve(options.targetFilePath);
319
+ const filePaths = (await listFiles(options.cwd, { patterns: options.includeFiles })).map((filePath) => resolve(filePath)).filter((filePath) => filePath !== targetFilePath).sort().slice(0, maxSupplementalFiles);
320
+ const files = [];
321
+ for (const filePath of filePaths) {
322
+ const content = await readSupplementalFile(filePath, options.logger);
323
+ if (content === void 0) continue;
324
+ files.push({
325
+ content,
326
+ filePath
327
+ });
328
+ }
329
+ return files;
330
+ }
331
+ function createFindingEvidence(finding) {
332
+ const evidence = {
333
+ ...finding.confidence ? { confidence: finding.confidence } : {},
334
+ ...finding.suggestion ? { suggestion: finding.suggestion } : {}
335
+ };
336
+ return Object.keys(evidence).length > 0 ? evidence : void 0;
337
+ }
338
+ function formatSupplementalFiles(cwd, files) {
339
+ if (files.length === 0) return;
340
+ return ["Supplemental files:", ...files.map((file) => [`File: ${relative(cwd, file.filePath)}`, file.content].join("\n\n"))].join("\n\n");
341
+ }
342
+ async function readSupplementalFile(filePath, logger) {
343
+ let handle;
344
+ try {
345
+ handle = await open(filePath, "r");
346
+ const buffer = Buffer.alloc(65537);
347
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
348
+ if (bytesRead > maxSupplementalFileBytes) {
349
+ logger?.debug("Skipping oversized declarative supplemental file", {
350
+ filePath,
351
+ maxBytes: maxSupplementalFileBytes
352
+ });
353
+ return;
354
+ }
355
+ return buffer.subarray(0, bytesRead).toString("utf8");
356
+ } catch (error) {
357
+ logger?.debug("Skipping unreadable declarative supplemental file", {
358
+ error,
359
+ filePath
360
+ });
361
+ return;
362
+ } finally {
363
+ await handle?.close();
364
+ }
365
+ }
366
+ function resolveFindingFilePath(cwd, targetFilePath, findingFilePath) {
367
+ if (findingFilePath === void 0 || findingFilePath.length === 0) return targetFilePath;
368
+ return isAbsolute(findingFilePath) ? findingFilePath : resolve(cwd, findingFilePath);
369
+ }
370
+ //#endregion
371
+ //#region src/agents/presets/basic-coding-agent.ts
372
+ const maxAgentSteps = 8;
373
+ const reportFindingsToolName = "report_findings";
374
+ function createBasicCodingAgentRule(rule) {
375
+ return {
376
+ cache: false,
377
+ create: (ctx) => ({ async onTargetFile(target) {
378
+ const model = await ctx.model();
379
+ const result = await createCodingAgent(model).run({
380
+ cwd: ctx.cwd,
381
+ instruction: rule.instruction,
382
+ outputLanguage: ctx.outputLanguage,
383
+ sourceText: target.file.text,
384
+ targetFilePath: target.file.path,
385
+ tools: createTools(ctx.cwd)
386
+ });
387
+ if (result.usage) ctx.metering.recordUsage({
388
+ filePath: target.file.path,
389
+ inputTokens: result.usage.inputTokens,
390
+ metadata: { operation: `declarative-${rule.name}-coding-agent` },
391
+ modelId: model.id,
392
+ outputTokens: result.usage.outputTokens,
393
+ providerId: model.provider.id,
394
+ ruleId: ctx.id,
395
+ totalTokens: result.usage.totalTokens
396
+ });
397
+ reportDeclarativeFindings({
398
+ ctx,
399
+ excludeFiles: rule.excludeFiles,
400
+ findings: result.findings,
401
+ includeFiles: rule.includeFiles,
402
+ targetFilePath: target.file.path
403
+ });
404
+ } })
405
+ };
406
+ }
407
+ function createCodingAgent(model, runner = chat({
408
+ baseURL: model.provider.endpoint,
409
+ headers: model.provider.headers,
410
+ model: model.id,
411
+ parallelToolCalls: false,
412
+ stopWhen: or(hasToolCall(reportFindingsToolName), stepCountAtLeast(maxAgentSteps)),
413
+ toolChoice: "required"
414
+ })) {
415
+ return { async run(options) {
416
+ let report;
417
+ const tools = options.tools.map((agentTool) => rawTool({
418
+ description: agentTool.description,
419
+ execute: async (input) => {
420
+ return await agentTool.execute(input) ?? "";
421
+ },
422
+ name: agentTool.name,
423
+ parameters: agentTool.parameters
424
+ }));
425
+ tools.push(rawTool({
426
+ description: "Submit all findings and finish the review. Submit an empty findings array when there are no issues.",
427
+ execute: async (input) => {
428
+ report = parse(declarativeFindingResponseSchema, input);
429
+ return report;
430
+ },
431
+ name: reportFindingsToolName,
432
+ parameters: toolParametersFromSchema(declarativeFindingResponseSchema),
433
+ strict: true
434
+ }));
435
+ const result = await runner({
436
+ channel: {
437
+ emit: () => {},
438
+ subscribe: () => () => {}
439
+ },
440
+ input: [user([
441
+ formatOutputLanguageInstruction(options.outputLanguage),
442
+ `Project root: ${options.cwd}`,
443
+ `Reviewed target file path: ${options.targetFilePath}`,
444
+ `Reviewed target source with line numbers:\n\n${formatSourceWithLineNumbers(options.sourceText)}`
445
+ ].filter(Boolean).join("\n\n"))],
446
+ instructions: [
447
+ options.instruction,
448
+ "Use the filesystem tools to inspect the project as needed before reaching a conclusion.",
449
+ `When the review is complete, call ${reportFindingsToolName} exactly once with all findings. Submit an empty findings array when there are no issues.`
450
+ ].join("\n\n"),
451
+ tools,
452
+ turnId: "alint"
453
+ });
454
+ if (!report) throw new Error(`basic-coding-agent stopped without calling ${reportFindingsToolName}`);
455
+ return {
456
+ findings: report.findings,
457
+ usage: result.usage ? {
458
+ inputTokens: result.usage.inputTokens,
459
+ outputTokens: result.usage.outputTokens,
460
+ totalTokens: result.usage.totalTokens
461
+ } : void 0
462
+ };
463
+ } };
464
+ }
465
+ //#endregion
466
+ //#region src/plugins/declarative/plugin.ts
467
+ function createDeclarativePlugin(options) {
468
+ return { rules: Object.fromEntries(options.rules.map((rule) => [rule.name, createDeclarativeRule(rule)])) };
469
+ }
470
+ function createDeclarativeRule(rule) {
471
+ if (rule.builtInAgent === "basic-coding-agent") return createBasicCodingAgentRule(rule);
472
+ return createBasicStructuredRule(rule);
473
+ }
474
+ //#endregion
91
475
  //#region src/plugins/sources/manifest.ts
92
476
  function resolveRelativeRootEntry(packageJson) {
93
477
  const [entry] = exports(packageJson, ".", {
@@ -114,7 +498,7 @@ async function createLockEntry$1(installed, options) {
114
498
  }
115
499
  async function install$1(options) {
116
500
  const packageDir = await canonicalRoot(options.alias, options.specifier.directory);
117
- await validateRoot(options.alias, packageDir);
501
+ await validateInstallRoot(options.alias, packageDir);
118
502
  return {
119
503
  path: packageDir,
120
504
  type: "directory"
@@ -124,10 +508,7 @@ async function resolve$2(entry) {
124
508
  const lockedPath = isAbsolute(entry.lockEntry.path) || win32.isAbsolute(entry.lockEntry.path) ? entry.lockEntry.path : resolve(entry.cwd, entry.lockEntry.path);
125
509
  const [packageDir, lockedPackageDir] = await Promise.all([lockedRoot(entry.alias, entry.specifier.directory, "configured"), lockedRoot(entry.alias, lockedPath, "locked")]);
126
510
  if (packageDir !== lockedPackageDir) throw new Error(`Directory plugin "${entry.alias}" has moved or its symlink target changed. Run: alint plugin install`);
127
- return {
128
- cache: "content",
129
- entry: await validateRoot(entry.alias, packageDir)
130
- };
511
+ return validateResolvedRoot(entry.alias, packageDir);
131
512
  }
132
513
  async function canonicalRoot(alias, directory) {
133
514
  let packageDir;
@@ -146,6 +527,15 @@ async function canonicalRoot(alias, directory) {
146
527
  if (!directoryStat.isDirectory()) throw new Error(`Directory plugin "${alias}" path "${directory}" is not a directory.`);
147
528
  return packageDir;
148
529
  }
530
+ async function hasPackageJson(packageDir) {
531
+ try {
532
+ await access(join(packageDir, "package.json"));
533
+ return true;
534
+ } catch (error) {
535
+ if (isENOENTError(error)) return false;
536
+ throw error;
537
+ }
538
+ }
149
539
  async function lockedRoot(alias, directory, source) {
150
540
  try {
151
541
  return await realpath(directory);
@@ -154,7 +544,25 @@ async function lockedRoot(alias, directory, source) {
154
544
  throw new Error(`Could not resolve ${source} directory plugin "${alias}" at "${directory}".`, { cause: error });
155
545
  }
156
546
  }
157
- async function validateRoot(alias, packageDir) {
547
+ async function validateInstallRoot(alias, packageDir) {
548
+ if (await hasPackageJson(packageDir)) return {
549
+ alias,
550
+ cache: "content",
551
+ entry: await validatePackageRoot(alias, packageDir),
552
+ kind: "module"
553
+ };
554
+ await loadDeclarativeRules({
555
+ alias,
556
+ root: packageDir
557
+ });
558
+ return {
559
+ alias,
560
+ cache: "content",
561
+ entry: packageDir,
562
+ kind: "declarative"
563
+ };
564
+ }
565
+ async function validatePackageRoot(alias, packageDir) {
158
566
  const manifestPath = join(packageDir, "package.json");
159
567
  let packageJson;
160
568
  try {
@@ -183,6 +591,21 @@ async function validateRoot(alias, packageDir) {
183
591
  if (!entryStat.isFile()) throw new Error(`Directory plugin "${alias}" entry "${relativeEntry}" is not a regular file.`);
184
592
  return physicalEntry;
185
593
  }
594
+ async function validateResolvedRoot(alias, packageDir) {
595
+ if (await hasPackageJson(packageDir)) return {
596
+ alias,
597
+ cache: "content",
598
+ entry: await validatePackageRoot(alias, packageDir),
599
+ kind: "module"
600
+ };
601
+ if (!await hasDeclarativeRuleFiles(packageDir)) throw new Error(`Directory plugin "${alias}" must contain package.json or rule.alint.* files.`);
602
+ return {
603
+ alias,
604
+ cache: "content",
605
+ entry: packageDir,
606
+ kind: "declarative"
607
+ };
608
+ }
186
609
  //#endregion
187
610
  //#region src/plugins/sources/package/index.ts
188
611
  function createLockEntry(installed, options) {
@@ -246,8 +669,10 @@ async function resolve$1(entry) {
246
669
  if (!isPathInside(resolvedEntry, packageDir)) throw new Error(`Plugin lock entry "${entry.alias}" resolves outside the locked package directory.`);
247
670
  JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
248
671
  return {
672
+ alias: entry.alias,
249
673
  cache: "default",
250
- entry: resolvedEntry
674
+ entry: resolvedEntry,
675
+ kind: "module"
251
676
  };
252
677
  }
253
678
  async function extractEntry(packageDir, header, stream) {
@@ -563,6 +988,10 @@ function resolveDirectoryLockIdentity(path, cwd) {
563
988
  //#endregion
564
989
  //#region src/plugins/sources/import.ts
565
990
  async function importPlugin(target) {
991
+ if (target.kind === "declarative") return createDeclarativePlugin({ rules: await loadDeclarativeRules({
992
+ alias: target.alias,
993
+ root: target.entry
994
+ }) });
566
995
  const url = pathToFileURL(target.entry);
567
996
  if (target.cache === "content") url.searchParams.set("content", createHash("sha256").update(await readFile(target.entry)).digest("hex"));
568
997
  return getDefaultExport(await import(url.href));
@@ -584,6 +1013,7 @@ const ruleConfigEntrySchema = union([ruleSeveritySchema, tuple([ruleSeveritySche
584
1013
  const staticConfigItemSchema = looseObject({
585
1014
  agent: optional(unknown()),
586
1015
  basePath: optional(string()),
1016
+ directories: optional(array(filePatternSchema)),
587
1017
  extends: optional(array(unknown())),
588
1018
  files: optional(array(filePatternSchema)),
589
1019
  ignore: optional(looseObject({ gitignore: optional(boolean()) })),
@@ -1094,4 +1524,4 @@ async function writeSetupConfig(filePath, config) {
1094
1524
  await writeFile(filePath, stringifySetupConfigToml(config), "utf8");
1095
1525
  }
1096
1526
  //#endregion
1097
- export { createEmptyPluginLockFile, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, listMissing, listUnresolved, loadAlintConfig, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
1527
+ export { builtInAgentNames, createDeclarativePlugin, createEmptyPluginLockFile, createReportScope, declarativeFindingResponseSchema, declarativeFindingSchema, declarativeRuleFilePattern, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, hasDeclarativeRuleFiles, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, isBuiltInAgentName, listMissing, listUnresolved, loadAlintConfig, loadDeclarativeRules, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/config",
3
3
  "type": "module",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -15,9 +15,11 @@
15
15
  "dependencies": {
16
16
  "@moeru/std": "^0.1.0-beta.18",
17
17
  "@package-json/types": "^0.0.13",
18
+ "apeira": "^0.0.7",
18
19
  "c12": "^3.3.4",
19
20
  "es-toolkit": "^1.49.0",
20
21
  "jiti": "^2.7.0",
22
+ "minimatch": "^10.2.5",
21
23
  "ofetch": "^1.5.1",
22
24
  "pathe": "^2.0.3",
23
25
  "resolve.exports": "2.0.3",
@@ -25,15 +27,12 @@
25
27
  "tar-stream": "^3.2.0",
26
28
  "valibot": "^1.4.1",
27
29
  "xdg-basedir": "^5.1.0",
28
- "@alint-js/core": "0.0.21"
30
+ "@alint-js/core": "0.0.23",
31
+ "@alint-js/tools-fs": "0.0.23"
29
32
  },
30
33
  "devDependencies": {
31
- "@types/node": "^26.0.1",
32
34
  "@types/tar-stream": "^3.1.4",
33
- "h3": "2.0.1-rc.25",
34
- "tsdown": "^0.22.3",
35
- "typescript": "^6.0.3",
36
- "vitest": "^4.1.9"
35
+ "h3": "2.0.1-rc.25"
37
36
  },
38
37
  "scripts": {
39
38
  "build": "tsdown",