@alint-js/core 0.0.38 → 0.1.1

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
@@ -3,14 +3,15 @@ import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-
3
3
  import { a as sliceRange, i as sliceLines, n as createSourceFile, r as createSourceRuntime, t as extractJsSourceTargets } from "./extract-CIWssxNK.mjs";
4
4
  import { minimatch } from "minimatch";
5
5
  import { isAbsolute, relative, resolve } from "pathe";
6
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
7
- import { dirname, extname, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
8
- import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { access, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
7
+ import { basename, dirname, extname, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
9
8
  import process, { cwd } from "node:process";
10
- import { array, check, description, getDotPath, is, literal, number, object, optional, pipe, record, safeParse, string, union, unknown } from "valibot";
9
+ import { Buffer } from "node:buffer";
11
10
  import { createHash, randomUUID } from "node:crypto";
12
11
  import { statSync } from "node:fs";
12
+ import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
13
13
  import { errorMessageFrom } from "@moeru/std/error";
14
+ import { AsyncLocalStorage } from "node:async_hooks";
14
15
  //#region src/config/config-array.ts
15
16
  const effectiveBasePathSymbol = Symbol("effectiveBasePath");
16
17
  const inheritedMatcherScopesSymbol = Symbol("inheritedMatcherScopes");
@@ -253,7 +254,7 @@ function withLanguage$1(file, language) {
253
254
  }
254
255
  //#endregion
255
256
  //#region src/core/languages/registry.ts
256
- function createLanguageRegistry() {
257
+ function createLanguageRegistry$1() {
257
258
  return {
258
259
  byExtension: /* @__PURE__ */ new Map(),
259
260
  languages: /* @__PURE__ */ new Map()
@@ -305,7 +306,10 @@ function withLanguage(file, language) {
305
306
  //#endregion
306
307
  //#region src/core/languages/resolve.ts
307
308
  function resolveLanguage(file, registry, options) {
308
- const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname(file.path)) ?? "text/plain";
309
+ return resolveLanguageForPath(file.path, registry, options);
310
+ }
311
+ function resolveLanguageForPath(filePath, registry, options) {
312
+ const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname(filePath)) ?? "text/plain";
309
313
  const language = registry.languages.get(languageName);
310
314
  if (!language) throw new Error(`Unknown language "${languageName}".`);
311
315
  return language;
@@ -313,232 +317,172 @@ function resolveLanguage(file, registry, options) {
313
317
  //#endregion
314
318
  //#region src/core/languages/index.ts
315
319
  function createBuiltInLanguageRegistry() {
316
- const registry = createLanguageRegistry();
320
+ const registry = createLanguageRegistry$1();
317
321
  registerLanguage(registry, textLanguage);
318
322
  registerLanguage(registry, javascriptLanguage);
319
323
  registerLanguage(registry, typescriptLanguage);
320
324
  return registry;
321
325
  }
322
326
  //#endregion
323
- //#region src/dsl/registry.ts
324
- function buildRuleRegistry(config) {
325
- const rules = /* @__PURE__ */ new Map();
326
- const localIds = /* @__PURE__ */ new Map();
327
- const enabledRules = [];
328
- for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
329
- const id = `${alias}/${localId}`;
330
- if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
331
- rules.set(id, rule);
332
- localIds.set(id, localId);
333
- }
334
- for (const [id, entry] of Object.entries(config.rules)) {
335
- const rule = rules.get(id);
336
- if (!rule) throw new Error(`Unknown rule "${id}".`);
337
- const normalizedEntry = normalizeRuleConfigEntry(entry);
338
- const severity = normalizedEntry.severity;
339
- if (severity === "off") continue;
340
- enabledRules.push({
341
- id,
342
- localId: localIds.get(id) ?? id,
343
- options: parseRuleOptions(id, rule.options, normalizedEntry.options),
344
- rule,
345
- severity
346
- });
347
- }
348
- return {
349
- enabledRules,
350
- rules
327
+ //#region package.json
328
+ var version = "0.1.1";
329
+ //#endregion
330
+ //#region src/core/hash.ts
331
+ function createStableHasher() {
332
+ const hash = createHash("sha256");
333
+ const api = {
334
+ digest: () => hash.digest("hex"),
335
+ update: (value) => {
336
+ const serialized = stableStringify(value);
337
+ hash.update(`${serialized.length}:`);
338
+ hash.update(serialized);
339
+ return api;
340
+ }
351
341
  };
342
+ return api;
352
343
  }
353
- function formatRuleOptionIssues(index, issues) {
354
- return issues.map((issue) => {
355
- const path = getDotPath(issue);
356
- return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
357
- }).join("; ");
358
- }
359
- function isRuleConfigTuple(entry) {
360
- return Array.isArray(entry);
361
- }
362
- function normalizeRuleConfigEntry(entry) {
363
- if (isRuleConfigTuple(entry)) {
364
- const [severity = "warn", ...options] = entry;
365
- return {
366
- options,
367
- severity
368
- };
369
- }
370
- return {
371
- options: [],
372
- severity: entry ?? "warn"
373
- };
344
+ function hashText(text) {
345
+ return createHash("sha256").update(text).digest("hex");
374
346
  }
375
- function parseRuleOption(id, index, schema, value) {
376
- const result = safeParse(schema, value);
377
- if (result.success) return result.output;
378
- if (value === void 0) {
379
- const emptyObjectResult = safeParse(schema, {});
380
- if (emptyObjectResult.success) return emptyObjectResult.output;
381
- }
382
- throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
347
+ function stableHash(value) {
348
+ return hashText(stableStringify(value));
383
349
  }
384
- function parseRuleOptions(id, schema, options) {
385
- if (!schema) {
386
- if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
387
- return [];
350
+ function stableStringify(value) {
351
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
352
+ if (typeof value === "object" && value !== null) {
353
+ const record = value;
354
+ return `{${Object.keys(record).sort().flatMap((key) => {
355
+ const property = record[key];
356
+ return property === void 0 ? [] : [`${JSON.stringify(key)}:${stableStringify(property)}`];
357
+ }).join(",")}}`;
388
358
  }
389
- if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
390
- return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
359
+ return JSON.stringify(value);
391
360
  }
392
361
  //#endregion
393
- //#region src/models/resolve.ts
394
- function resolveModel(registry, options = {}) {
395
- const candidates = flattenModels(registry);
396
- const ruleId = options.ruleId ?? "<unknown>";
397
- if (options.request !== void 0) {
398
- const request = options.request;
399
- const candidate = candidates.find((candidate) => matchesRequest(candidate, request));
400
- if (candidate === void 0) throw new Error(`Unknown model "${request}".`);
401
- if (!satisfiesHardRequirements(candidate.model, options.requirement)) throw new Error(`Model "${request}" does not satisfy requirement for rule "${ruleId}".`);
402
- return toResolvedModel(candidate, options.requirement);
403
- }
404
- const candidate = preferSize(candidates.filter(({ model }) => satisfiesHardRequirements(model, options.requirement)), options.requirement);
405
- if (candidate === void 0) throw new Error(`No model satisfies requirement for rule "${ruleId}".`);
406
- return toResolvedModel(candidate, options.requirement);
407
- }
408
- function flattenModels(registry) {
409
- return registry.providers.flatMap((provider) => provider.models.map((model) => ({
410
- model,
411
- provider
412
- })));
413
- }
414
- function matchesRequest(candidate, request) {
415
- return [
416
- candidate.model.id,
417
- candidate.model.name,
418
- ...candidate.model.aliases ?? []
419
- ].filter((name) => name !== void 0).some((name) => name === request || `${candidate.provider.id}/${name}` === request);
420
- }
421
- function preferSize(candidates, requirement) {
422
- if (requirement?.size === void 0) return candidates[0];
423
- return candidates.find(({ model }) => model.size === requirement.size) ?? candidates[0];
424
- }
425
- function satisfiesHardRequirements(model, requirement) {
426
- if (requirement === void 0) return true;
427
- if (requirement.capabilities !== void 0 && !requirement.capabilities.every((capability) => (model.capabilities ?? []).includes(capability))) return false;
428
- if (requirement.minContextWindow !== void 0 && (model.contextWindow === void 0 || model.contextWindow < requirement.minContextWindow)) return false;
429
- return true;
430
- }
431
- function toResolvedModel(candidate, requirement) {
432
- const { model, provider } = candidate;
433
- return {
434
- aliases: [...model.aliases ?? []],
435
- capabilities: [...model.capabilities ?? []],
436
- contextWindow: model.contextWindow,
437
- id: model.id,
438
- name: model.name ?? model.id,
439
- params: {
440
- ...model.defaultParams ?? {},
441
- ...requirement?.params ?? {}
442
- },
443
- provider: {
444
- endpoint: provider.endpoint,
445
- headers: { ...provider.headers ?? {} },
446
- id: provider.id,
447
- type: provider.type
448
- },
449
- size: model.size
450
- };
451
- }
362
+ //#region src/core/cache/types.ts
363
+ const CACHE_MAGIC = "ALINT_CACHE";
452
364
  //#endregion
453
- //#region src/core/cache.ts
454
- const CACHE_SCHEMA_VERSION = 1;
365
+ //#region src/core/cache/store.ts
455
366
  const DEFAULT_CACHE_FILE_NAME = ".alintcache";
456
- const CacheFileSchema = pipe(object({
457
- createdAt: pipe(string(), description("Cache file creation timestamp.")),
458
- entries: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
459
- diagnostics: pipe(array(object({
460
- filePath: pipe(string(), description("Diagnostic file path.")),
461
- message: pipe(string(), description("Diagnostic message.")),
462
- ruleId: pipe(string(), description("Diagnostic rule id.")),
463
- severity: pipe(union([literal("error"), literal("warn")]), description("Diagnostic severity."))
464
- })), description("Diagnostics produced for the cached target.")),
465
- filePath: pipe(string(), description("Cache entry file path.")),
466
- fingerprint: pipe(object({
467
- alintVersion: pipe(string(), description("Alint version used to create the cache entry.")),
468
- configHash: pipe(string(), description("Runner config hash used to create the cache entry.")),
469
- modelHash: pipe(string(), description("Model configuration hash used to create the cache entry.")),
470
- ruleHash: pipe(string(), description("Rule configuration hash used to create the cache entry."))
471
- }), description("Cache entry fingerprint.")),
472
- target: pipe(object({
473
- hash: pipe(string(), description("Cached target hash.")),
474
- identity: pipe(string(), description("Stable cached target identity.")),
475
- kind: pipe(string(), description("Cached target kind."))
476
- }), description("Cached target metadata.")),
477
- usage: pipe(array(object({
478
- inputTokens: pipe(optional(number()), description("Optional input token count.")),
479
- modelId: pipe(string(), description("Usage model id.")),
480
- outputTokens: pipe(optional(number()), description("Optional output token count.")),
481
- providerId: pipe(string(), description("Usage provider id.")),
482
- ruleId: pipe(string(), description("Usage rule id.")),
483
- totalTokens: pipe(optional(number()), description("Optional total token count."))
484
- })), description("Inference usage records for the cache entry."))
485
- })), description("Cache entries keyed by cache key.")),
486
- files: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
487
- contentHash: pipe(string(), description("Cached file content hash.")),
488
- entries: pipe(array(string()), description("Cache entry keys associated with the file.")),
489
- path: pipe(string(), description("Normalized cached file path."))
490
- })), description("Cached files keyed by normalized file path.")),
491
- schemaVersion: pipe(literal(CACHE_SCHEMA_VERSION), description("Cache schema version.")),
492
- updatedAt: pipe(string(), description("Cache file update timestamp."))
493
- }), description("Alint cache file."));
494
- function createCacheKey(input) {
495
- return stableHash(input);
496
- }
367
+ const FiniteNumberSchema = pipe(number(), finite());
368
+ const PositionSchema = object({
369
+ column: FiniteNumberSchema,
370
+ line: FiniteNumberSchema
371
+ });
372
+ const SourceLocationSchema = object({
373
+ end: PositionSchema,
374
+ start: PositionSchema
375
+ });
376
+ const DiagnosticLocationSchema = object({
377
+ end: optional(PositionSchema),
378
+ start: PositionSchema
379
+ });
380
+ const SourceRangeSchema = object({
381
+ end: FiniteNumberSchema,
382
+ start: FiniteNumberSchema
383
+ });
384
+ const DiagnosticSchema = object({
385
+ cached: optional(boolean()),
386
+ evidence: optional(custom(isJsonValue)),
387
+ filePath: string(),
388
+ loc: optional(DiagnosticLocationSchema),
389
+ message: string(),
390
+ model: optional(object({
391
+ providerId: string(),
392
+ requested: optional(string()),
393
+ resolvedId: string()
394
+ })),
395
+ ruleId: string(),
396
+ severity: union([literal("error"), literal("warn")])
397
+ });
398
+ const UsageSchema = object({
399
+ filePath: optional(string()),
400
+ inputTokens: optional(FiniteNumberSchema),
401
+ metadata: optional(custom(isJsonValue)),
402
+ modelId: string(),
403
+ outputTokens: optional(FiniteNumberSchema),
404
+ providerId: string(),
405
+ ruleId: string(),
406
+ totalTokens: optional(FiniteNumberSchema)
407
+ });
408
+ const CacheEntrySchema = object({
409
+ diagnostics: array(DiagnosticSchema),
410
+ fingerprint: object({
411
+ configHash: string(),
412
+ modelHash: string(),
413
+ ruleHash: string(),
414
+ targetHash: string()
415
+ }),
416
+ target: object({
417
+ hash: string(),
418
+ identity: string(),
419
+ kind: string(),
420
+ loc: optional(SourceLocationSchema),
421
+ name: optional(string()),
422
+ range: optional(SourceRangeSchema)
423
+ }),
424
+ usage: array(UsageSchema)
425
+ });
426
+ function objectRecord(value) {
427
+ return pipe(unknown(), check((input) => typeof input === "object" && input !== null && !Array.isArray(input)), record(string(), value));
428
+ }
429
+ const CacheFileBodySchema = pipe(object({
430
+ createdAt: string(),
431
+ entries: objectRecord(CacheEntrySchema),
432
+ owners: objectRecord(object({
433
+ contentHash: optional(string()),
434
+ kind: union([literal("file"), literal("project")]),
435
+ path: string(),
436
+ slots: array(string())
437
+ })),
438
+ updatedAt: string()
439
+ }), check((body) => {
440
+ const entryKeys = Object.keys(body.entries);
441
+ const referencedSlots = Object.values(body.owners).flatMap((owner) => owner.slots);
442
+ const uniqueSlots = new Set(referencedSlots);
443
+ return uniqueSlots.size === referencedSlots.length && uniqueSlots.size === entryKeys.length && entryKeys.every((key) => uniqueSlots.has(key));
444
+ }));
497
445
  async function createCacheStore(options) {
498
446
  const location = resolveCacheLocation(options.cwd, options.location);
499
447
  if (!options.enabled) return createNoopCacheStore(location);
500
- const cacheFile = await readCacheFile(location);
448
+ const header = `${CACHE_MAGIC} 2 ${options.alintVersion ?? version}`;
449
+ const body = await loadCacheBody(location, header, options.readOnly === true);
450
+ if (options.readOnly) return createReadOnlyCacheStore(location, body, options.cwd);
451
+ const fileExists = options.fileExists ?? defaultFileExists;
452
+ const writeFile$1 = options.writeFile ?? writeFile;
501
453
  return {
502
- get: (key) => cacheFile.entries[key],
454
+ beginOwner: (owner) => beginOwner(body, owner, options.cwd),
503
455
  location,
504
- markFile: (filePath, contentHash, entries) => {
505
- const path = normalizeCachePath(options.cwd, filePath);
506
- cacheFile.files[path] = {
507
- contentHash,
508
- entries,
509
- path
510
- };
511
- },
512
456
  reconcile: async () => {
513
- await mkdir(dirname(location), { recursive: true });
514
- cacheFile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
515
- const tempPath = join(dirname(location), `.${DEFAULT_CACHE_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`);
516
- await writeFile(tempPath, `${JSON.stringify(cacheFile, null, 2)}\n`);
517
- await rename(tempPath, location);
518
- },
519
- set: (key, entry) => {
520
- cacheFile.entries[key] = entry;
457
+ await collectMissingFileOwners(body, options.cwd, fileExists);
458
+ body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
459
+ await persistCacheBody(location, header, body, writeFile$1);
521
460
  }
522
461
  };
523
462
  }
524
463
  function createTargetIdentityResolver(targets) {
525
464
  const baseCounts = /* @__PURE__ */ new Map();
465
+ const duplicateCandidateCounts = /* @__PURE__ */ new Map();
526
466
  for (const target of targets) {
527
467
  const base = createBaseTargetIdentity(target);
528
468
  baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
529
469
  }
530
- return (target) => {
470
+ for (const target of targets) {
471
+ const base = createBaseTargetIdentity(target);
472
+ if ((baseCounts.get(base) ?? 0) <= 1) continue;
473
+ const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
474
+ duplicateCandidateCounts.set(candidate, (duplicateCandidateCounts.get(candidate) ?? 0) + 1);
475
+ }
476
+ return (target, targetIndex) => {
531
477
  const base = createBaseTargetIdentity(target);
532
478
  if ((baseCounts.get(base) ?? 0) <= 1) return base;
533
- if (target.range) return `${base}:${target.range.start}:${target.range.end}`;
534
- return base;
479
+ const candidate = target.range ? `${base}:${target.range.start}:${target.range.end}` : base;
480
+ if ((duplicateCandidateCounts.get(candidate) ?? 0) <= 1) return candidate;
481
+ return `${candidate}:${targetIndex}`;
535
482
  };
536
483
  }
537
- function hashText(text) {
538
- return createHash("sha256").update(text).digest("hex");
539
- }
540
484
  function normalizeCachePath(cwd, filePath) {
541
- return relative$1(cwd, isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath)).split(sep).join("/");
485
+ return relative$1(cwd, isAbsolute$1(filePath) ? resolve$1(filePath) : resolve$1(cwd, filePath)).split(sep).join("/") || ".";
542
486
  }
543
487
  function normalizeRunnerCacheConfig(cache, cwd) {
544
488
  if (cache === false) return {
@@ -559,12 +503,50 @@ function resolveCacheLocation(cwd, location) {
559
503
  const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
560
504
  if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
561
505
  try {
562
- if (statSyncIsDirectory(resolved)) return join(resolved, DEFAULT_CACHE_FILE_NAME);
506
+ if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
563
507
  } catch {}
564
508
  return resolved;
565
509
  }
566
- function stableHash(value) {
567
- return hashText(stableStringify(value));
510
+ function beginOwner(body, owner, cwd) {
511
+ const normalizedOwner = {
512
+ kind: owner.kind,
513
+ path: normalizeCachePath(cwd, owner.path)
514
+ };
515
+ const key = ownerKey(normalizedOwner, cwd);
516
+ const previousOwner = body.owners[key];
517
+ const nextEntries = /* @__PURE__ */ new Map();
518
+ return {
519
+ commit: (metadata = {}) => {
520
+ const committedEntries = metadata.mode === "merge" ? ownerEntries(body, body.owners[key]) : /* @__PURE__ */ new Map();
521
+ for (const [entryKey, cacheEntry] of nextEntries) committedEntries.set(entryKey, cacheEntry);
522
+ const replacedSlots = /* @__PURE__ */ new Set([...previousOwner?.slots ?? [], ...body.owners[key]?.slots ?? []]);
523
+ for (const replacedSlot of replacedSlots) delete body.entries[replacedSlot];
524
+ for (const [entryKey, cacheEntry] of committedEntries) body.entries[entryKey] = cacheEntry;
525
+ body.owners[key] = {
526
+ contentHash: metadata.contentHash,
527
+ kind: normalizedOwner.kind,
528
+ path: normalizedOwner.path,
529
+ slots: [...committedEntries.keys()].sort()
530
+ };
531
+ body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
532
+ },
533
+ discard: (cacheSlot) => nextEntries.delete(slotKey(normalizedOwner, cacheSlot, cwd)),
534
+ lookup: (cacheSlot, fingerprint) => {
535
+ const entryKey = slotKey(normalizedOwner, cacheSlot, cwd);
536
+ const cached = body.entries[entryKey];
537
+ if (!cached || !fingerprintsEqual(cached.fingerprint, fingerprint)) return void 0;
538
+ nextEntries.set(entryKey, cached);
539
+ return cached;
540
+ },
541
+ put: (cacheSlot, cacheEntry) => nextEntries.set(slotKey(normalizedOwner, cacheSlot, cwd), cacheEntry)
542
+ };
543
+ }
544
+ async function collectMissingFileOwners(body, cwd, fileExists) {
545
+ for (const [key, owner] of Object.entries(body.owners)) {
546
+ if (owner.kind !== "file" || await fileExists(resolve$1(cwd, owner.path))) continue;
547
+ for (const entryKey of owner.slots) delete body.entries[entryKey];
548
+ delete body.owners[key];
549
+ }
568
550
  }
569
551
  function createBaseTargetIdentity(target) {
570
552
  if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
@@ -573,53 +555,216 @@ function createBaseTargetIdentity(target) {
573
555
  if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
574
556
  return target.kind;
575
557
  }
576
- function createEmptyCacheFile() {
558
+ function createEmptyCacheBody() {
577
559
  const now = (/* @__PURE__ */ new Date()).toISOString();
578
560
  return {
579
561
  createdAt: now,
580
562
  entries: {},
581
- files: {},
582
- schemaVersion: CACHE_SCHEMA_VERSION,
563
+ owners: {},
583
564
  updatedAt: now
584
565
  };
585
566
  }
586
567
  function createNoopCacheStore(location) {
568
+ const transaction = {
569
+ commit: () => {},
570
+ discard: () => {},
571
+ lookup: () => void 0,
572
+ put: () => {}
573
+ };
574
+ return {
575
+ beginOwner: () => transaction,
576
+ location,
577
+ reconcile: async () => {}
578
+ };
579
+ }
580
+ function createReadOnlyCacheStore(location, body, cwd) {
581
+ const transaction = (owner) => ({
582
+ commit: () => {},
583
+ discard: () => {},
584
+ lookup: (slot, fingerprint) => {
585
+ const cached = body.entries[slotKey(owner, slot, cwd)];
586
+ return cached && fingerprintsEqual(cached.fingerprint, fingerprint) ? cached : void 0;
587
+ },
588
+ put: () => {}
589
+ });
587
590
  return {
588
- get: () => void 0,
591
+ beginOwner: transaction,
589
592
  location,
590
- markFile: () => {},
591
- reconcile: async () => {},
592
- set: () => {}
593
+ reconcile: async () => {}
593
594
  };
594
595
  }
595
- function isCacheFile(value) {
596
- return is(CacheFileSchema, value);
596
+ async function defaultFileExists(path) {
597
+ try {
598
+ await access(path);
599
+ return true;
600
+ } catch (error) {
601
+ if (isMissingFileError(error)) return false;
602
+ throw error;
603
+ }
604
+ }
605
+ function fingerprintsEqual(left, right) {
606
+ return left.configHash === right.configHash && left.modelHash === right.modelHash && left.ruleHash === right.ruleHash && left.targetHash === right.targetHash;
607
+ }
608
+ function hasJsonArrayProperties(input, ancestors) {
609
+ let arrayLength;
610
+ let indexCount = 0;
611
+ for (const key of Reflect.ownKeys(input)) {
612
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
613
+ if (!descriptor || !("value" in descriptor)) return false;
614
+ if (key === "length") {
615
+ if (descriptor.enumerable || typeof descriptor.value !== "number") return false;
616
+ arrayLength = descriptor.value;
617
+ continue;
618
+ }
619
+ if (typeof key !== "string" || !isCanonicalArrayIndex(key) || !descriptor.enumerable) return false;
620
+ if (!isJsonValueAt(descriptor.value, ancestors)) return false;
621
+ indexCount += 1;
622
+ }
623
+ return arrayLength !== void 0 && indexCount === arrayLength;
624
+ }
625
+ function hasJsonObjectProperties(input, ancestors) {
626
+ for (const key of Reflect.ownKeys(input)) {
627
+ if (typeof key !== "string") return false;
628
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
629
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) return false;
630
+ if (!isJsonValueAt(descriptor.value, ancestors)) return false;
631
+ }
632
+ return true;
633
+ }
634
+ function isCanonicalArrayIndex(key) {
635
+ const index = Number(key);
636
+ return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
597
637
  }
598
- function isRecord(value) {
599
- return typeof value === "object" && value !== null && !Array.isArray(value);
638
+ function isJsonValue(input) {
639
+ return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
600
640
  }
601
- async function readCacheFile(location) {
641
+ function isJsonValueAt(input, ancestors) {
642
+ if (input === null || typeof input === "string" || typeof input === "boolean") return true;
643
+ if (typeof input === "number") return Number.isFinite(input);
644
+ if (typeof input !== "object") return false;
645
+ if (ancestors.has(input)) return false;
646
+ ancestors.add(input);
602
647
  try {
603
- const parsed = JSON.parse(await readFile(location, "utf8"));
604
- if (isCacheFile(parsed)) return parsed;
605
- } catch {}
606
- return createEmptyCacheFile();
648
+ if (Array.isArray(input)) {
649
+ if (Object.getPrototypeOf(input) !== Array.prototype) return false;
650
+ return hasJsonArrayProperties(input, ancestors);
651
+ }
652
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return false;
653
+ return hasJsonObjectProperties(input, ancestors);
654
+ } catch {
655
+ return false;
656
+ } finally {
657
+ ancestors.delete(input);
658
+ }
607
659
  }
608
- function stableStringify(value) {
609
- if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
610
- if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => {
611
- const property = value[key];
612
- if (property === void 0) return;
613
- return `${JSON.stringify(key)}:${stableStringify(property)}`;
614
- }).filter((entry) => entry !== void 0).join(",")}}`;
615
- return JSON.stringify(value);
660
+ function isMissingFileError(error) {
661
+ return isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR");
662
+ }
663
+ function isNodeErrorCode(error, code) {
664
+ return error instanceof Error && "code" in error && error.code === code;
665
+ }
666
+ async function loadCacheBody(location, expectedHeader, readOnly) {
667
+ let handle;
668
+ try {
669
+ handle = await open(location, "r");
670
+ const header = await readHeader(handle);
671
+ await handle.close();
672
+ handle = void 0;
673
+ if (header !== expectedHeader) {
674
+ if (!readOnly) await rm(location, { force: true });
675
+ return createEmptyCacheBody();
676
+ }
677
+ const text = await readFile(location, "utf8");
678
+ return parseCacheBody(text.slice(text.indexOf("\n") + 1));
679
+ } catch (error) {
680
+ await handle?.close().catch(() => {});
681
+ if (isMissingFileError(error)) return createEmptyCacheBody();
682
+ if (!readOnly) await rm(location, { force: true }).catch(() => {});
683
+ return createEmptyCacheBody();
684
+ }
685
+ }
686
+ function ownerEntries(body, owner) {
687
+ const entries = /* @__PURE__ */ new Map();
688
+ for (const slot of owner?.slots ?? []) {
689
+ const cacheEntry = body.entries[slot];
690
+ if (cacheEntry) entries.set(slot, cacheEntry);
691
+ }
692
+ return entries;
693
+ }
694
+ function ownerKey(owner, cwd) {
695
+ return stableHash({
696
+ kind: owner.kind,
697
+ path: normalizeCachePath(cwd, owner.path)
698
+ });
699
+ }
700
+ function parseCacheBody(text) {
701
+ const value = JSON.parse(text);
702
+ return parse(CacheFileBodySchema, value);
703
+ }
704
+ async function persistCacheBody(location, header, body, writeFile) {
705
+ const validatedBody = parse(CacheFileBodySchema, body);
706
+ await mkdir(dirname(location), { recursive: true });
707
+ const tempPath = join(dirname(location), `.${basename(location)}.${process.pid}.${randomUUID()}.tmp`);
708
+ try {
709
+ await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
710
+ await rename(tempPath, location);
711
+ } catch (error) {
712
+ await rm(tempPath, { force: true }).catch(() => {});
713
+ throw error;
714
+ }
616
715
  }
617
- function statSyncIsDirectory(path) {
618
- return statSync(path).isDirectory();
716
+ async function readHeader(handle) {
717
+ const buffer = Buffer.alloc(256);
718
+ const { bytesRead } = await handle.read(buffer, 0, 256, 0);
719
+ const newline = buffer.subarray(0, bytesRead).indexOf(10);
720
+ return newline < 0 ? void 0 : buffer.subarray(0, newline).toString("utf8");
721
+ }
722
+ function slotKey(owner, slot, cwd) {
723
+ return stableHash({
724
+ owner: ownerKey(owner, cwd),
725
+ ...slot
726
+ });
619
727
  }
620
728
  //#endregion
621
- //#region package.json
622
- var version = "0.0.38";
729
+ //#region src/core/execution/records.ts
730
+ function snapshotDiagnostic(diagnostic) {
731
+ const snapshot = { ...diagnostic };
732
+ if (diagnostic.loc) snapshot.loc = {
733
+ ...diagnostic.loc.end ? { end: { ...diagnostic.loc.end } } : {},
734
+ start: { ...diagnostic.loc.start }
735
+ };
736
+ if (diagnostic.model) snapshot.model = { ...diagnostic.model };
737
+ return snapshot;
738
+ }
739
+ function snapshotDiagnostics(diagnostics) {
740
+ return diagnostics.map(snapshotDiagnostic);
741
+ }
742
+ function snapshotFailure(failure) {
743
+ return {
744
+ job: snapshotProgressJobRef(failure.job),
745
+ kind: failure.kind,
746
+ message: failure.message
747
+ };
748
+ }
749
+ function snapshotProgressJobRef(job) {
750
+ return {
751
+ id: job.id,
752
+ index: job.index,
753
+ inputPath: job.inputPath,
754
+ ruleId: job.ruleId,
755
+ target: {
756
+ identity: job.target.identity,
757
+ kind: job.target.kind,
758
+ name: job.target.name
759
+ }
760
+ };
761
+ }
762
+ function snapshotUsage(record) {
763
+ return { ...record };
764
+ }
765
+ function snapshotUsageRecords(records) {
766
+ return records.map(snapshotUsage);
767
+ }
623
768
  //#endregion
624
769
  //#region src/core/execution/job.ts
625
770
  var CacheReplayProcessingError = class {
@@ -628,76 +773,43 @@ var CacheReplayProcessingError = class {
628
773
  this.cause = cause;
629
774
  }
630
775
  };
631
- function createRuleJobs(plans) {
632
- const jobs = [];
633
- const total = plans.reduce((sum, plan) => sum + plan.planned, 0);
634
- const ruleTotals = /* @__PURE__ */ new Map();
635
- const ruleIndexes = /* @__PURE__ */ new Map();
636
- for (const plan of plans) for (const target of plan.targets) for (const execution of target.executions) {
637
- const ruleId = execution.runtime.enabledRule.id;
638
- ruleTotals.set(ruleId, (ruleTotals.get(ruleId) ?? 0) + 1);
639
- }
640
- for (const plan of plans) for (const target of plan.targets) for (const execution of target.executions) {
641
- const ruleId = execution.runtime.enabledRule.id;
642
- const index = jobs.length + 1;
643
- const ruleIndex = (ruleIndexes.get(ruleId) ?? 0) + 1;
644
- ruleIndexes.set(ruleId, ruleIndex);
645
- jobs.push({
646
- execution,
647
- job: {
648
- id: stableHash({
649
- index,
650
- planId: plan.id,
651
- ruleId,
652
- targetIdentity: target.identity
653
- }),
654
- index,
655
- inputPath: plan.path,
656
- ruleId,
657
- ruleIndex,
658
- ruleTotal: ruleTotals.get(ruleId) ?? 0,
659
- target: {
660
- identity: target.identity,
661
- kind: target.kind,
662
- name: target.name
663
- },
664
- total
665
- },
666
- target
667
- });
668
- }
669
- return jobs;
776
+ function compareJobOrder(left, right) {
777
+ return scopeRank(left.scope) - scopeRank(right.scope) || left.inputIndex - right.inputIndex || left.targetIndex - right.targetIndex || left.ruleIndex - right.ruleIndex;
670
778
  }
671
779
  async function executeRuleJob(job, options) {
672
780
  const bucket = {
673
781
  diagnostics: [],
674
782
  usage: []
675
783
  };
676
- if (options.runSignal?.aborted) return finish(job, options, bucket, {
784
+ if (options.runSignal?.aborted) return finish(job, bucket, {
677
785
  cache: "miss",
678
786
  state: "cancelled"
679
787
  });
680
- const cacheKey = createExecutionCacheKey(job, options.cache);
681
- const cachedEntry = cacheKey ? options.cache.store.get(cacheKey) : void 0;
682
- if (cacheKey && cachedEntry) {
683
- rememberCacheEntry(options.cache, job.target.cacheFilePaths, cacheKey);
788
+ const slot = job.target.cacheOwner && job.execution.runtime.cacheable ? {
789
+ ruleId: job.execution.runtime.enabledRule.id,
790
+ scope: job.target.kind,
791
+ targetIdentity: job.target.identity
792
+ } : void 0;
793
+ const fingerprint = slot ? createFingerprint(job, options.cache.modelHash) : void 0;
794
+ const cachedEntry = slot && fingerprint ? job.target.cacheOwner?.lookup(slot, fingerprint) : void 0;
795
+ if (slot && cachedEntry) {
684
796
  try {
685
- replayCachedEntry(cachedEntry, bucket, job.job, options.progress);
797
+ replayCachedEntry(cachedEntry, bucket, job.jobRef, options.progress, options.runProgress);
686
798
  } catch (error) {
687
799
  if (!(error instanceof CacheReplayProcessingError)) throw error;
688
- return finish(job, options, bucket, {
800
+ job.target.cacheOwner?.discard(slot);
801
+ return finish(job, bucket, {
689
802
  cache: "hit",
690
- cause: error.cause,
691
803
  failure: failure(error.cause, job, "cache-replay"),
692
804
  state: "failed"
693
805
  });
694
806
  }
695
- return finish(job, options, bucket, {
807
+ return finish(job, bucket, {
696
808
  cache: "hit",
697
809
  state: "cached"
698
810
  });
699
811
  }
700
- if (options.cacheOnly) return finish(job, options, bucket, {
812
+ if (options.cacheOnly) return finish(job, bucket, {
701
813
  cache: "miss",
702
814
  state: "skipped"
703
815
  });
@@ -715,8 +827,10 @@ async function executeRuleJob(job, options) {
715
827
  const state = {
716
828
  activeFilePath: job.target.activeFilePath,
717
829
  bucket,
718
- job: job.job,
830
+ jobRef: job.jobRef,
719
831
  reporterFailed: false,
832
+ runProgress: options.runProgress,
833
+ sealed: false,
720
834
  signal: controller.signal
721
835
  };
722
836
  let handlerCause;
@@ -728,31 +842,29 @@ async function executeRuleJob(job, options) {
728
842
  handlerCause = cause;
729
843
  handlerFailed = true;
730
844
  } finally {
845
+ state.sealed = true;
731
846
  if (timer != null) clearTimeout(timer);
732
847
  options.runSignal?.removeEventListener("abort", forwardRunAbort);
733
848
  }
734
849
  if (state.reporterFailed) throw state.reporterCause;
735
- if (!timedOut && !handlerFailed && cacheKey) try {
736
- options.cache.store.set(cacheKey, createCacheEntry(job, options.cache, bucket));
737
- rememberCacheEntry(options.cache, job.target.cacheFilePaths, cacheKey);
738
- } catch {}
739
- if (options.runSignal?.aborted) return finish(job, options, bucket, {
850
+ if (options.runSignal?.aborted) return finish(job, bucket, {
740
851
  cache: "miss",
741
852
  state: "cancelled"
742
853
  });
743
- if (timedOut) return finish(job, options, bucket, {
854
+ if (timedOut) return finish(job, bucket, {
744
855
  cache: "miss",
745
- cause: deadlineError,
746
856
  failure: failure(deadlineError, job, "timeout"),
747
857
  state: "failed"
748
858
  });
749
- if (handlerFailed) return finish(job, options, bucket, {
859
+ if (handlerFailed) return finish(job, bucket, {
750
860
  cache: "miss",
751
- cause: handlerCause,
752
861
  failure: failure(handlerCause, job, "handler"),
753
862
  state: "failed"
754
863
  });
755
- return finish(job, options, bucket, {
864
+ if (slot && fingerprint) try {
865
+ job.target.cacheOwner?.put(slot, createCacheEntry(job, fingerprint, bucket));
866
+ } catch {}
867
+ return finish(job, bucket, {
756
868
  cache: "miss",
757
869
  state: "completed"
758
870
  });
@@ -762,44 +874,44 @@ function resolveRuleExecutionTimeout(timeoutMs) {
762
874
  if (!Number.isFinite(timeoutMs) || !Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("Rule execution timeout must be a finite positive integer.");
763
875
  return timeoutMs;
764
876
  }
765
- function createCacheEntry(job, cache, bucket) {
877
+ function createCacheEntry(job, fingerprint, bucket) {
766
878
  return {
767
- diagnostics: bucket.diagnostics,
768
- filePath: normalizeCachePath(cache.cwd, job.job.inputPath),
769
- fingerprint: {
770
- alintVersion: version,
771
- configHash: job.target.configHash,
772
- modelHash: cache.modelHash,
773
- ruleHash: job.execution.runtime.ruleHash
774
- },
879
+ diagnostics: snapshotDiagnostics(bucket.diagnostics),
880
+ fingerprint,
775
881
  target: {
776
- hash: createTargetHash(job),
882
+ hash: fingerprint.targetHash,
777
883
  identity: job.target.identity,
778
884
  kind: job.target.kind,
779
- loc: job.target.loc,
885
+ loc: job.target.loc ? {
886
+ end: {
887
+ column: job.target.loc.end.column,
888
+ line: job.target.loc.end.line
889
+ },
890
+ start: {
891
+ column: job.target.loc.start.column,
892
+ line: job.target.loc.start.line
893
+ }
894
+ } : void 0,
780
895
  name: job.target.name,
781
- range: job.target.range
896
+ range: job.target.range ? {
897
+ end: job.target.range.end,
898
+ start: job.target.range.start
899
+ } : void 0
782
900
  },
783
- usage: bucket.usage
901
+ usage: snapshotUsageRecords(bucket.usage)
784
902
  };
785
903
  }
786
- function createExecutionCacheKey(job, cache) {
787
- if (!cache.enabled || !job.execution.runtime.cacheable || job.target.cacheFilePaths.length === 0) return void 0;
788
- return createCacheKey({
789
- alintVersion: version,
904
+ function createFingerprint(job, modelHash) {
905
+ return {
790
906
  configHash: job.target.configHash,
791
- filePath: normalizeCachePath(cache.cwd, job.job.inputPath),
792
- modelHash: cache.modelHash,
907
+ modelHash,
793
908
  ruleHash: job.execution.runtime.ruleHash,
794
- schemaVersion: 1,
795
- targetHash: createTargetHash(job),
796
- targetIdentity: job.target.identity,
797
- targetKind: job.target.kind
798
- });
909
+ targetHash: createTargetHash(job)
910
+ };
799
911
  }
800
912
  function createTargetHash(job) {
801
913
  const target = job.target;
802
- return stableHash({
914
+ return target.cacheTargetHash ?? stableHash({
803
915
  language: target.language,
804
916
  loc: target.loc,
805
917
  metadata: target.metadata,
@@ -811,36 +923,32 @@ function createTargetHash(job) {
811
923
  }
812
924
  function failure(cause, job, kind) {
813
925
  return {
814
- job: job.job,
926
+ job: snapshotProgressJobRef(job.jobRef),
815
927
  kind,
816
- message: errorMessageFrom(cause) ?? String(cause)
928
+ message: failureMessage$1(cause)
817
929
  };
818
930
  }
819
- function finish(job, options, bucket, terminal) {
820
- const outcome = {
821
- bucket,
822
- job,
823
- ...terminal
824
- };
825
- options.progress?.onJobEnd?.({
826
- cache: outcome.cache,
827
- endedAt: options.clock(),
828
- failure: outcome.failure,
829
- job: job.job,
830
- startedAt: options.startedAt,
831
- state: outcome.state
832
- });
833
- return outcome;
834
- }
835
- function rememberCacheEntry(cache, filePaths, cacheKey) {
836
- for (const filePath of filePaths) {
837
- const normalizedPath = normalizeCachePath(cache.cwd, filePath);
838
- const entries = cache.fileEntryKeys.get(normalizedPath) ?? /* @__PURE__ */ new Set();
839
- entries.add(cacheKey);
840
- cache.fileEntryKeys.set(normalizedPath, entries);
931
+ function failureMessage$1(cause) {
932
+ try {
933
+ return errorMessageFrom(cause) ?? "Unknown rule failure.";
934
+ } catch {
935
+ return "Unknown rule failure.";
841
936
  }
842
937
  }
843
- function replayCachedEntry(entry, bucket, job, progress) {
938
+ function finish(job, bucket, terminal) {
939
+ const detachedTerminal = terminal.state === "failed" ? {
940
+ ...terminal,
941
+ failure: snapshotFailure(terminal.failure)
942
+ } : terminal;
943
+ return {
944
+ diagnostics: snapshotDiagnostics(bucket.diagnostics),
945
+ jobRef: snapshotProgressJobRef(job.jobRef),
946
+ orderKey: { ...job.orderKey },
947
+ usage: snapshotUsageRecords(bucket.usage),
948
+ ...detachedTerminal
949
+ };
950
+ }
951
+ function replayCachedEntry(entry, bucket, job, progress, runProgress) {
844
952
  let diagnostics;
845
953
  let usage;
846
954
  try {
@@ -850,452 +958,209 @@ function replayCachedEntry(entry, bucket, job, progress) {
850
958
  throw new CacheReplayProcessingError(cause);
851
959
  }
852
960
  for (const cachedDiagnostic of diagnostics) {
853
- const diagnostic = {
961
+ const diagnostic = snapshotDiagnostic({
854
962
  ...cachedDiagnostic,
855
963
  cached: true
856
- };
964
+ });
857
965
  bucket.diagnostics.push(diagnostic);
858
966
  progress?.onDiagnostic?.({
859
- diagnostic,
860
- job
967
+ diagnostic: snapshotDiagnostic(diagnostic),
968
+ job: snapshotProgressJobRef(job),
969
+ progress: runProgress.snapshot()
861
970
  });
862
971
  }
863
972
  for (const record of usage) {
864
- bucket.usage.push(record);
973
+ const usageRecord = snapshotUsage(record);
974
+ bucket.usage.push(usageRecord);
865
975
  progress?.onUsage?.({
866
- job,
867
- record
976
+ job: snapshotProgressJobRef(job),
977
+ progress: runProgress.snapshot(),
978
+ record: snapshotUsage(usageRecord)
868
979
  });
869
980
  }
870
981
  }
871
- //#endregion
872
- //#region src/core/targets/directory.ts
873
- function createDirectoryExecutionPlans(directories, sourcePlanCount) {
874
- return directories.map((directory, directoryOffset) => {
875
- const executions = directory.ruleRuntimes.map((runtime) => {
876
- if (runtime.handlers.onTargetWith) return {
877
- run: () => runtime.handlers.onTargetWith?.(directory.target),
878
- runtime
879
- };
880
- if (runtime.handlers.onTargetDirectory) return {
881
- run: () => runtime.handlers.onTargetDirectory?.(directory.target),
882
- runtime
883
- };
884
- }).filter((execution) => execution !== void 0);
885
- const targets = executions.length === 0 ? [] : [{
886
- activeFilePath: directory.target.path,
887
- cacheFilePaths: [],
888
- configHash: directory.configHash,
889
- executions,
890
- identity: directory.target.path,
891
- kind: "directory",
892
- language: "directory",
893
- text: directory.target.path
894
- }];
895
- return {
896
- id: `directory:${directory.target.path}`,
897
- index: sourcePlanCount + directoryOffset + 1,
898
- kind: "directory",
899
- path: directory.target.path,
900
- planned: executions.length,
901
- targets
902
- };
903
- });
982
+ function scopeRank(scope) {
983
+ if (scope === "source") return 0;
984
+ if (scope === "directory") return 1;
985
+ return 2;
904
986
  }
905
987
  //#endregion
906
- //#region src/core/targets/project.ts
907
- function createProjectExecutionPlan(options) {
908
- const project = createProjectTarget(options.root, options.files);
909
- const executions = options.ruleRuntimes.map((runtime) => {
910
- if (runtime.handlers.onTargetWith) return {
911
- run: () => runtime.handlers.onTargetWith?.(project),
912
- runtime
913
- };
914
- if (runtime.handlers.onTargetProject) return {
915
- run: () => runtime.handlers.onTargetProject?.(project),
916
- runtime
988
+ //#region src/core/execution/progress.ts
989
+ function createRunProgress(filesTotal) {
990
+ assertNonNegativeInteger(filesTotal, "File total");
991
+ const execution = createCounts();
992
+ let final = false;
993
+ const snapshot = () => {
994
+ const jobsCompleted = terminalCount(execution);
995
+ return {
996
+ execution: { ...execution },
997
+ filesTotal,
998
+ final,
999
+ jobsCompleted,
1000
+ jobsStarted: jobsCompleted + execution.running,
1001
+ jobsTotal: execution.planned
917
1002
  };
918
- }).filter((execution) => execution !== void 0);
919
- if (executions.length === 0) return;
920
- const target = {
921
- cacheFilePaths: options.files.map((file) => file.file.path),
922
- configHash: options.configHash,
923
- executions,
924
- identity: "project",
925
- kind: "project",
926
- language: "project",
927
- text: stableHash({
928
- files: options.files.map((file) => ({
929
- configHash: file.configHash,
930
- contentHash: hashText(file.file.text),
931
- path: normalizeCachePath(options.root, file.file.path)
932
- })).sort((left, right) => left.path.localeCompare(right.path)),
933
- targets: project.targets.map((target) => ({
934
- filePath: normalizeCachePath(options.root, target.file.path),
935
- identity: target.identity,
936
- kind: target.kind,
937
- language: target.language,
938
- loc: target.loc,
939
- metadata: target.metadata,
940
- name: target.name,
941
- origin: target.origin,
942
- range: target.range,
943
- text: target.text
944
- })),
945
- tree: createProjectTreeShape(options.files, options.root)
946
- })
947
- };
948
- return {
949
- id: `project:${options.root}`,
950
- index: options.index,
951
- kind: "project",
952
- path: options.root,
953
- planned: executions.length,
954
- targets: [target]
955
- };
956
- }
957
- function createProjectTarget(root, files) {
958
- const preparedFiles = [...files].sort((left, right) => left.file.path.localeCompare(right.file.path));
959
- return {
960
- files: preparedFiles.map((file) => file.file),
961
- kind: "project",
962
- root,
963
- targets: preparedFiles.flatMap((file) => file.targets)
964
1003
  };
965
- }
966
- function createProjectTreeShape(files, root) {
967
- const directories = /* @__PURE__ */ new Set();
968
- const paths = files.map((file) => normalizeCachePath(root, file.file.path)).sort();
969
- for (const path of paths) {
970
- const parts = path.split("/");
971
- for (let index = 1; index < parts.length; index += 1) directories.add(parts.slice(0, index).join("/"));
972
- }
973
1004
  return {
974
- directories: [...directories].sort(),
975
- files: paths
1005
+ finalize: () => {
1006
+ if (execution.queued !== 0 || execution.running !== 0) throw new Error("Cannot finalize progress while jobs are queued or running.");
1007
+ final = true;
1008
+ return snapshot();
1009
+ },
1010
+ finish: (from, to) => {
1011
+ assertMutable(final);
1012
+ if (execution[from] <= 0) throw new Error(`Cannot finish a job without a ${from} job.`);
1013
+ execution[from] -= 1;
1014
+ execution[to] += 1;
1015
+ return snapshot();
1016
+ },
1017
+ queue: (count = 1) => {
1018
+ assertMutable(final);
1019
+ assertNonNegativeInteger(count, "Queued job count");
1020
+ assertSafeSum(execution.planned, count, "Planned job count");
1021
+ assertSafeSum(execution.queued, count, "Queued job count");
1022
+ execution.planned += count;
1023
+ execution.queued += count;
1024
+ return snapshot();
1025
+ },
1026
+ snapshot,
1027
+ start: () => {
1028
+ assertMutable(final);
1029
+ if (execution.queued <= 0) throw new Error("Cannot start a job without a queued job.");
1030
+ execution.queued -= 1;
1031
+ execution.running += 1;
1032
+ return snapshot();
1033
+ }
976
1034
  };
977
1035
  }
978
- //#endregion
979
- //#region src/core/targets/source.ts
980
- function createSourceExecutionPlans(files, cwd) {
981
- return files.map((preparedFile, fileOffset) => {
982
- const targets = collectExecutionTargets(preparedFile);
983
- const resolveTargetIdentity = createTargetIdentityResolver(targets.map((target) => toTargetIdentityInput(cwd, preparedFile.file.path, target)));
984
- for (const target of targets) target.identity = resolveTargetIdentity(toTargetIdentityInput(cwd, preparedFile.file.path, target));
985
- const filePlan = {
986
- id: `source:${preparedFile.file.path}`,
987
- index: fileOffset + 1,
988
- kind: "source",
989
- path: preparedFile.file.path,
990
- planned: 0,
991
- preparedFile,
992
- targets
993
- };
994
- filePlan.planned = calculateFilePlanExecutions(filePlan);
995
- return filePlan;
996
- });
1036
+ function assertMutable(final) {
1037
+ if (final) throw new Error("Cannot update finalized progress.");
997
1038
  }
998
- function calculateFilePlanExecutions(filePlan) {
999
- return filePlan.targets.reduce((total, target) => total + target.executions.length, 0);
1000
- }
1001
- function collectExecutionTargets(preparedFile) {
1002
- const targets = [];
1003
- for (const sourceTarget of preparedFile.targets) {
1004
- const executions = preparedFile.ruleRuntimes.map((runtime) => {
1005
- return createSourceTargetExecution(runtime, sourceTarget);
1006
- }).filter((execution) => execution !== void 0);
1007
- if (executions.length === 0) continue;
1008
- targets.push({
1009
- activeFilePath: preparedFile.file.path,
1010
- cacheFilePaths: [preparedFile.file.path],
1011
- configHash: preparedFile.configHash,
1012
- executions,
1013
- identity: sourceTarget.identity,
1014
- kind: sourceTarget.kind,
1015
- language: sourceTarget.language,
1016
- loc: sourceTarget.loc,
1017
- metadata: sourceTarget.metadata,
1018
- name: sourceTarget.name,
1019
- origin: sourceTarget.origin,
1020
- range: sourceTarget.range,
1021
- text: sourceTarget.text
1022
- });
1023
- }
1024
- return targets;
1039
+ function assertNonNegativeInteger(value, name) {
1040
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer.`);
1025
1041
  }
1026
- function createSourceTargetExecution(runtime, target) {
1027
- if (runtime.handlers.onTargetWith) return {
1028
- run: () => runtime.handlers.onTargetWith?.(target),
1029
- runtime
1030
- };
1031
- if (target.kind === "class" && runtime.handlers.onTargetClass) return {
1032
- run: () => runtime.handlers.onTargetClass?.(target),
1033
- runtime
1034
- };
1035
- if (target.kind === "file" && runtime.handlers.onTargetFile) return {
1036
- run: () => runtime.handlers.onTargetFile?.(target),
1037
- runtime
1038
- };
1039
- if (target.kind === "function" && runtime.handlers.onTargetFunction) return {
1040
- run: () => runtime.handlers.onTargetFunction?.(target),
1041
- runtime
1042
- };
1042
+ function assertSafeSum(left, right, name) {
1043
+ if (!Number.isSafeInteger(left + right)) throw new TypeError(`${name} must remain a safe integer.`);
1043
1044
  }
1044
- function toTargetIdentityInput(cwd, filePath, target) {
1045
+ function createCounts() {
1045
1046
  return {
1046
- filePath: target.kind === "file" ? normalizeCachePath(cwd, filePath) : void 0,
1047
- identity: target.identity,
1048
- kind: target.kind,
1049
- name: target.name,
1050
- range: target.range
1047
+ cached: 0,
1048
+ cancelled: 0,
1049
+ completed: 0,
1050
+ failed: 0,
1051
+ planned: 0,
1052
+ queued: 0,
1053
+ running: 0,
1054
+ skipped: 0
1051
1055
  };
1052
1056
  }
1057
+ function terminalCount(execution) {
1058
+ return execution.cached + execution.cancelled + execution.completed + execution.failed + execution.skipped;
1059
+ }
1053
1060
  //#endregion
1054
- //#region src/core/run.ts
1055
- var AlintRunCancelledError = class extends Error {
1056
- result;
1057
- constructor(result, cause) {
1058
- super("Alint run cancelled.", { cause });
1059
- this.name = "AlintRunCancelledError";
1060
- this.result = result;
1061
- }
1062
- };
1063
- /** Cancellation error carrying the partial result gathered before the abort. */
1064
- var AlintAbortError = class extends AlintRunCancelledError {
1065
- constructor(result, options = {}) {
1066
- super(result, options.cause);
1067
- this.name = "AlintAbortError";
1068
- }
1069
- };
1070
- var AlintRunError = class extends Error {
1071
- failures;
1072
- result;
1073
- constructor(message, result, options = {}) {
1074
- super(message, { cause: options.cause });
1075
- this.name = "AlintRunError";
1076
- this.failures = options.failures ?? [];
1077
- this.result = result;
1078
- }
1079
- };
1080
- async function runAlint(options = {}) {
1081
- const timeoutMs = resolveRuleExecutionTimeout(options.runner?.timeoutMs);
1082
- const concurrency = resolveRuleConcurrency(options.runner?.ruleConcurrency);
1083
- const cwd$1 = options.cwd ?? cwd();
1084
- const config = options.config ?? [];
1085
- const setupConfig = options.setupConfig ?? {
1086
- providers: [],
1087
- version: 1
1088
- };
1089
- const clock = Date.now;
1090
- const src = createSourceRuntime();
1091
- const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
1092
- const cacheStore = await createCacheStore({
1093
- cwd: cwd$1,
1094
- enabled: normalizedCacheConfig.enabled,
1095
- location: normalizedCacheConfig.location
1096
- });
1097
- const cacheContext = {
1098
- cwd: cwd$1,
1099
- enabled: normalizedCacheConfig.enabled,
1100
- fileEntryKeys: /* @__PURE__ */ new Map(),
1101
- modelHash: stableHash({
1102
- modelOverride: options.modelOverride,
1103
- outputLanguage: options.outputLanguage,
1104
- setupConfig
1105
- }),
1106
- store: cacheStore
1107
- };
1108
- const pendingFiles = [...options.files ?? []];
1109
- const pendingDirectories = [...options.directories ?? []];
1110
- const files = (await Promise.all(pendingFiles.map(async (filePath) => {
1111
- const file = await src.readFile(resolve(cwd$1, filePath));
1112
- const resolvedConfig = resolveConfigForFile(file.path, config, { cwd: cwd$1 });
1113
- if (resolvedConfig.ignored) return;
1114
- const effectiveConfig = resolvedConfig.config;
1115
- const languageRegistry = createBuiltInLanguageRegistry();
1116
- for (const plugin of Object.values(effectiveConfig.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(languageRegistry, language);
1117
- const language = resolveLanguage(file, languageRegistry, { language: effectiveConfig.language });
1118
- const targets = await language.extract(file, {
1119
- cwd: cwd$1,
1120
- languageOptions: effectiveConfig.languageOptions,
1121
- src
1122
- });
1123
- const registry = buildRuleRegistry(effectiveConfig);
1124
- const ruleRuntimes = createRuleRuntimes({
1125
- cwd: cwd$1,
1126
- effectiveAgent: effectiveConfig.agent,
1127
- effectiveSettings: effectiveConfig.settings,
1128
- options,
1129
- progress: options.progress,
1130
- registry,
1131
- setupConfig,
1132
- src
1133
- });
1134
- return {
1135
- configHash: stableHash({
1136
- language: effectiveConfig.language,
1137
- languageOptions: effectiveConfig.languageOptions,
1138
- processor: effectiveConfig.processor,
1139
- resolvedLanguage: language.name,
1140
- settings: effectiveConfig.settings
1141
- }),
1142
- file,
1143
- ruleRuntimes,
1144
- targets
1145
- };
1146
- }))).filter((file) => file !== void 0);
1147
- const directories = pendingDirectories.map((directoryPath) => {
1148
- const target = {
1149
- kind: "directory",
1150
- path: resolve(cwd$1, directoryPath)
1151
- };
1152
- const resolvedConfig = resolveConfigForDirectory(target.path, config, { cwd: cwd$1 });
1153
- if (resolvedConfig.ignored) return;
1154
- const effectiveConfig = resolvedConfig.config;
1155
- const registry = buildRuleRegistry(effectiveConfig);
1156
- return {
1157
- configHash: stableHash({ settings: effectiveConfig.settings }),
1158
- ruleRuntimes: createRuleRuntimes({
1159
- cwd: cwd$1,
1160
- effectiveAgent: effectiveConfig.agent,
1161
- effectiveSettings: effectiveConfig.settings,
1162
- options,
1163
- progress: options.progress,
1164
- registry,
1165
- setupConfig,
1166
- src
1167
- }),
1168
- target
1169
- };
1170
- }).filter((directory) => directory !== void 0);
1171
- const resolvedProjectConfig = resolveConfigForProject(cwd$1, config, { cwd: cwd$1 });
1172
- const projectConfig = resolvedProjectConfig.config;
1173
- const projectRuleRuntimes = resolvedProjectConfig.ignored ? [] : createRuleRuntimes({
1174
- cwd: cwd$1,
1175
- effectiveAgent: projectConfig.agent,
1176
- effectiveSettings: projectConfig.settings,
1177
- options,
1178
- progress: options.progress,
1179
- registry: buildRuleRegistry(projectConfig),
1180
- setupConfig,
1181
- src
1182
- });
1183
- const filePlans = createSourceExecutionPlans(files, cwd$1);
1184
- const directoryPlans = createDirectoryExecutionPlans(directories, filePlans.length);
1185
- const projectPlan = resolvedProjectConfig.ignored || options.projectTargets === false ? void 0 : createProjectExecutionPlan({
1186
- configHash: stableHash({ settings: projectConfig.settings }),
1187
- files,
1188
- index: filePlans.length + directoryPlans.length + 1,
1189
- root: cwd$1,
1190
- ruleRuntimes: projectRuleRuntimes
1191
- });
1192
- const jobs = createRuleJobs([
1193
- ...filePlans,
1194
- ...directoryPlans,
1195
- ...projectPlan ? [projectPlan] : []
1196
- ]);
1197
- const runStartedAt = clock();
1198
- const settledOutcomes = new Array(jobs.length);
1199
- let infrastructureError;
1200
- let infrastructureFailed = false;
1201
- options.progress?.onRunStart?.({
1202
- jobsTotal: jobs.length,
1203
- startedAt: runStartedAt
1204
- });
1205
- for (const job of jobs) options.progress?.onJobQueued?.({ job: job.job });
1206
- try {
1207
- let cursor = 0;
1208
- const worker = async () => {
1209
- while (cursor < jobs.length) {
1210
- const index = cursor;
1211
- cursor += 1;
1212
- const job = jobs[index];
1213
- if (options.signal?.aborted) {
1214
- settledOutcomes[index] = {
1215
- bucket: {
1216
- diagnostics: [],
1217
- usage: []
1218
- },
1219
- cache: "miss",
1220
- job,
1221
- state: "cancelled"
1222
- };
1223
- continue;
1224
- }
1225
- try {
1226
- const startedAt = clock();
1227
- options.progress?.onJobStart?.({
1228
- job: job.job,
1229
- startedAt
1230
- });
1231
- settledOutcomes[index] = await executeRuleJob(job, {
1232
- cache: cacheContext,
1233
- cacheOnly: options.cacheOnly,
1234
- clock,
1235
- progress: options.progress,
1236
- runSignal: options.signal,
1237
- startedAt,
1238
- timeoutMs
1239
- });
1240
- } catch (error) {
1241
- if (!infrastructureFailed) {
1242
- infrastructureError = error;
1243
- infrastructureFailed = true;
1244
- }
1245
- }
1061
+ //#region src/models/resolve.ts
1062
+ function resolveModel(registry, options = {}) {
1063
+ const candidates = flattenModels(registry);
1064
+ const ruleId = options.ruleId ?? "<unknown>";
1065
+ if (options.request !== void 0) {
1066
+ const request = options.request;
1067
+ const canonicalCandidates = candidates.filter((candidate) => canonicalIdentity(candidate) === request);
1068
+ if (canonicalCandidates.length > 1) throw duplicateDefinitionError(request);
1069
+ let candidate = canonicalCandidates[0];
1070
+ if (candidate === void 0) {
1071
+ const matchingCandidates = candidates.filter((candidate) => matchesRequest(candidate, request));
1072
+ const matchesByCanonical = /* @__PURE__ */ new Map();
1073
+ for (const matchingCandidate of matchingCandidates) {
1074
+ const identity = canonicalIdentity(matchingCandidate);
1075
+ const matches = matchesByCanonical.get(identity) ?? [];
1076
+ matches.push(matchingCandidate);
1077
+ matchesByCanonical.set(identity, matches);
1246
1078
  }
1247
- };
1248
- await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, worker));
1249
- } finally {
1250
- if (!options.cacheOnly) await reconcileCache(filePlans, cacheContext);
1079
+ const duplicateIdentity = [...matchesByCanonical].find(([, matches]) => matches.length > 1)?.[0];
1080
+ if (duplicateIdentity !== void 0) throw duplicateDefinitionError(duplicateIdentity);
1081
+ if (matchingCandidates.length === 0) throw new Error(`Unknown model "${request}".`);
1082
+ if (matchingCandidates.length > 1) {
1083
+ const choices = [...matchesByCanonical.keys()];
1084
+ throw new Error(`Ambiguous model "${request}".\nSpecify a provider-qualified model:\n${choices.map((choice) => ` ${choice}`).join("\n")}`);
1085
+ }
1086
+ candidate = matchingCandidates[0];
1087
+ }
1088
+ if (!satisfiesHardRequirements(candidate.model, options.requirement)) throw new Error(`Model "${request}" does not satisfy requirement for rule "${ruleId}".`);
1089
+ return toResolvedModel(candidate, options.requirement);
1251
1090
  }
1252
- const outcomes = Array.from({ length: jobs.length }, (_, index) => settledOutcomes[index] ?? {
1253
- bucket: {
1254
- diagnostics: [],
1255
- usage: []
1091
+ const candidate = preferSize(candidates.filter(({ model }) => satisfiesHardRequirements(model, options.requirement)), options.requirement);
1092
+ if (candidate === void 0) throw new Error(`No model satisfies requirement for rule "${ruleId}".`);
1093
+ return toResolvedModel(candidate, options.requirement);
1094
+ }
1095
+ function canonicalIdentity({ model, provider }) {
1096
+ return `${provider.id}/${model.id}`;
1097
+ }
1098
+ function duplicateDefinitionError(identity) {
1099
+ return /* @__PURE__ */ new Error(`Model "${identity}" is configured more than once.\nRemove duplicate provider/model definitions from the setup configuration.`);
1100
+ }
1101
+ function flattenModels(registry) {
1102
+ return registry.providers.flatMap((provider) => provider.models.map((model) => ({
1103
+ model,
1104
+ provider
1105
+ })));
1106
+ }
1107
+ function matchesRequest(candidate, request) {
1108
+ return [
1109
+ candidate.model.id,
1110
+ candidate.model.name,
1111
+ ...candidate.model.aliases ?? []
1112
+ ].filter((name) => name !== void 0).some((name) => name === request || `${candidate.provider.id}/${name}` === request);
1113
+ }
1114
+ function preferSize(candidates, requirement) {
1115
+ if (requirement?.size === void 0) return candidates[0];
1116
+ return candidates.find(({ model }) => model.size === requirement.size) ?? candidates[0];
1117
+ }
1118
+ function satisfiesHardRequirements(model, requirement) {
1119
+ if (requirement === void 0) return true;
1120
+ if (requirement.capabilities !== void 0 && !requirement.capabilities.every((capability) => (model.capabilities ?? []).includes(capability))) return false;
1121
+ if (requirement.minContextWindow !== void 0 && (model.contextWindow === void 0 || model.contextWindow < requirement.minContextWindow)) return false;
1122
+ return true;
1123
+ }
1124
+ function toResolvedModel(candidate, requirement) {
1125
+ const { model, provider } = candidate;
1126
+ return {
1127
+ aliases: [...model.aliases ?? []],
1128
+ capabilities: [...model.capabilities ?? []],
1129
+ contextWindow: model.contextWindow,
1130
+ id: model.id,
1131
+ name: model.name ?? model.id,
1132
+ params: {
1133
+ ...model.defaultParams ?? {},
1134
+ ...requirement?.params ?? {}
1256
1135
  },
1257
- cache: "miss",
1258
- job: jobs[index],
1259
- state: "cancelled"
1260
- });
1261
- const failedOutcomes = outcomes.filter((outcome) => outcome.state === "failed");
1262
- const failures = failedOutcomes.map((outcome) => outcome.failure);
1263
- const result = {
1264
- diagnostics: outcomes.flatMap((outcome) => outcome.bucket.diagnostics),
1265
- execution: executionCounts(outcomes),
1266
- usage: runUsage(outcomes)
1136
+ provider: {
1137
+ endpoint: provider.endpoint,
1138
+ headers: { ...provider.headers ?? {} },
1139
+ id: provider.id,
1140
+ type: provider.type
1141
+ },
1142
+ size: model.size
1267
1143
  };
1268
- options.progress?.onRunEnd?.({
1269
- diagnostics: result.diagnostics,
1270
- endedAt: clock(),
1271
- execution: result.execution,
1272
- startedAt: runStartedAt,
1273
- usage: result.usage
1274
- });
1275
- if (infrastructureFailed) throw infrastructureError;
1276
- if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
1277
- if (failures.length > 0) throw new AlintRunError(`${failures.length} rule execution${failures.length === 1 ? "" : "s"} failed.`, result, {
1278
- cause: new AggregateError(failedOutcomes.map((outcome) => outcome.cause), "Rule execution failures."),
1279
- failures
1280
- });
1281
- return result;
1282
1144
  }
1145
+ //#endregion
1146
+ //#region src/core/execution/runtime.ts
1283
1147
  function createRuleRuntimes(options) {
1284
- return options.registry.enabledRules.map((enabledRule) => {
1148
+ return options.rules.map(({ enabledRule, ruleIndex }) => {
1285
1149
  const executionState = new AsyncLocalStorage();
1286
1150
  const context = {
1287
1151
  agent: options.effectiveAgent ? withAgentRetry((request) => options.effectiveAgent({
1288
1152
  ...request,
1289
1153
  signal: combineAbortSignals(executionState.getStore()?.signal, request.signal)
1290
- }), options.options.runner?.agentRetries, { onRetry: ({ attempt, maxAttempts }) => {
1154
+ }), options.runOptions.runner?.agentRetries, { onRetry: ({ attempt, maxAttempts }) => {
1291
1155
  const state = executionState.getStore();
1292
- if (!state) return;
1156
+ if (!state || state.sealed) return;
1293
1157
  const startedAt = Date.now();
1294
1158
  try {
1295
1159
  options.progress?.onJobRetry?.({
1296
1160
  attempt,
1297
- job: state.job,
1161
+ job: snapshotProgressJobRef(state.jobRef),
1298
1162
  maxAttempts,
1163
+ progress: state.runProgress.snapshot(),
1299
1164
  startedAt
1300
1165
  });
1301
1166
  } catch (cause) {
@@ -1313,15 +1178,17 @@ function createRuleRuntimes(options) {
1313
1178
  metering: { recordUsage: (record) => {
1314
1179
  const state = executionState.getStore();
1315
1180
  if (!state) throw new Error("Cannot record usage outside an active rule job.");
1316
- const usageRecord = {
1181
+ if (state.sealed) return;
1182
+ const usageRecord = snapshotUsage({
1317
1183
  ...record,
1318
1184
  ruleId: record.ruleId ?? enabledRule.id
1319
- };
1185
+ });
1320
1186
  state.bucket.usage.push(usageRecord);
1321
1187
  try {
1322
1188
  options.progress?.onUsage?.({
1323
- job: state.job,
1324
- record: usageRecord
1189
+ job: snapshotProgressJobRef(state.jobRef),
1190
+ progress: state.runProgress.snapshot(),
1191
+ record: snapshotUsage(usageRecord)
1325
1192
  });
1326
1193
  } catch (cause) {
1327
1194
  if (!state.reporterFailed) {
@@ -1332,7 +1199,7 @@ function createRuleRuntimes(options) {
1332
1199
  }
1333
1200
  } },
1334
1201
  model: async (selector) => {
1335
- const request = options.options.modelOverride ?? (typeof selector === "string" ? selector : void 0);
1202
+ const request = options.runOptions.modelOverride ?? (typeof selector === "string" ? selector : void 0);
1336
1203
  const requirement = mergeModelRequirement(enabledRule.rule.model, typeof selector === "string" ? void 0 : selector);
1337
1204
  const resolvedModel = resolveModel(options.setupConfig, {
1338
1205
  request,
@@ -1340,19 +1207,20 @@ function createRuleRuntimes(options) {
1340
1207
  ruleId: enabledRule.id
1341
1208
  });
1342
1209
  const state = executionState.getStore();
1343
- if (state) state.currentModel = toDiagnosticModel(resolvedModel, request);
1210
+ if (state && !state.sealed) state.currentModel = toDiagnosticModel(resolvedModel, request);
1344
1211
  return resolvedModel;
1345
1212
  },
1346
1213
  options: enabledRule.options,
1347
- outputLanguage: options.options.outputLanguage,
1214
+ outputLanguage: options.runOptions.outputLanguage,
1348
1215
  report: (descriptor) => {
1349
1216
  const state = executionState.getStore();
1350
1217
  if (!state) throw new Error("Cannot report a diagnostic outside an active rule job.");
1218
+ if (state.sealed) return;
1351
1219
  const filePath = descriptor.filePath ?? state.activeFilePath;
1352
1220
  if (!filePath) throw new Error(`Diagnostic for rule "${enabledRule.id}" is missing filePath.`);
1353
1221
  const diagnosticModel = state.currentModel ? { ...state.currentModel } : void 0;
1354
1222
  state.currentModel = void 0;
1355
- const diagnostic = {
1223
+ const diagnostic = snapshotDiagnostic({
1356
1224
  evidence: descriptor.evidence,
1357
1225
  filePath,
1358
1226
  loc: descriptor.loc,
@@ -1360,12 +1228,13 @@ function createRuleRuntimes(options) {
1360
1228
  model: diagnosticModel,
1361
1229
  ruleId: enabledRule.id,
1362
1230
  severity: enabledRule.severity
1363
- };
1231
+ });
1364
1232
  state.bucket.diagnostics.push(diagnostic);
1365
1233
  try {
1366
1234
  options.progress?.onDiagnostic?.({
1367
- diagnostic,
1368
- job: state.job
1235
+ diagnostic: snapshotDiagnostic(diagnostic),
1236
+ job: snapshotProgressJobRef(state.jobRef),
1237
+ progress: state.runProgress.snapshot()
1369
1238
  });
1370
1239
  } catch (cause) {
1371
1240
  if (!state.reporterFailed) {
@@ -1395,26 +1264,13 @@ function createRuleRuntimes(options) {
1395
1264
  model: enabledRule.rule.model,
1396
1265
  options: enabledRule.options,
1397
1266
  severity: enabledRule.severity
1398
- })
1267
+ }),
1268
+ ruleIndex
1399
1269
  };
1400
1270
  });
1401
1271
  }
1402
- function executionCounts(outcomes) {
1403
- const counts = {
1404
- cached: 0,
1405
- cancelled: 0,
1406
- completed: 0,
1407
- failed: 0,
1408
- planned: outcomes.length,
1409
- queued: 0,
1410
- running: 0,
1411
- skipped: 0
1412
- };
1413
- for (const outcome of outcomes) counts[outcome.state] += 1;
1414
- return counts;
1415
- }
1416
1272
  function mergeCapabilities(base, extra) {
1417
- if (!base && !extra) return;
1273
+ if (!base && !extra) return void 0;
1418
1274
  return [.../* @__PURE__ */ new Set([...base ?? [], ...extra ?? []])];
1419
1275
  }
1420
1276
  function mergeMinContextWindow(base, extra) {
@@ -1423,7 +1279,7 @@ function mergeMinContextWindow(base, extra) {
1423
1279
  return Math.max(base, extra);
1424
1280
  }
1425
1281
  function mergeModelRequirement(base, extra) {
1426
- if (!base && !extra) return;
1282
+ if (!base && !extra) return void 0;
1427
1283
  const capabilities = mergeCapabilities(base?.capabilities, extra?.capabilities);
1428
1284
  const params = {
1429
1285
  ...base?.params ?? {},
@@ -1436,36 +1292,1080 @@ function mergeModelRequirement(base, extra) {
1436
1292
  size: extra?.size ?? base?.size
1437
1293
  };
1438
1294
  }
1439
- async function reconcileCache(filePlans, cacheContext) {
1440
- try {
1441
- for (const filePlan of filePlans) {
1442
- const file = filePlan.preparedFile.file;
1443
- const normalizedPath = normalizeCachePath(cacheContext.cwd, file.path);
1444
- const entries = [...cacheContext.fileEntryKeys.get(normalizedPath) ?? []];
1445
- cacheContext.store.markFile(file.path, hashText(file.text), entries);
1446
- }
1447
- await cacheContext.store.reconcile();
1448
- } catch {}
1295
+ function toDiagnosticModel(model, request) {
1296
+ return {
1297
+ providerId: model.provider.id,
1298
+ requested: request,
1299
+ resolvedId: model.id
1300
+ };
1449
1301
  }
1450
- function resolveRuleConcurrency(ruleConcurrency) {
1451
- if (ruleConcurrency === void 0) return 4;
1302
+ //#endregion
1303
+ //#region src/core/execution/scheduler.ts
1304
+ var RuleScheduler = class {
1305
+ options;
1306
+ accepting = true;
1307
+ active = 0;
1308
+ cancellingQueued = false;
1309
+ closePromise;
1310
+ closeReject;
1311
+ closeResolve;
1312
+ infrastructureError;
1313
+ infrastructureFailed = false;
1314
+ lanes = [];
1315
+ nextAdmissionIndex = 1;
1316
+ pumpQueued = false;
1317
+ readyLanes = [];
1318
+ constructor(options) {
1319
+ this.options = options;
1320
+ resolveRuleConcurrency(options.concurrency);
1321
+ if (options.signal?.aborted) this.accepting = false;
1322
+ else options.signal?.addEventListener("abort", this.abort, { once: true });
1323
+ }
1324
+ cancelWithError(error) {
1325
+ this.fail(error);
1326
+ }
1327
+ close() {
1328
+ this.accepting = false;
1329
+ if (!this.closePromise) this.closePromise = new Promise((resolve, reject) => {
1330
+ this.closeResolve = resolve;
1331
+ this.closeReject = reject;
1332
+ });
1333
+ this.queuePump();
1334
+ this.settleClose();
1335
+ return this.closePromise;
1336
+ }
1337
+ schedule(jobs) {
1338
+ if (!this.accepting || jobs.length === 0) return {
1339
+ jobsAdded: 0,
1340
+ outcomes: Promise.resolve([])
1341
+ };
1342
+ const lane = createLane();
1343
+ this.lanes.push(lane);
1344
+ let jobsAdded = 0;
1345
+ for (const job of jobs) {
1346
+ if (!this.accepting) break;
1347
+ const index = jobsAdded;
1348
+ try {
1349
+ job.jobRef.index = this.nextAdmissionIndex;
1350
+ this.nextAdmissionIndex += 1;
1351
+ const progress = this.options.progress.queue();
1352
+ jobsAdded += 1;
1353
+ lane.remaining += 1;
1354
+ lane.items.push({
1355
+ index,
1356
+ job,
1357
+ lane
1358
+ });
1359
+ this.options.reporter?.onJobQueued?.({
1360
+ job: snapshotProgressJobRef(job.jobRef),
1361
+ progress
1362
+ });
1363
+ } catch (error) {
1364
+ this.fail(error);
1365
+ throw error;
1366
+ }
1367
+ }
1368
+ lane.outcomes.length = jobsAdded;
1369
+ if (jobsAdded === 0) this.settleLane(lane);
1370
+ else {
1371
+ this.enqueue(lane);
1372
+ this.queuePump();
1373
+ }
1374
+ return {
1375
+ jobsAdded,
1376
+ outcomes: lane.promise
1377
+ };
1378
+ }
1379
+ snapshot() {
1380
+ return this.options.progress.snapshot();
1381
+ }
1382
+ abort = () => {
1383
+ this.accepting = false;
1384
+ this.cancelQueued();
1385
+ this.settleClose();
1386
+ };
1387
+ cancel(item, from, startedAt) {
1388
+ const outcome = cancelledOutcome(item.job);
1389
+ const progress = this.options.progress.finish(from, "cancelled");
1390
+ item.lane.outcomes[item.index] = outcome;
1391
+ item.lane.remaining -= 1;
1392
+ try {
1393
+ this.options.reporter?.onJobEnd?.({
1394
+ cache: "miss",
1395
+ endedAt: this.options.clock(),
1396
+ job: snapshotProgressJobRef(item.job.jobRef),
1397
+ progress,
1398
+ startedAt,
1399
+ state: "cancelled"
1400
+ });
1401
+ } catch (error) {
1402
+ this.fail(error);
1403
+ }
1404
+ this.settleLane(item.lane);
1405
+ }
1406
+ cancelQueued() {
1407
+ if (this.cancellingQueued) return;
1408
+ this.cancellingQueued = true;
1409
+ this.readyLanes = [];
1410
+ for (const lane of this.lanes) lane.ready = false;
1411
+ try {
1412
+ for (const lane of [...this.lanes]) {
1413
+ const queued = lane.items.splice(0);
1414
+ for (const item of queued) this.cancel(item, "queued");
1415
+ }
1416
+ } finally {
1417
+ this.cancellingQueued = false;
1418
+ }
1419
+ }
1420
+ enqueue(lane) {
1421
+ if (lane.ready || lane.settled || lane.items.length === 0) return;
1422
+ lane.ready = true;
1423
+ this.readyLanes.push(lane);
1424
+ }
1425
+ fail(error) {
1426
+ this.recordFailure(error);
1427
+ this.accepting = false;
1428
+ this.cancelQueued();
1429
+ this.settleClose();
1430
+ }
1431
+ finish(item, outcome, startedAt) {
1432
+ const progress = this.options.progress.finish("running", outcome.state);
1433
+ item.lane.outcomes[item.index] = outcome;
1434
+ item.lane.remaining -= 1;
1435
+ try {
1436
+ this.options.reporter?.onJobEnd?.({
1437
+ cache: outcome.cache,
1438
+ endedAt: this.options.clock(),
1439
+ failure: outcome.failure ? snapshotFailure(outcome.failure) : void 0,
1440
+ job: snapshotProgressJobRef(item.job.jobRef),
1441
+ progress,
1442
+ startedAt,
1443
+ state: outcome.state
1444
+ });
1445
+ } catch (error) {
1446
+ this.fail(error);
1447
+ }
1448
+ this.settleLane(item.lane);
1449
+ }
1450
+ next() {
1451
+ while (this.readyLanes.length > 0) {
1452
+ const lane = this.readyLanes.shift();
1453
+ lane.ready = false;
1454
+ const item = lane.items.shift();
1455
+ this.enqueue(lane);
1456
+ if (item) return item;
1457
+ }
1458
+ }
1459
+ pump() {
1460
+ this.pumpQueued = false;
1461
+ if (this.infrastructureFailed || this.options.signal?.aborted) {
1462
+ this.cancelQueued();
1463
+ this.settleClose();
1464
+ return;
1465
+ }
1466
+ while (this.active < this.options.concurrency) {
1467
+ const item = this.next();
1468
+ if (!item) break;
1469
+ let startedAt;
1470
+ try {
1471
+ startedAt = this.options.clock();
1472
+ } catch (error) {
1473
+ this.fail(error);
1474
+ this.cancel(item, "queued");
1475
+ break;
1476
+ }
1477
+ try {
1478
+ const progress = this.options.progress.start();
1479
+ this.options.reporter?.onJobStart?.({
1480
+ job: snapshotProgressJobRef(item.job.jobRef),
1481
+ progress,
1482
+ startedAt
1483
+ });
1484
+ } catch (error) {
1485
+ this.fail(error);
1486
+ this.cancel(item, "running");
1487
+ break;
1488
+ }
1489
+ this.active += 1;
1490
+ this.run(item, startedAt);
1491
+ }
1492
+ this.settleClose();
1493
+ }
1494
+ queuePump() {
1495
+ if (this.pumpQueued) return;
1496
+ this.pumpQueued = true;
1497
+ queueMicrotask(() => this.pump());
1498
+ }
1499
+ recordFailure(error) {
1500
+ if (this.infrastructureFailed) return;
1501
+ this.infrastructureFailed = true;
1502
+ this.infrastructureError = error;
1503
+ }
1504
+ removeLane(lane) {
1505
+ const index = this.lanes.indexOf(lane);
1506
+ if (index < 0) return;
1507
+ this.lanes.splice(index, 1);
1508
+ if (lane.ready) {
1509
+ const readyIndex = this.readyLanes.indexOf(lane);
1510
+ if (readyIndex >= 0) this.readyLanes.splice(readyIndex, 1);
1511
+ lane.ready = false;
1512
+ }
1513
+ }
1514
+ async run(item, startedAt) {
1515
+ try {
1516
+ const outcome = await this.options.execute(item.job, startedAt);
1517
+ this.finish(item, outcome, startedAt);
1518
+ } catch (error) {
1519
+ this.fail(error);
1520
+ this.cancel(item, "running", startedAt);
1521
+ } finally {
1522
+ this.active -= 1;
1523
+ this.queuePump();
1524
+ this.settleClose();
1525
+ }
1526
+ }
1527
+ settleClose() {
1528
+ if (!this.closePromise || this.active !== 0 || this.lanes.length !== 0) return;
1529
+ this.options.signal?.removeEventListener("abort", this.abort);
1530
+ if (this.infrastructureFailed) this.closeReject?.(this.infrastructureError);
1531
+ else this.closeResolve?.();
1532
+ }
1533
+ settleLane(lane) {
1534
+ if (lane.settled || lane.remaining !== 0) return;
1535
+ lane.settled = true;
1536
+ this.removeLane(lane);
1537
+ lane.resolve(lane.outcomes.filter((outcome) => outcome !== void 0));
1538
+ this.settleClose();
1539
+ }
1540
+ };
1541
+ function cancelledOutcome(job) {
1542
+ return {
1543
+ cache: "miss",
1544
+ diagnostics: [],
1545
+ jobRef: snapshotProgressJobRef(job.jobRef),
1546
+ orderKey: { ...job.orderKey },
1547
+ state: "cancelled",
1548
+ usage: []
1549
+ };
1550
+ }
1551
+ function resolveRuleConcurrency(ruleConcurrency) {
1552
+ if (ruleConcurrency === void 0) return 4;
1452
1553
  if (!Number.isFinite(ruleConcurrency) || !Number.isInteger(ruleConcurrency) || ruleConcurrency <= 0) throw new TypeError("Rule execution concurrency must be a finite positive integer.");
1453
1554
  return ruleConcurrency;
1454
1555
  }
1556
+ function createLane() {
1557
+ let resolve;
1558
+ return {
1559
+ items: [],
1560
+ outcomes: [],
1561
+ promise: new Promise((resolvePromise) => {
1562
+ resolve = resolvePromise;
1563
+ }),
1564
+ ready: false,
1565
+ remaining: 0,
1566
+ resolve,
1567
+ settled: false
1568
+ };
1569
+ }
1570
+ //#endregion
1571
+ //#region src/dsl/registry.ts
1572
+ function buildRuleRegistry(config) {
1573
+ const rules = /* @__PURE__ */ new Map();
1574
+ const localIds = /* @__PURE__ */ new Map();
1575
+ const enabledRules = [];
1576
+ for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
1577
+ const id = `${alias}/${localId}`;
1578
+ if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
1579
+ rules.set(id, rule);
1580
+ localIds.set(id, localId);
1581
+ }
1582
+ for (const [id, entry] of Object.entries(config.rules)) {
1583
+ const rule = rules.get(id);
1584
+ if (!rule) throw new Error(`Unknown rule "${id}".`);
1585
+ const normalizedEntry = normalizeRuleConfigEntry(entry);
1586
+ const severity = normalizedEntry.severity;
1587
+ if (severity === "off") continue;
1588
+ enabledRules.push({
1589
+ id,
1590
+ localId: localIds.get(id) ?? id,
1591
+ options: parseRuleOptions(id, rule.options, normalizedEntry.options),
1592
+ rule,
1593
+ severity
1594
+ });
1595
+ }
1596
+ return {
1597
+ enabledRules,
1598
+ rules
1599
+ };
1600
+ }
1601
+ function formatRuleOptionIssues(index, issues) {
1602
+ return issues.map((issue) => {
1603
+ const path = getDotPath(issue);
1604
+ return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
1605
+ }).join("; ");
1606
+ }
1607
+ function isRuleConfigTuple(entry) {
1608
+ return Array.isArray(entry);
1609
+ }
1610
+ function normalizeRuleConfigEntry(entry) {
1611
+ if (isRuleConfigTuple(entry)) {
1612
+ const [severity = "warn", ...options] = entry;
1613
+ return {
1614
+ options,
1615
+ severity
1616
+ };
1617
+ }
1618
+ return {
1619
+ options: [],
1620
+ severity: entry ?? "warn"
1621
+ };
1622
+ }
1623
+ function parseRuleOption(id, index, schema, value) {
1624
+ const result = safeParse(schema, value);
1625
+ if (result.success) return result.output;
1626
+ if (value === void 0) {
1627
+ const emptyObjectResult = safeParse(schema, {});
1628
+ if (emptyObjectResult.success) return emptyObjectResult.output;
1629
+ }
1630
+ throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
1631
+ }
1632
+ function parseRuleOptions(id, schema, options) {
1633
+ if (!schema) {
1634
+ if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
1635
+ return [];
1636
+ }
1637
+ if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
1638
+ return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
1639
+ }
1640
+ //#endregion
1641
+ //#region src/core/preparation.ts
1642
+ function prepareRun(options = {}) {
1643
+ const cwd$2 = options.cwd ?? cwd();
1644
+ const config = options.config ?? [];
1645
+ const files = [];
1646
+ const directories = [];
1647
+ for (const filePath of options.files ?? []) {
1648
+ const path = resolve(cwd$2, filePath);
1649
+ const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
1650
+ if (resolvedConfig.ignored) continue;
1651
+ const effectiveConfig = resolvedConfig.config;
1652
+ const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: effectiveConfig.language });
1653
+ files.push({
1654
+ agent: effectiveConfig.agent,
1655
+ configHash: stableHash({
1656
+ language: effectiveConfig.language,
1657
+ languageOptions: effectiveConfig.languageOptions,
1658
+ processor: effectiveConfig.processor,
1659
+ resolvedLanguage: language.name,
1660
+ settings: effectiveConfig.settings
1661
+ }),
1662
+ fileIndex: files.length,
1663
+ language,
1664
+ languageOptions: effectiveConfig.languageOptions,
1665
+ path,
1666
+ rules: prepareRules(effectiveConfig),
1667
+ settings: effectiveConfig.settings
1668
+ });
1669
+ }
1670
+ for (const directoryPath of options.directories ?? []) {
1671
+ const path = resolve(cwd$2, directoryPath);
1672
+ const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
1673
+ if (resolvedConfig.ignored) continue;
1674
+ const effectiveConfig = resolvedConfig.config;
1675
+ directories.push({
1676
+ agent: effectiveConfig.agent,
1677
+ configHash: stableHash({ settings: effectiveConfig.settings }),
1678
+ directoryIndex: directories.length,
1679
+ rules: prepareRules(effectiveConfig),
1680
+ settings: effectiveConfig.settings,
1681
+ target: {
1682
+ kind: "directory",
1683
+ path
1684
+ }
1685
+ });
1686
+ }
1687
+ return {
1688
+ directories,
1689
+ files,
1690
+ project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config)
1691
+ };
1692
+ }
1693
+ function createLanguageRegistry(config) {
1694
+ const registry = createBuiltInLanguageRegistry();
1695
+ for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
1696
+ return registry;
1697
+ }
1698
+ function prepareProject(root, config) {
1699
+ const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
1700
+ if (resolvedConfig.ignored) return void 0;
1701
+ const effectiveConfig = resolvedConfig.config;
1702
+ return {
1703
+ agent: effectiveConfig.agent,
1704
+ configHash: stableHash({ settings: effectiveConfig.settings }),
1705
+ root,
1706
+ rules: prepareRules(effectiveConfig),
1707
+ settings: effectiveConfig.settings
1708
+ };
1709
+ }
1710
+ function prepareRules(config) {
1711
+ return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
1712
+ enabledRule,
1713
+ ruleIndex
1714
+ }));
1715
+ }
1716
+ //#endregion
1717
+ //#region src/core/project/index.ts
1718
+ var ProjectIndexBuilder = class {
1719
+ files = [];
1720
+ hash;
1721
+ hasher = createStableHasher();
1722
+ nextFileIndex = 0;
1723
+ pending = /* @__PURE__ */ new Map();
1724
+ root;
1725
+ targets = [];
1726
+ constructor(root) {
1727
+ this.root = root;
1728
+ this.hasher.update({ root });
1729
+ }
1730
+ add(snapshot) {
1731
+ if (this.hash !== void 0) throw new Error("Cannot add a project file after the index has been built.");
1732
+ if (!Number.isSafeInteger(snapshot.fileIndex) || snapshot.fileIndex < 0) throw new TypeError("Project file index must be a non-negative safe integer.");
1733
+ if (snapshot.fileIndex < this.nextFileIndex || this.pending.has(snapshot.fileIndex)) throw new Error(`Project file index ${snapshot.fileIndex} has already been added.`);
1734
+ this.pending.set(snapshot.fileIndex, copySnapshot(snapshot));
1735
+ this.flushContiguousSnapshots();
1736
+ }
1737
+ build() {
1738
+ if (this.pending.size > 0) throw new Error(`Cannot build project index with a gap at file index ${this.nextFileIndex}.`);
1739
+ this.hash ??= this.hasher.digest();
1740
+ return {
1741
+ hash: this.hash,
1742
+ target: {
1743
+ files: this.files.map(copyFile),
1744
+ kind: "project",
1745
+ root: this.root,
1746
+ targets: this.targets.map(copyDescriptor)
1747
+ }
1748
+ };
1749
+ }
1750
+ flushContiguousSnapshots() {
1751
+ let snapshot = this.pending.get(this.nextFileIndex);
1752
+ while (snapshot) {
1753
+ this.pending.delete(this.nextFileIndex);
1754
+ this.files.push(copyFile(snapshot.file));
1755
+ this.targets.push(...snapshot.targets.map((target) => copyDescriptor(target.descriptor)));
1756
+ this.hasher.update({
1757
+ configHash: snapshot.configHash,
1758
+ file: snapshot.file,
1759
+ targets: snapshot.targets.map((target) => ({
1760
+ descriptor: target.descriptor,
1761
+ semanticHash: target.semanticHash
1762
+ }))
1763
+ });
1764
+ this.nextFileIndex += 1;
1765
+ snapshot = this.pending.get(this.nextFileIndex);
1766
+ }
1767
+ }
1768
+ };
1769
+ function createProjectJobs(options) {
1770
+ const executions = options.runtimes.map((runtime) => projectExecution(runtime, options.project.target)).filter((execution) => execution !== void 0);
1771
+ if (executions.length === 0) return { jobs: [] };
1772
+ const owner = options.cacheStore.beginOwner({
1773
+ kind: "project",
1774
+ path: options.project.target.root
1775
+ });
1776
+ const target = {
1777
+ cacheOwner: owner,
1778
+ cacheTargetHash: options.project.hash,
1779
+ configHash: options.configHash,
1780
+ identity: "project",
1781
+ kind: "project",
1782
+ language: "project",
1783
+ text: options.project.hash
1784
+ };
1785
+ return {
1786
+ jobs: executions.map((execution) => {
1787
+ const ruleId = execution.runtime.enabledRule.id;
1788
+ return {
1789
+ execution,
1790
+ jobRef: {
1791
+ id: stableHash({
1792
+ input: options.project.target.root,
1793
+ ruleId,
1794
+ targetIdentity: "project",
1795
+ targetIndex: 0
1796
+ }),
1797
+ index: 0,
1798
+ inputPath: options.project.target.root,
1799
+ ruleId,
1800
+ target: {
1801
+ identity: "project",
1802
+ kind: "project"
1803
+ }
1804
+ },
1805
+ orderKey: {
1806
+ inputIndex: 0,
1807
+ ruleIndex: execution.runtime.ruleIndex,
1808
+ scope: "project",
1809
+ targetIndex: 0
1810
+ },
1811
+ target
1812
+ };
1813
+ }),
1814
+ owner
1815
+ };
1816
+ }
1817
+ function copyDescriptor(descriptor) {
1818
+ return {
1819
+ filePath: descriptor.filePath,
1820
+ identity: descriptor.identity,
1821
+ kind: descriptor.kind,
1822
+ name: descriptor.name,
1823
+ range: descriptor.range && { ...descriptor.range }
1824
+ };
1825
+ }
1826
+ function copyFile(file) {
1827
+ return {
1828
+ contentHash: file.contentHash,
1829
+ language: file.language,
1830
+ path: file.path,
1831
+ targetCount: file.targetCount
1832
+ };
1833
+ }
1834
+ function copySnapshot(snapshot) {
1835
+ return {
1836
+ configHash: snapshot.configHash,
1837
+ file: copyFile(snapshot.file),
1838
+ fileIndex: snapshot.fileIndex,
1839
+ targets: snapshot.targets.map(copyTargetSnapshot)
1840
+ };
1841
+ }
1842
+ function copyTargetSnapshot(target) {
1843
+ return {
1844
+ descriptor: copyDescriptor(target.descriptor),
1845
+ semanticHash: target.semanticHash
1846
+ };
1847
+ }
1848
+ function projectExecution(runtime, target) {
1849
+ if (runtime.handlers.onTargetWith) return {
1850
+ run: () => runtime.handlers.onTargetWith?.(target),
1851
+ runtime
1852
+ };
1853
+ if (runtime.handlers.onTargetProject) return {
1854
+ run: () => runtime.handlers.onTargetProject?.(target),
1855
+ runtime
1856
+ };
1857
+ }
1858
+ async function executeSourceSession(input, options) {
1859
+ openSession(options.metrics);
1860
+ try {
1861
+ if (options.signal?.aborted) return { outcomes: [] };
1862
+ let file;
1863
+ try {
1864
+ file = await options.src.readFile(input.path);
1865
+ } catch (error) {
1866
+ return {
1867
+ failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
1868
+ outcomes: []
1869
+ };
1870
+ }
1871
+ if (options.signal?.aborted) return { outcomes: [] };
1872
+ let targets;
1873
+ try {
1874
+ targets = await input.language.extract(file, {
1875
+ cwd: options.cwd,
1876
+ languageOptions: input.languageOptions,
1877
+ src: options.src
1878
+ });
1879
+ } catch (error) {
1880
+ return {
1881
+ failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
1882
+ outcomes: []
1883
+ };
1884
+ }
1885
+ const contentHash = hashText(file.text);
1886
+ const project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
1887
+ const cacheOwner = options.cacheStore.beginOwner({
1888
+ kind: "file",
1889
+ path: file.path
1890
+ });
1891
+ const jobs = createSourceJobs(input, createExecutionTargets(input, targets, options.ruleRuntimes, options.cwd, cacheOwner));
1892
+ if (options.signal?.aborted) {
1893
+ cacheOwner.commit({
1894
+ contentHash,
1895
+ mode: "merge"
1896
+ });
1897
+ return {
1898
+ outcomes: [],
1899
+ project
1900
+ };
1901
+ }
1902
+ const batch = options.scheduler.schedule(jobs);
1903
+ try {
1904
+ options.progress?.onFileReady?.({
1905
+ fileIndex: input.fileIndex,
1906
+ inputPath: input.path,
1907
+ jobsAdded: batch.jobsAdded,
1908
+ progress: options.scheduler.snapshot()
1909
+ });
1910
+ } catch (error) {
1911
+ options.scheduler.cancelWithError(error);
1912
+ throw error;
1913
+ }
1914
+ const outcomes = await batch.outcomes;
1915
+ cacheOwner.commit(options.signal?.aborted ? {
1916
+ contentHash,
1917
+ mode: "merge"
1918
+ } : { contentHash });
1919
+ return {
1920
+ outcomes,
1921
+ project
1922
+ };
1923
+ } finally {
1924
+ closeSession(options.metrics);
1925
+ }
1926
+ }
1927
+ async function executeSourceSessions(inputs, options) {
1928
+ const results = new Array(inputs.length);
1929
+ let cursor = 0;
1930
+ let firstError;
1931
+ let failed = false;
1932
+ let stopping = false;
1933
+ const workerCount = Math.min(options.sourceWindow, inputs.length);
1934
+ const workers = Array.from({ length: workerCount }, async () => {
1935
+ while (!stopping && !options.signal?.aborted) {
1936
+ const index = cursor;
1937
+ cursor += 1;
1938
+ const input = inputs[index];
1939
+ if (!input) return;
1940
+ try {
1941
+ results[index] = await executeSourceSession(input, {
1942
+ ...options,
1943
+ ruleRuntimes: options.createRuleRuntimes(input)
1944
+ });
1945
+ } catch (error) {
1946
+ stopping = true;
1947
+ if (!failed) {
1948
+ failed = true;
1949
+ firstError = error;
1950
+ }
1951
+ }
1952
+ }
1953
+ });
1954
+ await Promise.all(workers);
1955
+ if (failed) throw firstError;
1956
+ return results.filter((result) => result !== void 0);
1957
+ }
1958
+ function resolveSourceWindow(ruleConcurrency) {
1959
+ return Math.max(1, Math.min(ruleConcurrency, 4));
1960
+ }
1961
+ function closeSession(metrics) {
1962
+ if (!metrics) return;
1963
+ metrics.active -= 1;
1964
+ metrics.closed += 1;
1965
+ }
1966
+ function createExecutionTargets(input, targets, runtimes, cwd, cacheOwner) {
1967
+ const resolveIdentity = createTargetIdentityResolver(targets.map((target) => ({
1968
+ filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
1969
+ identity: target.identity,
1970
+ kind: target.kind,
1971
+ name: target.name,
1972
+ range: target.range
1973
+ })));
1974
+ return targets.map((target, targetIndex) => {
1975
+ return {
1976
+ executions: runtimes.map((runtime) => sourceExecution(runtime, target)).filter((execution) => execution !== void 0),
1977
+ target: {
1978
+ activeFilePath: input.path,
1979
+ cacheOwner,
1980
+ configHash: input.configHash,
1981
+ identity: resolveIdentity({
1982
+ filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
1983
+ identity: target.identity,
1984
+ kind: target.kind,
1985
+ name: target.name,
1986
+ range: target.range
1987
+ }, targetIndex),
1988
+ kind: target.kind,
1989
+ language: target.language,
1990
+ loc: target.loc,
1991
+ metadata: target.metadata,
1992
+ name: target.name,
1993
+ origin: target.origin,
1994
+ range: target.range,
1995
+ text: target.text
1996
+ },
1997
+ targetIndex
1998
+ };
1999
+ }).filter((target) => target.executions.length > 0);
2000
+ }
2001
+ function createProjectSnapshot(input, language, path, contentHash, targets) {
2002
+ return {
2003
+ configHash: input.configHash,
2004
+ file: {
2005
+ contentHash,
2006
+ language,
2007
+ path,
2008
+ targetCount: targets.length
2009
+ },
2010
+ fileIndex: input.fileIndex,
2011
+ targets: targets.map((target) => ({
2012
+ descriptor: {
2013
+ filePath: target.file.path,
2014
+ identity: target.identity,
2015
+ kind: target.kind,
2016
+ ...target.name === void 0 ? {} : { name: target.name },
2017
+ ...target.range === void 0 ? {} : { range: { ...target.range } }
2018
+ },
2019
+ semanticHash: stableHash({
2020
+ language: target.language,
2021
+ loc: target.loc,
2022
+ metadata: target.metadata,
2023
+ name: target.name,
2024
+ origin: target.origin,
2025
+ range: target.range,
2026
+ text: target.text
2027
+ })
2028
+ }))
2029
+ };
2030
+ }
2031
+ function createSourceJobs(input, targets) {
2032
+ return targets.flatMap(({ executions, target, targetIndex }) => executions.map((execution) => {
2033
+ const ruleId = execution.runtime.enabledRule.id;
2034
+ return {
2035
+ execution,
2036
+ jobRef: {
2037
+ id: stableHash({
2038
+ fileIndex: input.fileIndex,
2039
+ input: input.path,
2040
+ ruleId,
2041
+ targetIdentity: target.identity,
2042
+ targetIndex
2043
+ }),
2044
+ index: 0,
2045
+ inputPath: input.path,
2046
+ ruleId,
2047
+ target: {
2048
+ identity: target.identity,
2049
+ kind: target.kind,
2050
+ name: target.name
2051
+ }
2052
+ },
2053
+ orderKey: {
2054
+ inputIndex: input.fileIndex,
2055
+ ruleIndex: execution.runtime.ruleIndex,
2056
+ scope: "source",
2057
+ targetIndex
2058
+ },
2059
+ target
2060
+ };
2061
+ }));
2062
+ }
2063
+ function failureMessage(error, fallback) {
2064
+ try {
2065
+ return errorMessageFrom(error) ?? fallback;
2066
+ } catch {
2067
+ return fallback;
2068
+ }
2069
+ }
2070
+ function fileFailure(input, kind, message) {
2071
+ return {
2072
+ file: {
2073
+ index: input.fileIndex,
2074
+ path: input.path
2075
+ },
2076
+ kind,
2077
+ message
2078
+ };
2079
+ }
2080
+ function openSession(metrics) {
2081
+ if (!metrics) return;
2082
+ metrics.active += 1;
2083
+ metrics.opened += 1;
2084
+ metrics.maximumActive = Math.max(metrics.maximumActive, metrics.active);
2085
+ }
2086
+ function sourceExecution(runtime, target) {
2087
+ if (runtime.handlers.onTargetWith) return {
2088
+ run: () => runtime.handlers.onTargetWith?.(target),
2089
+ runtime
2090
+ };
2091
+ if (target.kind === "class" && runtime.handlers.onTargetClass) return {
2092
+ run: () => runtime.handlers.onTargetClass?.(target),
2093
+ runtime
2094
+ };
2095
+ if (target.kind === "file" && runtime.handlers.onTargetFile) return {
2096
+ run: () => runtime.handlers.onTargetFile?.(target),
2097
+ runtime
2098
+ };
2099
+ if (target.kind === "function" && runtime.handlers.onTargetFunction) return {
2100
+ run: () => runtime.handlers.onTargetFunction?.(target),
2101
+ runtime
2102
+ };
2103
+ }
2104
+ //#endregion
2105
+ //#region src/core/targets/directory.ts
2106
+ function createDirectoryJobs(input, runtimes) {
2107
+ const target = input.target;
2108
+ return runtimes.flatMap((runtime) => {
2109
+ const execution = directoryExecution(runtime, target);
2110
+ if (!execution) return [];
2111
+ const ruleId = runtime.enabledRule.id;
2112
+ return [{
2113
+ execution,
2114
+ jobRef: {
2115
+ id: stableHash({
2116
+ directoryIndex: input.directoryIndex,
2117
+ input: target.path,
2118
+ ruleId,
2119
+ targetIdentity: target.path
2120
+ }),
2121
+ index: 0,
2122
+ inputPath: target.path,
2123
+ ruleId,
2124
+ target: {
2125
+ identity: target.path,
2126
+ kind: "directory"
2127
+ }
2128
+ },
2129
+ orderKey: {
2130
+ inputIndex: input.directoryIndex,
2131
+ ruleIndex: runtime.ruleIndex,
2132
+ scope: "directory",
2133
+ targetIndex: 0
2134
+ },
2135
+ target: {
2136
+ activeFilePath: target.path,
2137
+ configHash: input.configHash,
2138
+ identity: target.path,
2139
+ kind: "directory",
2140
+ language: "directory",
2141
+ text: target.path
2142
+ }
2143
+ }];
2144
+ });
2145
+ }
2146
+ function directoryExecution(runtime, target) {
2147
+ if (runtime.handlers.onTargetWith) return {
2148
+ run: () => runtime.handlers.onTargetWith?.(target),
2149
+ runtime
2150
+ };
2151
+ if (runtime.handlers.onTargetDirectory) return {
2152
+ run: () => runtime.handlers.onTargetDirectory?.(target),
2153
+ runtime
2154
+ };
2155
+ }
2156
+ //#endregion
2157
+ //#region src/core/run.ts
2158
+ var AlintRunCancelledError = class extends Error {
2159
+ result;
2160
+ constructor(result, cause) {
2161
+ super("Alint run cancelled.", { cause });
2162
+ this.name = "AlintRunCancelledError";
2163
+ this.result = result;
2164
+ }
2165
+ };
2166
+ /** Cancellation error carrying the partial result gathered before the abort. */
2167
+ var AlintAbortError = class extends AlintRunCancelledError {
2168
+ constructor(result, options = {}) {
2169
+ super(result, options.cause);
2170
+ this.name = "AlintAbortError";
2171
+ }
2172
+ };
2173
+ var AlintRunError = class extends Error {
2174
+ failures;
2175
+ result;
2176
+ constructor(message, result, options = {}) {
2177
+ super(message, { cause: options.cause });
2178
+ this.name = "AlintRunError";
2179
+ this.failures = options.failures ?? [];
2180
+ this.result = result;
2181
+ }
2182
+ };
2183
+ async function runAlint(options = {}) {
2184
+ const clock = Date.now;
2185
+ const timeoutMs = resolveRuleExecutionTimeout(options.runner?.timeoutMs);
2186
+ const concurrency = resolveRuleConcurrency(options.runner?.ruleConcurrency);
2187
+ const prepareStartedAt = clock();
2188
+ options.progress?.onPrepareStart?.({ startedAt: prepareStartedAt });
2189
+ const preparation = prepareRun(options);
2190
+ options.progress?.onPrepareEnd?.({
2191
+ endedAt: clock(),
2192
+ filesTotal: preparation.files.length,
2193
+ startedAt: prepareStartedAt
2194
+ });
2195
+ const runProgress = createRunProgress(preparation.files.length);
2196
+ const runStartedAt = clock();
2197
+ options.progress?.onExecuteStart?.({
2198
+ progress: runProgress.snapshot(),
2199
+ startedAt: runStartedAt
2200
+ });
2201
+ const cwd$1 = options.cwd ?? cwd();
2202
+ const setupConfig = options.setupConfig ?? {
2203
+ providers: [],
2204
+ version: 1
2205
+ };
2206
+ const src = createSourceRuntime();
2207
+ const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
2208
+ const cacheStore = await createCacheStore({
2209
+ cwd: cwd$1,
2210
+ enabled: normalizedCacheConfig.enabled,
2211
+ location: normalizedCacheConfig.location,
2212
+ readOnly: options.cacheOnly
2213
+ });
2214
+ const cacheContext = { modelHash: stableHash({
2215
+ modelOverride: options.modelOverride,
2216
+ outputLanguage: options.outputLanguage,
2217
+ setupConfig
2218
+ }) };
2219
+ const createRuntimes = (input) => createRuleRuntimes({
2220
+ cwd: cwd$1,
2221
+ effectiveAgent: input.agent,
2222
+ effectiveSettings: input.settings,
2223
+ progress: options.progress,
2224
+ rules: input.rules,
2225
+ runOptions: options,
2226
+ runProgress,
2227
+ setupConfig,
2228
+ src
2229
+ });
2230
+ const scheduler = new RuleScheduler({
2231
+ clock,
2232
+ concurrency,
2233
+ execute: (job, _startedAt) => executeRuleJob(job, {
2234
+ cache: cacheContext,
2235
+ cacheOnly: options.cacheOnly,
2236
+ progress: options.progress,
2237
+ runProgress,
2238
+ runSignal: options.signal,
2239
+ timeoutMs
2240
+ }),
2241
+ progress: runProgress,
2242
+ reporter: options.progress,
2243
+ signal: options.signal
2244
+ });
2245
+ const outcomes = [];
2246
+ const fileFailures = [];
2247
+ let infrastructureError;
2248
+ let infrastructureFailed = false;
2249
+ let admissionFailed = false;
2250
+ try {
2251
+ const projectRuntimes = preparation.project ? createRuntimes(preparation.project) : [];
2252
+ const projectBuilder = preparation.project && projectRuntimes.some((runtime) => runtime.handlers.onTargetWith || runtime.handlers.onTargetProject) ? new ProjectIndexBuilder(preparation.project.root) : void 0;
2253
+ const sourceResults = await executeSourceSessions(preparation.files, {
2254
+ cacheStore,
2255
+ createRuleRuntimes: createRuntimes,
2256
+ cwd: cwd$1,
2257
+ progress: options.progress,
2258
+ projectSnapshots: projectBuilder !== void 0,
2259
+ scheduler,
2260
+ signal: options.signal,
2261
+ sourceWindow: resolveSourceWindow(concurrency),
2262
+ src
2263
+ });
2264
+ for (const result of sourceResults) {
2265
+ outcomes.push(...result.outcomes);
2266
+ if (result.failure) fileFailures.push(result.failure);
2267
+ }
2268
+ if (projectBuilder) {
2269
+ for (const result of [...sourceResults].sort((left, right) => (left.project?.fileIndex ?? Number.MAX_SAFE_INTEGER) - (right.project?.fileIndex ?? Number.MAX_SAFE_INTEGER))) if (result.project) projectBuilder.add(result.project);
2270
+ }
2271
+ const directoryBatches = preparation.directories.map((input) => {
2272
+ return scheduler.schedule(createDirectoryJobs(input, createRuntimes(input)));
2273
+ });
2274
+ outcomes.push(...(await Promise.all(directoryBatches.map((batch) => batch.outcomes))).flat());
2275
+ if (fileFailures.length === 0 && preparation.project && !options.signal?.aborted) {
2276
+ if (projectBuilder) {
2277
+ const project = createProjectJobs({
2278
+ cacheStore,
2279
+ configHash: preparation.project.configHash,
2280
+ project: projectBuilder.build(),
2281
+ runtimes: projectRuntimes
2282
+ });
2283
+ const projectOwner = project.owner;
2284
+ const batch = scheduler.schedule(project.jobs);
2285
+ outcomes.push(...await batch.outcomes);
2286
+ projectOwner?.commit({ mode: options.signal?.aborted ? "merge" : "replace" });
2287
+ } else if (!options.cacheOnly) cacheStore.beginOwner({
2288
+ kind: "project",
2289
+ path: preparation.project.root
2290
+ }).commit({ mode: "replace" });
2291
+ }
2292
+ } catch (error) {
2293
+ scheduler.cancelWithError(error);
2294
+ infrastructureError = error;
2295
+ infrastructureFailed = true;
2296
+ admissionFailed = true;
2297
+ } finally {
2298
+ try {
2299
+ await scheduler.close();
2300
+ } catch (error) {
2301
+ infrastructureError ??= error;
2302
+ infrastructureFailed = true;
2303
+ }
2304
+ if (!admissionFailed && !options.cacheOnly) try {
2305
+ await cacheStore.reconcile();
2306
+ } catch {}
2307
+ }
2308
+ runProgress.finalize();
2309
+ if (admissionFailed) throw infrastructureError;
2310
+ outcomes.sort((left, right) => compareJobOrder(left.orderKey, right.orderKey));
2311
+ const ruleFailures = outcomes.flatMap((outcome) => outcome.state === "failed" ? [outcome.failure] : []);
2312
+ const failures = [...fileFailures.sort((left, right) => left.file.index - right.file.index), ...ruleFailures];
2313
+ const result = {
2314
+ diagnostics: outcomes.flatMap((outcome) => outcome.diagnostics),
2315
+ execution: executionCounts(outcomes),
2316
+ usage: runUsage(outcomes)
2317
+ };
2318
+ options.progress?.onExecuteEnd?.({
2319
+ endedAt: clock(),
2320
+ progress: runProgress.snapshot()
2321
+ });
2322
+ options.progress?.onRunEnd?.({
2323
+ diagnostics: snapshotDiagnostics(result.diagnostics),
2324
+ endedAt: clock(),
2325
+ execution: { ...result.execution },
2326
+ progress: runProgress.snapshot(),
2327
+ startedAt: runStartedAt,
2328
+ usage: snapshotRunUsage(result.usage)
2329
+ });
2330
+ if (infrastructureFailed) throw infrastructureError;
2331
+ if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
2332
+ 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, {
2333
+ cause: new AggregateError(failures.map((failure) => new Error(failure.message)), "Alint execution failures."),
2334
+ failures
2335
+ });
2336
+ return result;
2337
+ }
2338
+ function executionCounts(outcomes) {
2339
+ const counts = {
2340
+ cached: 0,
2341
+ cancelled: 0,
2342
+ completed: 0,
2343
+ failed: 0,
2344
+ planned: outcomes.length,
2345
+ queued: 0,
2346
+ running: 0,
2347
+ skipped: 0
2348
+ };
2349
+ for (const outcome of outcomes) counts[outcome.state] += 1;
2350
+ return counts;
2351
+ }
1455
2352
  function runUsage(outcomes) {
1456
2353
  const live = [];
1457
2354
  const cached = [];
1458
- for (const outcome of outcomes) (outcome.state === "cached" ? cached : live).push(...outcome.bucket.usage);
2355
+ for (const outcome of outcomes) (outcome.state === "cached" ? cached : live).push(...outcome.usage);
1459
2356
  return {
1460
2357
  ...usageTotals(live),
1461
2358
  ...cached.length > 0 ? { cached: usageTotals(cached) } : {}
1462
2359
  };
1463
2360
  }
1464
- function toDiagnosticModel(model, request) {
2361
+ function snapshotRunUsage(usage) {
1465
2362
  return {
1466
- providerId: model.provider.id,
1467
- requested: request,
1468
- resolvedId: model.id
2363
+ ...usage,
2364
+ ...usage.cached ? { cached: {
2365
+ ...usage.cached,
2366
+ records: snapshotUsageRecords(usage.cached.records)
2367
+ } } : {},
2368
+ records: snapshotUsageRecords(usage.records)
1469
2369
  };
1470
2370
  }
1471
2371
  function usageTotals(records) {