@alint-js/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,15 +1,17 @@
1
1
  import { o as withAgentRetry, t as combineAbortSignals } from "./agent-B6GG68NX.mjs";
2
2
  import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-Da342Fcx.mjs";
3
- import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-CLXRRiJW.mjs";
3
+ import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-DhLFgRej.mjs";
4
4
  import { minimatch } from "minimatch";
5
5
  import { basename, extname, isAbsolute, relative, resolve } from "pathe";
6
- import { access, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
7
- import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
8
- import { randomUUID } from "node:crypto";
6
+ import { array, boolean, check, custom, finite, getDotPath, integer, literal, looseObject, maxValue, minValue, number, object, optional, parse, picklist, pipe, record, safeParse, string, union, unknown } from "valibot";
9
7
  import process, { cwd } from "node:process";
10
- import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
11
8
  import { Buffer } from "node:buffer";
9
+ import { randomUUID } from "node:crypto";
12
10
  import { statSync } from "node:fs";
11
+ import { access, appendFile, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
12
+ import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
13
+ import lockfile from "proper-lockfile";
14
+ import { isNodeErrorCode } from "@alint-js/utils/node";
13
15
  import { errorMessageFrom } from "@moeru/std/error";
14
16
  import { AsyncLocalStorage } from "node:async_hooks";
15
17
  import { streamText } from "@xsai/stream-text";
@@ -236,744 +238,1049 @@ function withExpansionMetadata(item, inheritedMatcherScopes, effectiveBasePath)
236
238
  return item;
237
239
  }
238
240
  //#endregion
239
- //#region src/core/languages/js/index.ts
240
- const javascriptLanguage = {
241
- extensions: [
242
- ".cjs",
243
- ".js",
244
- ".jsx",
245
- ".mjs"
246
- ],
247
- extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
248
- name: "javascript"
241
+ //#region src/config/stop-gate.ts
242
+ const maximumStopGateTimeoutMs = 861e5;
243
+ const defaultStopGateConfig = {
244
+ enabled: false,
245
+ target: "dirty-files",
246
+ timeoutMs: 9e5
249
247
  };
250
- //#endregion
251
- //#region src/core/languages/registry.ts
252
- function createLanguageRegistry$1() {
248
+ const stopGateConfigSchema = looseObject({
249
+ enabled: optional(boolean("integrations.stopGate.enabled must be a boolean.")),
250
+ target: optional(picklist(["all", "dirty-files"], "integrations.stopGate.target must be \"all\" or \"dirty-files\".")),
251
+ timeoutMs: optional(pipe(number(`integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), integer(`integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), minValue(1, `integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`), maxValue(maximumStopGateTimeoutMs, `integrations.stopGate.timeoutMs must be an integer from 1 to ${maximumStopGateTimeoutMs}.`)))
252
+ }, "integrations.stopGate must be an object.");
253
+ function resolveStopGateConfig(config, cwd) {
254
+ const project = resolveConfigForProject(cwd, config, { cwd });
255
+ for (const skipped of project.skipped) if (skipped.item.integrations?.stopGate !== void 0) throw new Error("integrations.stopGate must be declared in a global config item without files, directories, or ignores.");
256
+ return project.matched.reduce((resolved, item) => mergeStopGateConfig(resolved, item), { ...defaultStopGateConfig });
257
+ }
258
+ function assertStopGateConfig(value) {
259
+ parse(stopGateConfigSchema, value);
260
+ }
261
+ function mergeStopGateConfig(resolved, item) {
262
+ const stopGate = item.integrations?.stopGate;
263
+ if (stopGate === void 0) return resolved;
264
+ assertStopGateConfig(stopGate);
253
265
  return {
254
- byExtension: /* @__PURE__ */ new Map(),
255
- languages: /* @__PURE__ */ new Map()
266
+ enabled: stopGate.enabled ?? resolved.enabled,
267
+ target: stopGate.target ?? resolved.target,
268
+ timeoutMs: stopGate.timeoutMs ?? resolved.timeoutMs
256
269
  };
257
270
  }
258
- function registerLanguage(registry, language) {
259
- const existing = registry.languages.get(language.name);
260
- if (existing && existing !== language) throw new Error(`Duplicate language "${language.name}".`);
261
- for (const extension of language.extensions ?? []) {
262
- const existingOwner = registry.byExtension.get(extension);
263
- if (existingOwner && existingOwner !== language.name) throw new Error(`Duplicate language extension "${extension}".`);
264
- }
265
- registry.languages.set(language.name, language);
266
- for (const extension of language.extensions ?? []) registry.byExtension.set(extension, language.name);
267
- }
268
271
  //#endregion
269
- //#region src/core/languages/text.ts
270
- const textLanguage = {
271
- extensions: [],
272
- extract: (file) => [{
273
- file,
274
- identity: "file",
275
- kind: "file",
276
- language: "plaintext",
277
- origin: { physicalPath: file.path },
278
- text: file.text
279
- }],
280
- name: "plaintext"
281
- };
272
+ //#region package.json
273
+ var version = "0.7.0";
282
274
  //#endregion
283
- //#region src/core/languages/ts/index.ts
284
- const typescriptLanguage = {
285
- extensions: [
286
- ".cts",
287
- ".mts",
288
- ".ts",
289
- ".tsx"
290
- ],
291
- extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
292
- name: "typescript"
293
- };
275
+ //#region src/core/cache/types.ts
276
+ const CACHE_MAGIC = "ALINT_CACHE";
294
277
  //#endregion
295
- //#region src/core/languages/resolve.ts
296
- function resolveLanguage(file, registry, options) {
297
- return resolveLanguageForPath(file.path, registry, options);
298
- }
299
- function resolveLanguageForPath(filePath, registry, options) {
300
- const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
301
- const language = registry.languages.get(languageName);
302
- if (!language) {
303
- const registered = [...registry.languages.keys()].sort().join(", ");
304
- throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
305
- }
306
- return language;
278
+ //#region src/core/cache/store.ts
279
+ const DEFAULT_CACHE_FILE_NAME = ".alintcache";
280
+ const LOCK_STALE_MS = 2e4;
281
+ const LOCK_UPDATE_MS = 3e3;
282
+ const METADATA_READ_LIMIT = 4096;
283
+ const FiniteNumberSchema = pipe(number(), finite());
284
+ const PositionSchema = object({
285
+ column: FiniteNumberSchema,
286
+ line: FiniteNumberSchema
287
+ });
288
+ const SourceLocationSchema = object({
289
+ end: PositionSchema,
290
+ start: PositionSchema
291
+ });
292
+ const DiagnosticLocationSchema = object({
293
+ end: optional(PositionSchema),
294
+ start: PositionSchema
295
+ });
296
+ const SourceRangeSchema = object({
297
+ end: FiniteNumberSchema,
298
+ start: FiniteNumberSchema
299
+ });
300
+ const DiagnosticSchema = object({
301
+ cached: optional(boolean()),
302
+ evidence: optional(custom(isJsonValue)),
303
+ filePath: string(),
304
+ loc: optional(DiagnosticLocationSchema),
305
+ message: string(),
306
+ model: optional(object({
307
+ providerId: string(),
308
+ requested: optional(string()),
309
+ resolvedId: string()
310
+ })),
311
+ ruleId: string(),
312
+ severity: union([literal("error"), literal("warn")])
313
+ });
314
+ const UsageSchema = object({
315
+ filePath: optional(string()),
316
+ inputTokens: optional(FiniteNumberSchema),
317
+ metadata: optional(custom(isJsonValue)),
318
+ modelId: string(),
319
+ outputTokens: optional(FiniteNumberSchema),
320
+ providerId: string(),
321
+ ruleId: string(),
322
+ totalTokens: optional(FiniteNumberSchema)
323
+ });
324
+ const CacheEntrySchema = object({
325
+ diagnostics: array(DiagnosticSchema),
326
+ fingerprint: object({
327
+ configHash: string(),
328
+ modelHash: string(),
329
+ ruleHash: string(),
330
+ targetHash: string()
331
+ }),
332
+ target: object({
333
+ hash: string(),
334
+ identity: string(),
335
+ kind: string(),
336
+ loc: optional(SourceLocationSchema),
337
+ name: optional(string()),
338
+ range: optional(SourceRangeSchema)
339
+ }),
340
+ usage: array(UsageSchema)
341
+ });
342
+ const CachedOwnerSchema = object({
343
+ contentHash: optional(string()),
344
+ kind: union([literal("file"), literal("project")]),
345
+ path: string(),
346
+ slots: array(string())
347
+ });
348
+ function objectRecord(value) {
349
+ return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
307
350
  }
308
- //#endregion
309
- //#region src/core/languages/index.ts
310
- function createBuiltInLanguageRegistry() {
311
- const registry = createLanguageRegistry$1();
312
- registerLanguage(registry, textLanguage);
313
- registerLanguage(registry, javascriptLanguage);
314
- registerLanguage(registry, typescriptLanguage);
315
- return registry;
351
+ const CacheFileBodySchema = pipe(object({
352
+ createdAt: string(),
353
+ entries: objectRecord(CacheEntrySchema),
354
+ owners: objectRecord(CachedOwnerSchema),
355
+ updatedAt: string()
356
+ }), check((body) => {
357
+ const entryKeys = Object.keys(body.entries);
358
+ const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
359
+ const uniqueSlots = new Set(referencedSlots);
360
+ return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
361
+ }));
362
+ const CacheMetadataSchema = object({
363
+ alintVersion: string(),
364
+ createdAt: string(),
365
+ magic: literal(CACHE_MAGIC),
366
+ schemaVersion: literal(2),
367
+ type: literal("metadata")
368
+ });
369
+ const PutEventSchema = object({
370
+ at: string(),
371
+ entry: CacheEntrySchema,
372
+ owner: CachedOwnerSchema,
373
+ ownerKey: string(),
374
+ slotKey: string(),
375
+ type: literal("put")
376
+ });
377
+ const ReplaceOwnerEventSchema = object({
378
+ at: string(),
379
+ entries: array(object({
380
+ entry: CacheEntrySchema,
381
+ slotKey: string()
382
+ })),
383
+ mode: union([literal("merge"), literal("replace")]),
384
+ owner: CachedOwnerSchema,
385
+ ownerKey: string(),
386
+ type: literal("replace-owner")
387
+ });
388
+ const RemoveOwnerEventSchema = object({
389
+ at: string(),
390
+ ownerKey: string(),
391
+ type: literal("remove-owner")
392
+ });
393
+ const CacheEventSchema = union([
394
+ PutEventSchema,
395
+ ReplaceOwnerEventSchema,
396
+ RemoveOwnerEventSchema
397
+ ]);
398
+ async function createCacheStore(options) {
399
+ const location = resolveCacheLocation(options.cwd, options.location);
400
+ if (!options.enabled) return createNoopCacheStore(location);
401
+ const alintVersion = options.alintVersion ?? version;
402
+ const initial = await loadCacheLog(location, alintVersion);
403
+ if (options.readOnly) return createReadOnlyCacheStore(location, initial.body, options.cwd);
404
+ const dependencies = {
405
+ append: options.appendFile ?? appendFile,
406
+ lock: options.lock ?? { acquire: lockfile.lock },
407
+ write: options.writeFile ?? writeFile
408
+ };
409
+ const trimmed = await trimOnce(location, alintVersion, initial, dependencies);
410
+ const writer = createEventWriter(location, trimmed.metadata, dependencies);
411
+ const fileExists = options.fileExists ?? defaultFileExists;
412
+ return {
413
+ beginOwner: (owner, metadata) => beginOwner(trimmed.body, owner, options.cwd, writer, metadata),
414
+ flush: () => writer.flush(false),
415
+ location,
416
+ reconcile: async () => {
417
+ const removedOwners = await collectMissingFileOwners(trimmed.body, options.cwd, fileExists);
418
+ for (const ownerKey of removedOwners) writer.enqueue({
419
+ at: (/* @__PURE__ */ new Date()).toISOString(),
420
+ ownerKey,
421
+ type: "remove-owner"
422
+ });
423
+ await writer.flush(true);
424
+ }
425
+ };
316
426
  }
317
- //#endregion
318
- //#region src/dsl/registry.ts
319
- function buildRuleRegistry(config) {
320
- const rules = /* @__PURE__ */ new Map();
321
- const localIds = /* @__PURE__ */ new Map();
322
- const enabledRules = [];
323
- for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
324
- const id = `${alias}/${localId}`;
325
- if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
326
- rules.set(id, rule);
327
- localIds.set(id, localId);
427
+ function createTargetIdentityResolver(targets) {
428
+ const baseCounts = /* @__PURE__ */ new Map();
429
+ const duplicateCandidateCounts = /* @__PURE__ */ new Map();
430
+ for (const target of targets) {
431
+ const base = createBaseTargetIdentity(target);
432
+ baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
328
433
  }
329
- for (const [id, entry] of Object.entries(config.rules)) {
330
- const rule = rules.get(id);
331
- if (!rule) throw new Error(`Unknown rule "${id}".`);
332
- const normalizedEntry = normalizeRuleConfigEntry(entry);
333
- const severity = normalizedEntry.severity;
334
- if (severity === "off") continue;
335
- enabledRules.push({
336
- id,
337
- localId: localIds.get(id) ?? id,
338
- options: parseRuleOptions(id, rule.options, normalizedEntry.options),
339
- rule,
340
- severity
341
- });
434
+ for (const target of targets) {
435
+ const base = createBaseTargetIdentity(target);
436
+ if ((baseCounts.get(base) ?? 0) <= 1) continue;
437
+ const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
438
+ duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
342
439
  }
440
+ return (target, targetIndex) => {
441
+ const base = createBaseTargetIdentity(target);
442
+ if ((baseCounts.get(base) ?? 0) <= 1) return base;
443
+ const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
444
+ if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
445
+ return `${candidate}:${targetIndex}`;
446
+ };
447
+ }
448
+ function normalizeCachePath(cwd, filePath) {
449
+ const resolvedPath = isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath);
450
+ return relative$1(cwd, resolvedPath).split(sep).join("/") || ".";
451
+ }
452
+ function normalizeRunnerCacheConfig(cache, cwd) {
453
+ if (cache === false) return {
454
+ enabled: false,
455
+ location: resolveCacheLocation(cwd)
456
+ };
457
+ if (cache === true || cache === void 0) return {
458
+ enabled: true,
459
+ location: resolveCacheLocation(cwd)
460
+ };
343
461
  return {
344
- enabledRules,
345
- rules
462
+ enabled: cache.enabled ?? true,
463
+ location: resolveCacheLocation(cwd, cache.location)
346
464
  };
347
465
  }
348
- function formatRuleOptionIssues(index, issues) {
349
- return issues.map((issue) => {
350
- const path = getDotPath(issue);
351
- return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
352
- }).join("; ");
466
+ function resolveCacheLocation(cwd, location) {
467
+ if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
468
+ const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
469
+ if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
470
+ try {
471
+ if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
472
+ } catch {}
473
+ return resolved;
353
474
  }
354
- function isRuleConfigTuple(entry) {
355
- return Array.isArray(entry);
475
+ async function appendBatch(location, payload, append) {
476
+ const handle = await open(location, "a+");
477
+ try {
478
+ const { size } = await handle.stat();
479
+ let separator = "";
480
+ if (size > 0) {
481
+ const finalByte = Buffer.alloc(1);
482
+ await handle.read(finalByte, 0, 1, size - 1);
483
+ separator = finalByte[0] === 10 ? "" : "\n";
484
+ }
485
+ await append(location, `${separator}${payload}`, "utf8");
486
+ } finally {
487
+ await handle.close();
488
+ }
356
489
  }
357
- function normalizeRuleConfigEntry(entry) {
358
- if (isRuleConfigTuple(entry)) {
359
- const [severity = "warn", ...options] = entry;
360
- return {
361
- options,
362
- severity
490
+ function applyEvent(body, event) {
491
+ if (event.type === "remove-owner") {
492
+ removeOwner(body, event.ownerKey);
493
+ body.updatedAt = event.at;
494
+ return;
495
+ }
496
+ if (event.type === "put") {
497
+ const current = body.owners[event.ownerKey];
498
+ const slots = new Set(current?.slots ?? []);
499
+ slots.add(event.slotKey);
500
+ body.entries[event.slotKey] = event.entry;
501
+ body.owners[event.ownerKey] = {
502
+ ...event.owner,
503
+ slots: [...slots].sort()
363
504
  };
505
+ body.updatedAt = event.at;
506
+ return;
364
507
  }
365
- return {
366
- options: [],
367
- severity: entry ?? "warn"
508
+ const entries = event.mode === "merge" ? ownerEntries(body, body.owners[event.ownerKey]) : /* @__PURE__ */ new Map();
509
+ for (const item of event.entries) entries.set(item.slotKey, item.entry);
510
+ for (const slot of body.owners[event.ownerKey]?.slots ?? []) delete body.entries[slot];
511
+ for (const [slotKey, entry] of entries) body.entries[slotKey] = entry;
512
+ body.owners[event.ownerKey] = {
513
+ ...event.owner,
514
+ slots: [...entries.keys()].sort()
368
515
  };
516
+ body.updatedAt = event.at;
369
517
  }
370
- function parseRuleOption(id, index, schema, value) {
371
- const result = safeParse(schema, value);
372
- if (result.success) return result.output;
373
- if (value === void 0) {
374
- const emptyObjectResult = safeParse(schema, {});
375
- if (emptyObjectResult.success) return emptyObjectResult.output;
376
- }
377
- throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
378
- }
379
- function parseRuleOptions(id, schema, options) {
380
- if (!schema) {
381
- if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
382
- return [];
383
- }
384
- if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
385
- return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
386
- }
387
- //#endregion
388
- //#region src/core/languages/diagnostics.ts
389
- /** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
390
- const MISSING_LANGUAGE_RULE = "alint/missing-language";
391
- /** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
392
- const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
393
- /**
394
- * One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
395
- * matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
396
- */
397
- function missingLanguageDiagnostics(missing) {
398
- return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
399
- const ruleIds = [...entry.ruleIds].sort();
400
- const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
401
- return {
402
- filePath: entry.filePath,
403
- message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
404
- ruleId: MISSING_LANGUAGE_RULE,
405
- severity: "error"
518
+ function beginOwner(body, owner, cwd, writer, ownerMetadata = {}) {
519
+ const normalizedOwner = {
520
+ kind: owner.kind,
521
+ path: normalizeCachePath(cwd, owner.path)
522
+ };
523
+ const key = ownerKey(normalizedOwner, cwd);
524
+ const previousOwner = body.owners[key];
525
+ const nextEntries = /* @__PURE__ */ new Map();
526
+ const checkpointEntries = /* @__PURE__ */ new Map();
527
+ const commit = (metadata = {}) => {
528
+ const contentHash = metadata.contentHash ?? ownerMetadata.contentHash;
529
+ const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
530
+ for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
531
+ const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
532
+ for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
533
+ for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
534
+ const cachedOwner = {
535
+ contentHash,
536
+ kind: normalizedOwner.kind,
537
+ path: normalizedOwner.path,
538
+ slots: [...committedEntries.keys()].sort()
406
539
  };
407
- });
408
- }
409
- /** Records one rule that named a language the config never registered, grouped by language id. */
410
- function recordMissingLanguage(into, languageId, ruleId, path) {
411
- const existing = into.get(languageId);
412
- if (existing === void 0) {
413
- into.set(languageId, {
414
- filePath: path,
415
- ruleIds: /* @__PURE__ */ new Set([ruleId])
540
+ body.owners[key] = cachedOwner;
541
+ body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
542
+ writer.enqueue({
543
+ at: body.updatedAt,
544
+ entries: [...committedEntries].map(([slotKey, entry]) => ({
545
+ entry,
546
+ slotKey
547
+ })),
548
+ mode: metadata.mode ?? "replace",
549
+ owner: cachedOwner,
550
+ ownerKey: key,
551
+ type: "replace-owner"
416
552
  });
417
- return;
418
- }
419
- existing.ruleIds.add(ruleId);
420
- if (path < existing.filePath) existing.filePath = path;
421
- }
422
- /**
423
- * Records one linted file no language claimed, grouped by extension so a missing pack is one
424
- * diagnostic, not one per file. `'off'` records nothing.
425
- */
426
- function recordUnregistered(into, path, severity) {
427
- if (severity === "off") return;
428
- const key = extname(path) || basename(path);
429
- const existing = into.get(key);
430
- into.set(key, {
431
- count: (existing?.count ?? 0) + 1,
432
- filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
433
- severity: existing?.severity === "error" ? "error" : severity
434
- });
553
+ checkpointEntries.clear();
554
+ };
555
+ return {
556
+ checkpoint: async () => {
557
+ const cachedOwner = {
558
+ contentHash: ownerMetadata.contentHash,
559
+ kind: normalizedOwner.kind,
560
+ path: normalizedOwner.path,
561
+ slots: []
562
+ };
563
+ for (const [entryKey, cacheEntry] of checkpointEntries) {
564
+ const event = {
565
+ at: (/* @__PURE__ */ new Date()).toISOString(),
566
+ entry: cacheEntry,
567
+ owner: cachedOwner,
568
+ ownerKey: key,
569
+ slotKey: entryKey,
570
+ type: "put"
571
+ };
572
+ applyEvent(body, event);
573
+ writer.enqueue(event);
574
+ }
575
+ checkpointEntries.clear();
576
+ await writer.flush(false);
577
+ },
578
+ commit,
579
+ discard: (cacheSlot) => {
580
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
581
+ nextEntries.delete(entryKey);
582
+ checkpointEntries.delete(entryKey);
583
+ },
584
+ lookup: (cacheSlot, fingerprint) => {
585
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
586
+ const cached = body.entries[entryKey];
587
+ if (!cached || !fingerprintsEqual(cached.fingerprint, fingerprint)) return void 0;
588
+ nextEntries.set(entryKey, cached);
589
+ return cached;
590
+ },
591
+ put: (cacheSlot, cacheEntry) => {
592
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
593
+ nextEntries.set(entryKey, cacheEntry);
594
+ checkpointEntries.set(entryKey, cacheEntry);
595
+ }
596
+ };
435
597
  }
436
- /** One diagnostic per extension, sorted, for the run to prepend to its results. */
437
- function unregisteredLanguageDiagnostics(unregistered) {
438
- return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
439
- filePath: entry.filePath,
440
- message: `No language is registered for "${extension}": ${entry.count} ${entry.count === 1 ? "file was" : "files were"} handled as plain text. Register one through a plugin's "languages", or pin \`language: 'plaintext'\` to accept plain text.`,
441
- ruleId: UNREGISTERED_LANGUAGE_RULE,
442
- severity: entry.severity
443
- }));
598
+ async function collectMissingFileOwners(body, cwd, fileExists) {
599
+ const removed = [];
600
+ for (const [key, owner] of Object.entries(body.owners)) {
601
+ if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
602
+ removeOwner(body, key);
603
+ removed.push(key);
604
+ }
605
+ return removed;
444
606
  }
445
- function unregisteredLanguageSeverity(linterOptions) {
446
- const value = linterOptions.reportUnregisteredLanguages;
447
- return value === "error" || value === "off" ? value : "warn";
607
+ function createBaseTargetIdentity(target) {
608
+ if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
609
+ if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
610
+ if (target.name) return `${target.kind}:${target.name}`;
611
+ if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
612
+ return target.kind;
448
613
  }
449
- //#endregion
450
- //#region src/core/languages/rule-languages.ts
451
- /**
452
- * The language a file falls back to when no registered language claims its extension. Not a real
453
- * language, which is why `'any'` excludes it.
454
- */
455
- const PLAIN_TEXT = "plaintext";
456
- /**
457
- * Whether a rule reads this target.
458
- *
459
- * A rule with language off still reads file targets, because reading raw text, or asking a model about a
460
- * whole file, needs no language. However, it never reads the function and class targets extraction produces.
461
- * `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
462
- *
463
- * `'any'` means any real language, so a file that fell back to plain text because nothing claimed
464
- * its extension is withheld: the rule asked for structure and there is none to give. The run says so
465
- * once per extension through `alint/unregistered-language` rather than letting each rule report an
466
- * empty file.
467
- */
468
- function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
469
- if (languages.kind === "off") return targetKind === "file";
470
- if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
471
- return languages.ids.includes(targetLanguage);
614
+ function createEmptyCacheBody(createdAt = (/* @__PURE__ */ new Date()).toISOString()) {
615
+ return {
616
+ createdAt,
617
+ entries: {},
618
+ owners: {},
619
+ updatedAt: createdAt
620
+ };
621
+ }
622
+ function createEventWriter(location, metadata, dependencies) {
623
+ const backlog = [];
624
+ let validationError;
625
+ let previousWrite = Promise.resolve();
626
+ const persistBacklog = async () => {
627
+ if (validationError) throw validationError;
628
+ if (backlog.length === 0) return;
629
+ await mkdir(dirname(location), { recursive: true });
630
+ await withCacheLock(location, dependencies.lock, getCacheLockOptions({
631
+ factor: 2,
632
+ maxTimeout: 16e3,
633
+ minTimeout: 2e3,
634
+ retries: 4
635
+ }), async () => {
636
+ await ensureCompatibleMetadata(location, metadata, dependencies.write);
637
+ const batchSize = backlog.length;
638
+ await appendBatch(location, backlog.slice(0, batchSize).map((event) => `${JSON.stringify(event)}\n`).join(""), dependencies.append);
639
+ backlog.splice(0, batchSize);
640
+ });
641
+ validationError = void 0;
642
+ };
643
+ return {
644
+ enqueue: (event) => {
645
+ try {
646
+ backlog.push(parse(CacheEventSchema, event));
647
+ } catch (error) {
648
+ validationError ??= error;
649
+ }
650
+ },
651
+ flush: (finalAttempt) => {
652
+ const write = previousWrite.then(persistBacklog);
653
+ previousWrite = write.catch(() => {});
654
+ return finalAttempt ? write : write.catch(() => {});
655
+ }
656
+ };
472
657
  }
473
- function resolveRuleLanguages(languages) {
474
- if (languages === void 0) return { kind: "off" };
475
- if (languages === "any") return { kind: "any" };
476
- if ("ids" in languages) return {
477
- ids: languages.ids,
478
- kind: "list",
479
- skipMissing: languages.skipMissing ?? false
658
+ function createNoopCacheStore(location) {
659
+ const transaction = {
660
+ checkpoint: async () => {},
661
+ commit: () => {},
662
+ discard: () => {},
663
+ lookup: () => void 0,
664
+ put: () => {}
480
665
  };
481
666
  return {
482
- ids: languages,
483
- kind: "list",
484
- skipMissing: false
667
+ beginOwner: () => transaction,
668
+ flush: async () => {},
669
+ location,
670
+ reconcile: async () => {}
485
671
  };
486
672
  }
487
- //#endregion
488
- //#region src/core/preparation.ts
489
- /**
490
- * The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
491
- * an index builder sweeping the workspace needs.
492
- *
493
- * It resolves each file's OWN config rather than a caller's, because the language a file resolves to
494
- * is a config decision and two config groups may register different plugins. It extracts on demand
495
- * and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
496
- * and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
497
- *
498
- * `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
499
- * before any rule runs, so by the first call it is present.
500
- */
501
- function createSourceExtractor(cwd, config, getSrc) {
502
- return async (filePath, options = {}) => {
503
- const path = resolve(cwd, filePath);
504
- const resolvedConfig = resolveConfigForFile(path, config, { cwd });
505
- if (resolvedConfig.ignored) return [];
506
- const effectiveConfig = resolvedConfig.config;
507
- const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
508
- const src = getSrc();
509
- const file = await src.readFile(path);
510
- return language.extract(file, {
511
- cwd,
512
- languageOptions: effectiveConfig.languageOptions,
513
- src
514
- });
673
+ function createReadOnlyCacheStore(location, body, cwd) {
674
+ return {
675
+ beginOwner: (owner) => ({
676
+ checkpoint: async () => {},
677
+ commit: () => {},
678
+ discard: () => {},
679
+ lookup: (slot, fingerprint) => {
680
+ const cached = body.entries[slotKey(owner, slot, cwd)];
681
+ return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
682
+ },
683
+ put: () => {}
684
+ }),
685
+ flush: async () => {},
686
+ location,
687
+ reconcile: async () => {}
515
688
  };
516
689
  }
517
- function prepareRun(options = {}) {
518
- const cwd$2 = options.cwd ?? cwd();
519
- const config = options.config ?? [];
520
- const files = [];
521
- const directories = [];
522
- const missingLanguages = /* @__PURE__ */ new Map();
523
- const unregisteredLanguages = /* @__PURE__ */ new Map();
524
- for (const filePath of options.files ?? []) {
525
- const path = resolve(cwd$2, filePath);
526
- const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
527
- if (resolvedConfig.ignored) continue;
528
- const effectiveConfig = resolvedConfig.config;
529
- const languageRegistry = createLanguageRegistry(effectiveConfig);
530
- const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
531
- const rules = prepareRules(effectiveConfig);
532
- recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
533
- if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
534
- files.push({
535
- agent: effectiveConfig.agent,
536
- configHash: stableHash({
537
- language: effectiveConfig.language,
538
- languageOptions: effectiveConfig.languageOptions,
539
- processor: effectiveConfig.processor,
540
- resolvedLanguage: language.name,
541
- settings: effectiveConfig.settings
542
- }),
543
- fileIndex: files.length,
544
- language,
545
- languageOptions: effectiveConfig.languageOptions,
546
- path,
547
- rules,
548
- settings: effectiveConfig.settings
549
- });
550
- }
551
- for (const directoryPath of options.directories ?? []) {
552
- const path = resolve(cwd$2, directoryPath);
553
- const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
554
- if (resolvedConfig.ignored) continue;
555
- const effectiveConfig = resolvedConfig.config;
556
- directories.push({
557
- agent: effectiveConfig.agent,
558
- configHash: stableHash({ settings: effectiveConfig.settings }),
559
- directoryIndex: directories.length,
560
- rules: prepareRules(effectiveConfig),
561
- settings: effectiveConfig.settings,
562
- target: {
563
- kind: "directory",
564
- path
565
- }
566
- });
690
+ async function defaultFileExists(path) {
691
+ try {
692
+ await access(path);
693
+ return true;
694
+ } catch (error) {
695
+ if (isMissingFileError(error)) return false;
696
+ throw error;
567
697
  }
698
+ }
699
+ function emptyLoadedLog(alintVersion) {
700
+ const metadata = {
701
+ alintVersion,
702
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
703
+ magic: CACHE_MAGIC,
704
+ schemaVersion: 2,
705
+ type: "metadata"
706
+ };
568
707
  return {
569
- directories,
570
- files,
571
- missingLanguages,
572
- project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
573
- unregisteredLanguages
708
+ body: createEmptyCacheBody(metadata.createdAt),
709
+ metadata
574
710
  };
575
711
  }
576
- function createLanguageRegistry(config) {
577
- const registry = createBuiltInLanguageRegistry();
578
- for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
579
- return registry;
712
+ async function ensureCompatibleMetadata(location, metadata, write) {
713
+ try {
714
+ if (parseMetadataLine(await readMetadataLine(location), metadata.alintVersion)) return;
715
+ } catch (error) {
716
+ if (!isMissingFileError(error)) throw error;
717
+ }
718
+ await write(location, `${JSON.stringify(metadata)}\n`);
580
719
  }
581
- function prepareProject(root, config) {
582
- const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
583
- if (resolvedConfig.ignored) return void 0;
584
- const effectiveConfig = resolvedConfig.config;
720
+ function fingerprintsEqual(left, right) {
721
+ return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
722
+ }
723
+ function getCacheLockOptions(retries) {
585
724
  return {
586
- agent: effectiveConfig.agent,
587
- configHash: stableHash({ settings: effectiveConfig.settings }),
588
- root,
589
- rules: prepareRules(effectiveConfig),
590
- settings: effectiveConfig.settings
725
+ realpath: false,
726
+ retries,
727
+ stale: LOCK_STALE_MS,
728
+ update: LOCK_UPDATE_MS
591
729
  };
592
730
  }
593
- function prepareRules(config) {
594
- return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
595
- enabledRule,
596
- ruleIndex
597
- }));
731
+ function hasJsonArrayProperties(input, ancestors) {
732
+ let arrayLength;
733
+ let indexCount = 0;
734
+ for (const key of Reflect.ownKeys(input)) {
735
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
736
+ if (!descriptor || !("value" in descriptor)) return false;
737
+ if (key === "length") {
738
+ if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
739
+ arrayLength = descriptor.value;
740
+ continue;
741
+ }
742
+ const index = typeof key === "string" ? Number(key) : NaN;
743
+ if (typeof key !== "string" || !Number.isInteger(index) || index < 0 || index >= 2 ** 32 - 1 || String(index) !== key || !descriptor.enumerable) return false;
744
+ if (!isJsonValueAt(descriptor.value, ancestors)) return false;
745
+ indexCount += 1;
746
+ }
747
+ return arrayLength !== void 0 && indexCount === arrayLength;
598
748
  }
599
- function recordMissingLanguages(into, rules, registry, path) {
600
- for (const { enabledRule } of rules) {
601
- const languages = resolveRuleLanguages(enabledRule.rule.languages);
602
- if (languages.kind !== "list" || languages.skipMissing) continue;
603
- for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
749
+ function hasJsonObjectProperties(input, ancestors) {
750
+ for (const key of Reflect.ownKeys(input)) {
751
+ if (typeof key !== "string") return false;
752
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
753
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return false;
754
+ if (!isJsonValueAt(descriptor.value, ancestors)) return false;
604
755
  }
756
+ return true;
605
757
  }
606
- //#endregion
607
- //#region package.json
608
- var version = "0.5.0";
609
- //#endregion
610
- //#region src/core/cache/types.ts
611
- const CACHE_MAGIC = "ALINT_CACHE";
612
- //#endregion
613
- //#region src/core/cache/store.ts
614
- const DEFAULT_CACHE_FILE_NAME = ".alintcache";
615
- const FiniteNumberSchema = pipe(number(), finite());
616
- const PositionSchema = object({
617
- column: FiniteNumberSchema,
618
- line: FiniteNumberSchema
619
- });
620
- const SourceLocationSchema = object({
621
- end: PositionSchema,
622
- start: PositionSchema
623
- });
624
- const DiagnosticLocationSchema = object({
625
- end: optional(PositionSchema),
626
- start: PositionSchema
627
- });
628
- const SourceRangeSchema = object({
629
- end: FiniteNumberSchema,
630
- start: FiniteNumberSchema
631
- });
632
- const DiagnosticSchema = object({
633
- cached: optional(boolean()),
634
- evidence: optional(custom(isJsonValue)),
635
- filePath: string(),
636
- loc: optional(DiagnosticLocationSchema),
637
- message: string(),
638
- model: optional(object({
639
- providerId: string(),
640
- requested: optional(string()),
641
- resolvedId: string()
642
- })),
643
- ruleId: string(),
644
- severity: union([literal("error"), literal("warn")])
645
- });
646
- const UsageSchema = object({
647
- filePath: optional(string()),
648
- inputTokens: optional(FiniteNumberSchema),
649
- metadata: optional(custom(isJsonValue)),
650
- modelId: string(),
651
- outputTokens: optional(FiniteNumberSchema),
652
- providerId: string(),
653
- ruleId: string(),
654
- totalTokens: optional(FiniteNumberSchema)
655
- });
656
- const CacheEntrySchema = object({
657
- diagnostics: array(DiagnosticSchema),
658
- fingerprint: object({
659
- configHash: string(),
660
- modelHash: string(),
661
- ruleHash: string(),
662
- targetHash: string()
663
- }),
664
- target: object({
665
- hash: string(),
666
- identity: string(),
667
- kind: string(),
668
- loc: optional(SourceLocationSchema),
669
- name: optional(string()),
670
- range: optional(SourceRangeSchema)
671
- }),
672
- usage: array(UsageSchema)
673
- });
674
- function objectRecord(value) {
675
- return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
758
+ function isJsonValue(input) {
759
+ return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
676
760
  }
677
- const CacheFileBodySchema = pipe(object({
678
- createdAt: string(),
679
- entries: objectRecord(CacheEntrySchema),
680
- owners: objectRecord(object({
681
- contentHash: optional(string()),
682
- kind: union([literal("file"), literal("project")]),
683
- path: string(),
684
- slots: array(string())
685
- })),
686
- updatedAt: string()
687
- }), check((body) => {
688
- const entryKeys = Object.keys(body.entries);
689
- const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
690
- const uniqueSlots = new Set(referencedSlots);
691
- return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
692
- }));
693
- async function createCacheStore(options) {
694
- const location = resolveCacheLocation(options.cwd, options.location);
695
- if (!options.enabled) return createNoopCacheStore(location);
696
- const alintVersion = options.alintVersion ?? version;
697
- const header = `${CACHE_MAGIC} 2 ${alintVersion}`;
698
- const body = await loadCacheBody(location, header, options.readOnly === true);
699
- if (options.readOnly) return createReadOnlyCacheStore(location, body, options.cwd);
700
- const fileExists = options.fileExists ?? defaultFileExists;
701
- const writeFile$1 = options.writeFile ?? writeFile;
702
- return {
703
- beginOwner: (owner) => beginOwner(body, owner, options.cwd),
704
- location,
705
- reconcile: async () => {
706
- await collectMissingFileOwners(body, options.cwd, fileExists);
707
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
708
- await persistCacheBody(location, header, body, writeFile$1);
761
+ function isJsonValueAt(input, ancestors) {
762
+ if (input === null || typeof input === "string" || typeof input === "boolean") return true;
763
+ if (typeof input === "number") return Number.isFinite(input);
764
+ if (typeof input !== "object") return false;
765
+ if (ancestors.has(input)) return false;
766
+ ancestors.add(input);
767
+ try {
768
+ if (Array.isArray(input)) {
769
+ if (Object.getPrototypeOf(input) !== Array.prototype) return false;
770
+ return hasJsonArrayProperties(input, ancestors);
709
771
  }
710
- };
772
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return false;
773
+ return hasJsonObjectProperties(input, ancestors);
774
+ } catch {
775
+ return false;
776
+ } finally {
777
+ ancestors.delete(input);
778
+ }
711
779
  }
712
- function createTargetIdentityResolver(targets) {
713
- const baseCounts = /* @__PURE__ */ new Map();
714
- const duplicateCandidateCounts = /* @__PURE__ */ new Map();
715
- for (const target of targets) {
716
- const base = createBaseTargetIdentity(target);
717
- baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
780
+ function isMissingFileError(error) {
781
+ return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
782
+ }
783
+ async function loadCacheLog(location, alintVersion) {
784
+ try {
785
+ const metadata = parseMetadataLine(await readMetadataLine(location), alintVersion);
786
+ if (!metadata) return emptyLoadedLog(alintVersion);
787
+ return replayLog(await readFile(location, "utf8"), metadata);
788
+ } catch (error) {
789
+ if (isMissingFileError(error)) return emptyLoadedLog(alintVersion);
790
+ throw error;
718
791
  }
719
- for (const target of targets) {
720
- const base = createBaseTargetIdentity(target);
721
- if ((baseCounts.get(base) ?? 0) <= 1) continue;
722
- const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
723
- duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
792
+ }
793
+ function ownerEntries(body, owner) {
794
+ const entries = /* @__PURE__ */ new Map();
795
+ for (const slot of owner?.slots ?? []) {
796
+ const cacheEntry = body.entries[slot];
797
+ if (cacheEntry) entries.set(slot, cacheEntry);
724
798
  }
725
- return (target, targetIndex) => {
726
- const base = createBaseTargetIdentity(target);
727
- if ((baseCounts.get(base) ?? 0) <= 1) return base;
728
- const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
729
- if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
730
- return `${candidate}:${targetIndex}`;
731
- };
799
+ return entries;
732
800
  }
733
- function normalizeCachePath(cwd, filePath) {
734
- const resolvedPath = isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath);
735
- return relative$1(cwd, resolvedPath).split(sep).join("/") || ".";
801
+ function ownerKey(owner, cwd) {
802
+ return stableHash({
803
+ kind: owner.kind,
804
+ path: normalizeCachePath(cwd, owner.path)
805
+ });
736
806
  }
737
- function normalizeRunnerCacheConfig(cache, cwd) {
738
- if (cache === false) return {
739
- enabled: false,
740
- location: resolveCacheLocation(cwd)
741
- };
742
- if (cache === true || cache === void 0) return {
743
- enabled: true,
744
- location: resolveCacheLocation(cwd)
745
- };
746
- return {
747
- enabled: cache.enabled ?? true,
748
- location: resolveCacheLocation(cwd, cache.location)
749
- };
807
+ function parseMetadataLine(line, expectedAlintVersion) {
808
+ if (!line) return void 0;
809
+ try {
810
+ const value = JSON.parse(line);
811
+ const metadata = parse(CacheMetadataSchema, value);
812
+ if (expectedAlintVersion !== void 0 && metadata.alintVersion !== expectedAlintVersion) return void 0;
813
+ return metadata;
814
+ } catch {
815
+ return;
816
+ }
750
817
  }
751
- function resolveCacheLocation(cwd, location) {
752
- if (!location) return join(cwd, DEFAULT_CACHE_FILE_NAME);
753
- const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
754
- if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
818
+ async function persistCompactedLog(location, loaded, write) {
819
+ const metadata = parse(CacheMetadataSchema, loaded.metadata);
820
+ const events = Object.entries(loaded.body.owners).map(([ownerKey, owner]) => ({
821
+ at: loaded.body.updatedAt,
822
+ entries: [...ownerEntries(loaded.body, owner)].map(([slotKey, entry]) => ({
823
+ entry,
824
+ slotKey
825
+ })),
826
+ mode: "replace",
827
+ owner,
828
+ ownerKey,
829
+ type: "replace-owner"
830
+ })).map((event) => parse(CacheEventSchema, event));
831
+ const contents = [`${JSON.stringify(metadata)}\n`, ...events.map((event) => `${JSON.stringify(event)}\n`)].join("");
832
+ const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
755
833
  try {
756
- if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
757
- } catch {}
758
- return resolved;
834
+ await write(tempPath, contents);
835
+ await rename(tempPath, location);
836
+ } catch (error) {
837
+ await rm(tempPath, { force: true }).catch(() => {});
838
+ throw error;
839
+ }
840
+ }
841
+ async function readMetadataLine(location) {
842
+ const handle = await open(location, "r");
843
+ try {
844
+ const buffer = Buffer.alloc(METADATA_READ_LIMIT);
845
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
846
+ const newlineIndex = buffer.subarray(0, bytesRead).indexOf(10);
847
+ if (newlineIndex < 0) return void 0;
848
+ const lineEnd = newlineIndex > 0 && buffer[newlineIndex - 1] === 13 ? newlineIndex - 1 : newlineIndex;
849
+ return buffer.toString("utf8", 0, lineEnd);
850
+ } finally {
851
+ await handle.close();
852
+ }
759
853
  }
760
- function beginOwner(body, owner, cwd) {
761
- const normalizedOwner = {
762
- kind: owner.kind,
763
- path: normalizeCachePath(cwd, owner.path)
764
- };
765
- const key = ownerKey(normalizedOwner, cwd);
766
- const previousOwner = body.owners[key];
767
- const nextEntries = /* @__PURE__ */ new Map();
854
+ function removeOwner(body, key) {
855
+ for (const slot of body.owners[key]?.slots ?? []) delete body.entries[slot];
856
+ delete body.owners[key];
857
+ }
858
+ function replayLog(text, metadata) {
859
+ const body = createEmptyCacheBody(metadata.createdAt);
860
+ const lines = text.split(/\r?\n/);
861
+ for (let index = 1; index < lines.length; index += 1) {
862
+ const line = lines[index];
863
+ if (!line) continue;
864
+ try {
865
+ const value = JSON.parse(line);
866
+ applyEvent(body, parse(CacheEventSchema, value));
867
+ } catch {}
868
+ }
768
869
  return {
769
- commit: (metadata = {}) => {
770
- const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
771
- for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
772
- const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
773
- for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
774
- for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
775
- body.owners[key] = {
776
- contentHash: metadata.contentHash,
777
- kind: normalizedOwner.kind,
778
- path: normalizedOwner.path,
779
- slots: [...committedEntries.keys()].sort()
780
- };
781
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
782
- },
783
- discard: (cacheSlot) => nextEntries.delete(slotKey(normalizedOwner, cacheSlot, cwd)),
784
- lookup: (cacheSlot, fingerprint) => {
785
- const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
786
- const cached = body.entries[entryKey];
787
- if (!cached || !fingerprintsEqual(cached.fingerprint, fingerprint)) return void 0;
788
- nextEntries.set(entryKey, cached);
789
- return cached;
790
- },
791
- put: (cacheSlot, cacheEntry) => nextEntries.set(slotKey(normalizedOwner, cacheSlot, cwd), cacheEntry)
870
+ body: parse(CacheFileBodySchema, body),
871
+ metadata
792
872
  };
793
873
  }
794
- async function collectMissingFileOwners(body, cwd, fileExists) {
795
- for (const [key, owner] of Object.entries(body.owners)) {
796
- if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
797
- for (const entryKey of owner.slots) delete body.entries[entryKey];
798
- delete body.owners[key];
874
+ function slotKey(owner, slot, cwd) {
875
+ return stableHash({
876
+ owner: ownerKey(owner, cwd),
877
+ ...slot
878
+ });
879
+ }
880
+ async function trimOnce(location, alintVersion, fallback, dependencies) {
881
+ await mkdir(dirname(location), { recursive: true });
882
+ try {
883
+ return await withCacheLock(location, dependencies.lock, getCacheLockOptions(0), async () => {
884
+ const latest = await loadCacheLog(location, alintVersion);
885
+ await persistCompactedLog(location, latest, dependencies.write);
886
+ return latest;
887
+ });
888
+ } catch (error) {
889
+ if (isNodeErrorCode(error, "ELOCKED")) return fallback;
890
+ throw error;
799
891
  }
800
892
  }
801
- function createBaseTargetIdentity(target) {
802
- if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
803
- if (target.kind === "file") return target.filePath ? `file:${target.filePath}` : "file";
804
- if (target.name) return `${target.kind}:${target.name}`;
805
- if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
806
- return target.kind;
893
+ async function withCacheLock(location, lock, options, operation) {
894
+ let compromised;
895
+ const release = await lock.acquire(location, {
896
+ ...options,
897
+ onCompromised: (error) => {
898
+ compromised = error;
899
+ }
900
+ });
901
+ let operationError;
902
+ try {
903
+ const result = await operation();
904
+ if (compromised) throw compromised;
905
+ return result;
906
+ } catch (error) {
907
+ operationError = error;
908
+ throw error;
909
+ } finally {
910
+ try {
911
+ await release();
912
+ } catch (error) {
913
+ if (operationError === void 0) throw error;
914
+ }
915
+ }
807
916
  }
808
- function createEmptyCacheBody() {
809
- const now = (/* @__PURE__ */ new Date()).toISOString();
917
+ //#endregion
918
+ //#region src/core/languages/js/index.ts
919
+ const javascriptLanguage = {
920
+ extensions: [
921
+ ".cjs",
922
+ ".js",
923
+ ".jsx",
924
+ ".mjs"
925
+ ],
926
+ extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
927
+ name: "javascript"
928
+ };
929
+ //#endregion
930
+ //#region src/core/languages/registry.ts
931
+ function createLanguageRegistry$1() {
810
932
  return {
811
- createdAt: now,
812
- entries: {},
813
- owners: {},
814
- updatedAt: now
933
+ byExtension: /* @__PURE__ */ new Map(),
934
+ languages: /* @__PURE__ */ new Map()
815
935
  };
816
936
  }
817
- function createNoopCacheStore(location) {
818
- const transaction = {
819
- commit: () => {},
820
- discard: () => {},
821
- lookup: () => void 0,
822
- put: () => {}
823
- };
937
+ function registerLanguage(registry, language) {
938
+ const existing = registry.languages.get(language.name);
939
+ if (existing && existing !== language) throw new Error(`Duplicate language "${language.name}".`);
940
+ for (const extension of language.extensions ?? []) {
941
+ const existingOwner = registry.byExtension.get(extension);
942
+ if (existingOwner && existingOwner !== language.name) throw new Error(`Duplicate language extension "${extension}".`);
943
+ }
944
+ registry.languages.set(language.name, language);
945
+ for (const extension of language.extensions ?? []) registry.byExtension.set(extension, language.name);
946
+ }
947
+ //#endregion
948
+ //#region src/core/languages/text.ts
949
+ const textLanguage = {
950
+ extensions: [],
951
+ extract: (file) => [{
952
+ file,
953
+ identity: "file",
954
+ kind: "file",
955
+ language: "plaintext",
956
+ origin: { physicalPath: file.path },
957
+ text: file.text
958
+ }],
959
+ name: "plaintext"
960
+ };
961
+ //#endregion
962
+ //#region src/core/languages/ts/index.ts
963
+ const typescriptLanguage = {
964
+ extensions: [
965
+ ".cts",
966
+ ".mts",
967
+ ".ts",
968
+ ".tsx"
969
+ ],
970
+ extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
971
+ name: "typescript"
972
+ };
973
+ //#endregion
974
+ //#region src/core/languages/resolve.ts
975
+ function resolveLanguage(file, registry, options) {
976
+ return resolveLanguageForPath(file.path, registry, options);
977
+ }
978
+ function resolveLanguageForPath(filePath, registry, options) {
979
+ const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
980
+ const language = registry.languages.get(languageName);
981
+ if (!language) {
982
+ const registered = [...registry.languages.keys()].sort().join(", ");
983
+ throw new Error(`Unknown language "${languageName}". Languages come from plugins, and these are registered: ${registered}.`);
984
+ }
985
+ return language;
986
+ }
987
+ //#endregion
988
+ //#region src/core/languages/index.ts
989
+ function createBuiltInLanguageRegistry() {
990
+ const registry = createLanguageRegistry$1();
991
+ registerLanguage(registry, textLanguage);
992
+ registerLanguage(registry, javascriptLanguage);
993
+ registerLanguage(registry, typescriptLanguage);
994
+ return registry;
995
+ }
996
+ //#endregion
997
+ //#region src/dsl/registry.ts
998
+ function buildRuleRegistry(config) {
999
+ const rules = /* @__PURE__ */ new Map();
1000
+ const localIds = /* @__PURE__ */ new Map();
1001
+ const enabledRules = [];
1002
+ for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
1003
+ const id = `${alias}/${localId}`;
1004
+ if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
1005
+ rules.set(id, rule);
1006
+ localIds.set(id, localId);
1007
+ }
1008
+ for (const [id, entry] of Object.entries(config.rules)) {
1009
+ const rule = rules.get(id);
1010
+ if (!rule) throw new Error(`Unknown rule "${id}".`);
1011
+ const normalizedEntry = normalizeRuleConfigEntry(entry);
1012
+ const severity = normalizedEntry.severity;
1013
+ if (severity === "off") continue;
1014
+ enabledRules.push({
1015
+ id,
1016
+ localId: localIds.get(id) ?? id,
1017
+ options: parseRuleOptions(id, rule.options, normalizedEntry.options),
1018
+ rule,
1019
+ severity
1020
+ });
1021
+ }
824
1022
  return {
825
- beginOwner: () => transaction,
826
- location,
827
- reconcile: async () => {}
1023
+ enabledRules,
1024
+ rules
828
1025
  };
829
1026
  }
830
- function createReadOnlyCacheStore(location, body, cwd) {
831
- const transaction = (owner) => ({
832
- commit: () => {},
833
- discard: () => {},
834
- lookup: (slot, fingerprint) => {
835
- const cached = body.entries[slotKey(owner, slot, cwd)];
836
- return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
837
- },
838
- put: () => {}
839
- });
1027
+ function formatRuleOptionIssues(index, issues) {
1028
+ return issues.map((issue) => {
1029
+ const path = getDotPath(issue);
1030
+ return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
1031
+ }).join("; ");
1032
+ }
1033
+ function isRuleConfigTuple(entry) {
1034
+ return Array.isArray(entry);
1035
+ }
1036
+ function normalizeRuleConfigEntry(entry) {
1037
+ if (isRuleConfigTuple(entry)) {
1038
+ const [severity = "warn", ...options] = entry;
1039
+ return {
1040
+ options,
1041
+ severity
1042
+ };
1043
+ }
840
1044
  return {
841
- beginOwner: transaction,
842
- location,
843
- reconcile: async () => {}
1045
+ options: [],
1046
+ severity: entry ?? "warn"
844
1047
  };
845
1048
  }
846
- async function defaultFileExists(path) {
847
- try {
848
- await access(path);
849
- return true;
850
- } catch (error) {
851
- if (isMissingFileError(error)) return false;
852
- throw error;
1049
+ function parseRuleOption(id, index, schema, value) {
1050
+ const result = safeParse(schema, value);
1051
+ if (result.success) return result.output;
1052
+ if (value === void 0) {
1053
+ const emptyObjectResult = safeParse(schema, {});
1054
+ if (emptyObjectResult.success) return emptyObjectResult.output;
853
1055
  }
1056
+ throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
854
1057
  }
855
- function fingerprintsEqual(left, right) {
856
- return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
857
- }
858
- function hasJsonArrayProperties(input, ancestors) {
859
- let arrayLength;
860
- let indexCount = 0;
861
- for (const key of Reflect.ownKeys(input)) {
862
- const descriptor = Object.getOwnPropertyDescriptor(input, key);
863
- if (!descriptor || !("value" in descriptor)) return false;
864
- if (key === "length") {
865
- if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
866
- arrayLength = descriptor.value;
867
- continue;
868
- }
869
- if (typeof key !== "string" || !isCanonicalArrayIndex(key) || !descriptor.enumerable) return false;
870
- if (!isJsonValueAt(descriptor.value, ancestors)) return false;
871
- indexCount += 1;
1058
+ function parseRuleOptions(id, schema, options) {
1059
+ if (!schema) {
1060
+ if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
1061
+ return [];
872
1062
  }
873
- return arrayLength !== void 0 && indexCount === arrayLength;
1063
+ if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
1064
+ return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
1065
+ }
1066
+ //#endregion
1067
+ //#region src/core/languages/diagnostics.ts
1068
+ /** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
1069
+ const MISSING_LANGUAGE_RULE = "alint/missing-language";
1070
+ /** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
1071
+ const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
1072
+ /**
1073
+ * One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
1074
+ * matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
1075
+ */
1076
+ function missingLanguageDiagnostics(missing) {
1077
+ return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
1078
+ const ruleIds = [...entry.ruleIds].sort();
1079
+ const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
1080
+ return {
1081
+ filePath: entry.filePath,
1082
+ message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
1083
+ ruleId: MISSING_LANGUAGE_RULE,
1084
+ severity: "error"
1085
+ };
1086
+ });
874
1087
  }
875
- function hasJsonObjectProperties(input, ancestors) {
876
- for (const key of Reflect.ownKeys(input)) {
877
- if (typeof key !== "string") return false;
878
- const descriptor = Object.getOwnPropertyDescriptor(input, key);
879
- if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return false;
880
- if (!isJsonValueAt(descriptor.value, ancestors)) return false;
1088
+ /** Records one rule that named a language the config never registered, grouped by language id. */
1089
+ function recordMissingLanguage(into, languageId, ruleId, path) {
1090
+ const existing = into.get(languageId);
1091
+ if (existing === void 0) {
1092
+ into.set(languageId, {
1093
+ filePath: path,
1094
+ ruleIds: /* @__PURE__ */ new Set([ruleId])
1095
+ });
1096
+ return;
881
1097
  }
882
- return true;
1098
+ existing.ruleIds.add(ruleId);
1099
+ if (path < existing.filePath) existing.filePath = path;
883
1100
  }
884
- function isCanonicalArrayIndex(key) {
885
- const index = Number(key);
886
- return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
1101
+ /**
1102
+ * Records one linted file no language claimed, grouped by extension so a missing pack is one
1103
+ * diagnostic, not one per file. `'off'` records nothing.
1104
+ */
1105
+ function recordUnregistered(into, path, severity) {
1106
+ if (severity === "off") return;
1107
+ const key = extname(path) || basename(path);
1108
+ const existing = into.get(key);
1109
+ into.set(key, {
1110
+ count: (existing?.count ?? 0) + 1,
1111
+ filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
1112
+ severity: existing?.severity === "error" ? "error" : severity
1113
+ });
887
1114
  }
888
- function isJsonValue(input) {
889
- return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
1115
+ /** One diagnostic per extension, sorted, for the run to prepend to its results. */
1116
+ function unregisteredLanguageDiagnostics(unregistered) {
1117
+ return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
1118
+ filePath: entry.filePath,
1119
+ message: `No language is registered for "${extension}": ${entry.count} ${entry.count === 1 ? "file was" : "files were"} handled as plain text. Register one through a plugin's "languages", or pin \`language: 'plaintext'\` to accept plain text.`,
1120
+ ruleId: UNREGISTERED_LANGUAGE_RULE,
1121
+ severity: entry.severity
1122
+ }));
890
1123
  }
891
- function isJsonValueAt(input, ancestors) {
892
- if (input === null || typeof input === "string" || typeof input === "boolean") return true;
893
- if (typeof input === "number") return Number.isFinite(input);
894
- if (typeof input !== "object") return false;
895
- if (ancestors.has(input)) return false;
896
- ancestors.add(input);
897
- try {
898
- if (Array.isArray(input)) {
899
- if (Object.getPrototypeOf(input) !== Array.prototype) return false;
900
- return hasJsonArrayProperties(input, ancestors);
901
- }
902
- if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return false;
903
- return hasJsonObjectProperties(input, ancestors);
904
- } catch {
905
- return false;
906
- } finally {
907
- ancestors.delete(input);
908
- }
1124
+ function unregisteredLanguageSeverity(linterOptions) {
1125
+ const value = linterOptions.reportUnregisteredLanguages;
1126
+ return value === "error" || value === "off" ? value : "warn";
909
1127
  }
910
- function isMissingFileError(error) {
911
- return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
1128
+ //#endregion
1129
+ //#region src/core/languages/rule-languages.ts
1130
+ /**
1131
+ * The language a file falls back to when no registered language claims its extension. Not a real
1132
+ * language, which is why `'any'` excludes it.
1133
+ */
1134
+ const PLAIN_TEXT = "plaintext";
1135
+ /**
1136
+ * Whether a rule reads this target.
1137
+ *
1138
+ * A rule with language off still reads file targets, because reading raw text, or asking a model about a
1139
+ * whole file, needs no language. However, it never reads the function and class targets extraction produces.
1140
+ * `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
1141
+ *
1142
+ * `'any'` means any real language, so a file that fell back to plain text because nothing claimed
1143
+ * its extension is withheld: the rule asked for structure and there is none to give. The run says so
1144
+ * once per extension through `alint/unregistered-language` rather than letting each rule report an
1145
+ * empty file.
1146
+ */
1147
+ function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
1148
+ if (languages.kind === "off") return targetKind === "file";
1149
+ if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
1150
+ return languages.ids.includes(targetLanguage);
912
1151
  }
913
- function isNodeErrorCode(error, code) {
914
- return error instanceof Error && "code" in error && error.code === code;
1152
+ function resolveRuleLanguages(languages) {
1153
+ if (languages === void 0) return { kind: "off" };
1154
+ if (languages === "any") return { kind: "any" };
1155
+ if ("ids" in languages) return {
1156
+ ids: languages.ids,
1157
+ kind: "list",
1158
+ skipMissing: languages.skipMissing ?? false
1159
+ };
1160
+ return {
1161
+ ids: languages,
1162
+ kind: "list",
1163
+ skipMissing: false
1164
+ };
915
1165
  }
916
- async function loadCacheBody(location, expectedHeader, readOnly) {
917
- let handle;
918
- try {
919
- handle = await open(location, "r");
920
- const header = await readHeader(handle);
921
- await handle.close();
922
- handle = void 0;
923
- if (header !== expectedHeader) {
924
- if (!readOnly) await rm(location, { force: true });
925
- return createEmptyCacheBody();
926
- }
927
- const text = await readFile(location, "utf8");
928
- return parseCacheBody(text.slice(text.indexOf("\n") + 1));
929
- } catch (error) {
930
- await handle?.close().catch(() => {});
931
- if (isMissingFileError(error)) return createEmptyCacheBody();
932
- if (!readOnly) await rm(location, { force: true }).catch(() => {});
933
- return createEmptyCacheBody();
934
- }
1166
+ //#endregion
1167
+ //#region src/core/preparation.ts
1168
+ /**
1169
+ * The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
1170
+ * an index builder sweeping the workspace needs.
1171
+ *
1172
+ * It resolves each file's OWN config rather than a caller's, because the language a file resolves to
1173
+ * is a config decision and two config groups may register different plugins. It extracts on demand
1174
+ * and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
1175
+ * and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
1176
+ *
1177
+ * `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
1178
+ * before any rule runs, so by the first call it is present.
1179
+ */
1180
+ function createSourceExtractor(cwd, config, getSrc) {
1181
+ return async (filePath, options = {}) => {
1182
+ const path = resolve(cwd, filePath);
1183
+ const resolvedConfig = resolveConfigForFile(path, config, { cwd });
1184
+ if (resolvedConfig.ignored) return [];
1185
+ const effectiveConfig = resolvedConfig.config;
1186
+ const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
1187
+ const src = getSrc();
1188
+ const file = await src.readFile(path);
1189
+ return language.extract(file, {
1190
+ cwd,
1191
+ languageOptions: effectiveConfig.languageOptions,
1192
+ src
1193
+ });
1194
+ };
935
1195
  }
936
- function ownerEntries(body, owner) {
937
- const entries = /* @__PURE__ */ new Map();
938
- for (const slot of owner?.slots ?? []) {
939
- const cacheEntry = body.entries[slot];
940
- if (cacheEntry) entries.set(slot, cacheEntry);
1196
+ function prepareRun(options = {}) {
1197
+ const cwd$2 = options.cwd ?? cwd();
1198
+ const config = options.config ?? [];
1199
+ const files = [];
1200
+ const directories = [];
1201
+ const missingLanguages = /* @__PURE__ */ new Map();
1202
+ const unregisteredLanguages = /* @__PURE__ */ new Map();
1203
+ for (const filePath of options.files ?? []) {
1204
+ const path = resolve(cwd$2, filePath);
1205
+ const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
1206
+ if (resolvedConfig.ignored) continue;
1207
+ const effectiveConfig = resolvedConfig.config;
1208
+ const languageRegistry = createLanguageRegistry(effectiveConfig);
1209
+ const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
1210
+ const rules = prepareRules(effectiveConfig);
1211
+ recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
1212
+ if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
1213
+ files.push({
1214
+ agent: effectiveConfig.agent,
1215
+ configHash: stableHash({
1216
+ language: effectiveConfig.language,
1217
+ languageOptions: effectiveConfig.languageOptions,
1218
+ processor: effectiveConfig.processor,
1219
+ resolvedLanguage: language.name,
1220
+ settings: effectiveConfig.settings
1221
+ }),
1222
+ fileIndex: files.length,
1223
+ language,
1224
+ languageOptions: effectiveConfig.languageOptions,
1225
+ path,
1226
+ rules,
1227
+ settings: effectiveConfig.settings
1228
+ });
941
1229
  }
942
- return entries;
943
- }
944
- function ownerKey(owner, cwd) {
945
- return stableHash({
946
- kind: owner.kind,
947
- path: normalizeCachePath(cwd, owner.path)
948
- });
1230
+ for (const directoryPath of options.directories ?? []) {
1231
+ const path = resolve(cwd$2, directoryPath);
1232
+ const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
1233
+ if (resolvedConfig.ignored) continue;
1234
+ const effectiveConfig = resolvedConfig.config;
1235
+ directories.push({
1236
+ agent: effectiveConfig.agent,
1237
+ configHash: stableHash({ settings: effectiveConfig.settings }),
1238
+ directoryIndex: directories.length,
1239
+ rules: prepareRules(effectiveConfig),
1240
+ settings: effectiveConfig.settings,
1241
+ target: {
1242
+ kind: "directory",
1243
+ path
1244
+ }
1245
+ });
1246
+ }
1247
+ return {
1248
+ directories,
1249
+ files,
1250
+ missingLanguages,
1251
+ project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
1252
+ unregisteredLanguages
1253
+ };
949
1254
  }
950
- function parseCacheBody(text) {
951
- const value = JSON.parse(text);
952
- return parse(CacheFileBodySchema, value);
1255
+ function createLanguageRegistry(config) {
1256
+ const registry = createBuiltInLanguageRegistry();
1257
+ for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
1258
+ return registry;
953
1259
  }
954
- async function persistCacheBody(location, header, body, writeFile) {
955
- const validatedBody = parse(CacheFileBodySchema, body);
956
- await mkdir(dirname(location), { recursive: true });
957
- const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
958
- try {
959
- await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
960
- await rename(tempPath, location);
961
- } catch (error) {
962
- await rm(tempPath, { force: true }).catch(() => {});
963
- throw error;
964
- }
1260
+ function prepareProject(root, config) {
1261
+ const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
1262
+ if (resolvedConfig.ignored) return void 0;
1263
+ const effectiveConfig = resolvedConfig.config;
1264
+ return {
1265
+ agent: effectiveConfig.agent,
1266
+ configHash: stableHash({ settings: effectiveConfig.settings }),
1267
+ root,
1268
+ rules: prepareRules(effectiveConfig),
1269
+ settings: effectiveConfig.settings
1270
+ };
965
1271
  }
966
- async function readHeader(handle) {
967
- const buffer = Buffer.alloc(256);
968
- const { bytesRead } = await handle.read(buffer, 0, 256, 0);
969
- const newline = buffer.subarray(0, bytesRead).indexOf(10);
970
- return newline < 0 ? void 0 : buffer.subarray(0, newline).toString("utf8");
1272
+ function prepareRules(config) {
1273
+ return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
1274
+ enabledRule,
1275
+ ruleIndex
1276
+ }));
971
1277
  }
972
- function slotKey(owner, slot, cwd) {
973
- return stableHash({
974
- owner: ownerKey(owner, cwd),
975
- ...slot
976
- });
1278
+ function recordMissingLanguages(into, rules, registry, path) {
1279
+ for (const { enabledRule } of rules) {
1280
+ const languages = resolveRuleLanguages(enabledRule.rule.languages);
1281
+ if (languages.kind !== "list" || languages.skipMissing) continue;
1282
+ for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
1283
+ }
977
1284
  }
978
1285
  //#endregion
979
1286
  //#region src/core/execution/records.ts
@@ -1112,9 +1419,10 @@ async function executeRuleJob(job, options) {
1112
1419
  failure: failure(handlerCause, job, handlerCause instanceof SourceChangedError ? "source-changed" : "handler"),
1113
1420
  state: "failed"
1114
1421
  });
1115
- if (slot && fingerprint) try {
1422
+ if (slot && fingerprint) {
1116
1423
  job.target.cacheOwner?.put(slot, createCacheEntry(job, fingerprint, bucket));
1117
- } catch {}
1424
+ await job.target.cacheOwner?.checkpoint();
1425
+ }
1118
1426
  return finish(job, bucket, {
1119
1427
  cache: "miss",
1120
1428
  state: "completed"
@@ -2097,7 +2405,7 @@ async function planSource(input, options) {
2097
2405
  cacheOwner = options.cacheStore.beginOwner({
2098
2406
  kind: "file",
2099
2407
  path: file.path
2100
- });
2408
+ }, { contentHash });
2101
2409
  const baseFile = {
2102
2410
  contentHash,
2103
2411
  language: file.language,
@@ -2112,10 +2420,7 @@ async function planSource(input, options) {
2112
2420
  };
2113
2421
  }
2114
2422
  if (options.signal?.aborted) {
2115
- cacheOwner.commit({
2116
- contentHash,
2117
- mode: "merge"
2118
- });
2423
+ cacheOwner.commit({ mode: "merge" });
2119
2424
  return {
2120
2425
  outcomes: Promise.resolve([]),
2121
2426
  project
@@ -2124,11 +2429,9 @@ async function planSource(input, options) {
2124
2429
  const batch = options.scheduler.schedule(jobs);
2125
2430
  reportFilePlanningComplete(input, batch.jobsAdded, options);
2126
2431
  return {
2127
- outcomes: batch.outcomes.then((settled) => {
2128
- cacheOwner.commit(options.signal?.aborted ? {
2129
- contentHash,
2130
- mode: "merge"
2131
- } : { contentHash });
2432
+ outcomes: batch.outcomes.then(async (settled) => {
2433
+ cacheOwner.commit(options.signal?.aborted ? { mode: "merge" } : void 0);
2434
+ await options.cacheStore.flush();
2132
2435
  return settled;
2133
2436
  }),
2134
2437
  project
@@ -2405,6 +2708,14 @@ var AlintAbortError = class extends AlintRunCancelledError {
2405
2708
  this.name = "AlintAbortError";
2406
2709
  }
2407
2710
  };
2711
+ var AlintCachePersistenceError = class extends Error {
2712
+ result;
2713
+ constructor(result, cause) {
2714
+ super("Failed to persist the alint cache.", { cause });
2715
+ this.name = "AlintCachePersistenceError";
2716
+ this.result = result;
2717
+ }
2718
+ };
2408
2719
  var AlintRunError = class extends Error {
2409
2720
  failures;
2410
2721
  result;
@@ -2482,6 +2793,7 @@ async function runAlint(options = {}) {
2482
2793
  const outcomes = [];
2483
2794
  const fileFailures = [];
2484
2795
  let infrastructureError;
2796
+ let cachePersistenceError;
2485
2797
  let infrastructureFailed = false;
2486
2798
  let admissionFailed = false;
2487
2799
  try {
@@ -2545,7 +2857,9 @@ async function runAlint(options = {}) {
2545
2857
  }
2546
2858
  if (!admissionFailed && !options.cacheOnly) try {
2547
2859
  await cacheStore.reconcile();
2548
- } catch {}
2860
+ } catch (error) {
2861
+ cachePersistenceError = error;
2862
+ }
2549
2863
  }
2550
2864
  runProgress.finalize();
2551
2865
  if (admissionFailed) throw infrastructureError;
@@ -2574,6 +2888,7 @@ async function runAlint(options = {}) {
2574
2888
  usage: snapshotRunUsage(result.usage)
2575
2889
  });
2576
2890
  if (infrastructureFailed) throw infrastructureError;
2891
+ if (cachePersistenceError !== void 0) throw new AlintCachePersistenceError(result, cachePersistenceError);
2577
2892
  if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
2578
2893
  if (failures.length > 0) throw new AlintRunError(fileFailures.length > 0 ? `${failures.length} alint execution${failures.length === 1 ? "" : "s"} failed.` : `${failures.length} rule execution${failures.length === 1 ? "" : "s"} failed.`, result, {
2579
2894
  cause: new AggregateError(failures.map((failure) => new Error(failure.message)), "Alint execution failures."),
@@ -2805,6 +3120,7 @@ async function measureModel({ model, prompt, signal }) {
2805
3120
  }];
2806
3121
  const startedAt = performance.now();
2807
3122
  let firstOutputAt;
3123
+ let outputDeltaCount = 0;
2808
3124
  const result = streamText({
2809
3125
  ...model.params,
2810
3126
  abortSignal: signal,
@@ -2824,7 +3140,10 @@ async function measureModel({ model, prompt, signal }) {
2824
3140
  ]);
2825
3141
  let settled;
2826
3142
  try {
2827
- for await (const event of result.eventStream) if (firstOutputAt === void 0 && (event.type === "reasoning.delta" || event.type === "text.delta")) firstOutputAt = performance.now();
3143
+ for await (const event of result.eventStream) if ((event.type === "reasoning.delta" || event.type === "text.delta") && event.delta.length > 0) {
3144
+ outputDeltaCount += 1;
3145
+ firstOutputAt ??= performance.now();
3146
+ }
2828
3147
  } finally {
2829
3148
  settled = await completion;
2830
3149
  }
@@ -2833,7 +3152,7 @@ async function measureModel({ model, prompt, signal }) {
2833
3152
  if (usageResult.status === "rejected") throw usageResult.reason;
2834
3153
  return {
2835
3154
  durationMs: endedAt - startedAt,
2836
- firstOutputMs: firstOutputAt === void 0 ? void 0 : firstOutputAt - startedAt,
3155
+ firstOutputMs: firstOutputAt === void 0 || outputDeltaCount < 2 ? void 0 : firstOutputAt - startedAt,
2837
3156
  outputTokens: usageResult.value?.outputTokens
2838
3157
  };
2839
3158
  }
@@ -2930,4 +3249,4 @@ function sampleCorpus(seed) {
2930
3249
  ].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
2931
3250
  }
2932
3251
  //#endregion
2933
- export { AlintAbortError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange, targetIdentity, withLanguage, withStableIdentities };
3252
+ export { AlintAbortError, AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defaultStopGateConfig, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, normalizeRunnerCacheConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, resolveStopGateConfig, runAlint, sliceLines, sliceRange, stableHash, targetIdentity, withLanguage, withStableIdentities };