@alint-js/config 0.0.21 → 0.0.22

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