@alint-js/core 0.1.0 → 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,257 +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 canonicalCandidates = candidates.filter((candidate) => canonicalIdentity(candidate) === request);
400
- if (canonicalCandidates.length > 1) throw duplicateDefinitionError(request);
401
- let candidate = canonicalCandidates[0];
402
- if (candidate === void 0) {
403
- const matchingCandidates = candidates.filter((candidate) => matchesRequest(candidate, request));
404
- const matchesByCanonical = /* @__PURE__ */ new Map();
405
- for (const matchingCandidate of matchingCandidates) {
406
- const identity = canonicalIdentity(matchingCandidate);
407
- const matches = matchesByCanonical.get(identity) ?? [];
408
- matches.push(matchingCandidate);
409
- matchesByCanonical.set(identity, matches);
410
- }
411
- const duplicateIdentity = [...matchesByCanonical].find(([, matches]) => matches.length > 1)?.[0];
412
- if (duplicateIdentity !== void 0) throw duplicateDefinitionError(duplicateIdentity);
413
- if (matchingCandidates.length === 0) throw new Error(`Unknown model "${request}".`);
414
- if (matchingCandidates.length > 1) {
415
- const choices = [...matchesByCanonical.keys()];
416
- throw new Error(`Ambiguous model "${request}".\nSpecify a provider-qualified model:\n${choices.map((choice) => ` ${choice}`).join("\n")}`);
417
- }
418
- candidate = matchingCandidates[0];
419
- }
420
- if (!satisfiesHardRequirements(candidate.model, options.requirement)) throw new Error(`Model "${request}" does not satisfy requirement for rule "${ruleId}".`);
421
- return toResolvedModel(candidate, options.requirement);
422
- }
423
- const candidate = preferSize(candidates.filter(({ model }) => satisfiesHardRequirements(model, options.requirement)), options.requirement);
424
- if (candidate === void 0) throw new Error(`No model satisfies requirement for rule "${ruleId}".`);
425
- return toResolvedModel(candidate, options.requirement);
426
- }
427
- function canonicalIdentity({ model, provider }) {
428
- return `${provider.id}/${model.id}`;
429
- }
430
- function duplicateDefinitionError(identity) {
431
- return /* @__PURE__ */ new Error(`Model "${identity}" is configured more than once.\nRemove duplicate provider/model definitions from the setup configuration.`);
432
- }
433
- function flattenModels(registry) {
434
- return registry.providers.flatMap((provider) => provider.models.map((model) => ({
435
- model,
436
- provider
437
- })));
438
- }
439
- function matchesRequest(candidate, request) {
440
- return [
441
- candidate.model.id,
442
- candidate.model.name,
443
- ...candidate.model.aliases ?? []
444
- ].filter((name) => name !== void 0).some((name) => name === request || `${candidate.provider.id}/${name}` === request);
445
- }
446
- function preferSize(candidates, requirement) {
447
- if (requirement?.size === void 0) return candidates[0];
448
- return candidates.find(({ model }) => model.size === requirement.size) ?? candidates[0];
449
- }
450
- function satisfiesHardRequirements(model, requirement) {
451
- if (requirement === void 0) return true;
452
- if (requirement.capabilities !== void 0 && !requirement.capabilities.every((capability) => (model.capabilities ?? []).includes(capability))) return false;
453
- if (requirement.minContextWindow !== void 0 && (model.contextWindow === void 0 || model.contextWindow < requirement.minContextWindow)) return false;
454
- return true;
455
- }
456
- function toResolvedModel(candidate, requirement) {
457
- const { model, provider } = candidate;
458
- return {
459
- aliases: [...model.aliases ?? []],
460
- capabilities: [...model.capabilities ?? []],
461
- contextWindow: model.contextWindow,
462
- id: model.id,
463
- name: model.name ?? model.id,
464
- params: {
465
- ...model.defaultParams ?? {},
466
- ...requirement?.params ?? {}
467
- },
468
- provider: {
469
- endpoint: provider.endpoint,
470
- headers: { ...provider.headers ?? {} },
471
- id: provider.id,
472
- type: provider.type
473
- },
474
- size: model.size
475
- };
476
- }
362
+ //#region src/core/cache/types.ts
363
+ const CACHE_MAGIC = "ALINT_CACHE";
477
364
  //#endregion
478
- //#region src/core/cache.ts
479
- const CACHE_SCHEMA_VERSION = 1;
365
+ //#region src/core/cache/store.ts
480
366
  const DEFAULT_CACHE_FILE_NAME = ".alintcache";
481
- const CacheFileSchema = pipe(object({
482
- createdAt: pipe(string(), description("Cache file creation timestamp.")),
483
- entries: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
484
- diagnostics: pipe(array(object({
485
- filePath: pipe(string(), description("Diagnostic file path.")),
486
- message: pipe(string(), description("Diagnostic message.")),
487
- ruleId: pipe(string(), description("Diagnostic rule id.")),
488
- severity: pipe(union([literal("error"), literal("warn")]), description("Diagnostic severity."))
489
- })), description("Diagnostics produced for the cached target.")),
490
- filePath: pipe(string(), description("Cache entry file path.")),
491
- fingerprint: pipe(object({
492
- alintVersion: pipe(string(), description("Alint version used to create the cache entry.")),
493
- configHash: pipe(string(), description("Runner config hash used to create the cache entry.")),
494
- modelHash: pipe(string(), description("Model configuration hash used to create the cache entry.")),
495
- ruleHash: pipe(string(), description("Rule configuration hash used to create the cache entry."))
496
- }), description("Cache entry fingerprint.")),
497
- target: pipe(object({
498
- hash: pipe(string(), description("Cached target hash.")),
499
- identity: pipe(string(), description("Stable cached target identity.")),
500
- kind: pipe(string(), description("Cached target kind."))
501
- }), description("Cached target metadata.")),
502
- usage: pipe(array(object({
503
- inputTokens: pipe(optional(number()), description("Optional input token count.")),
504
- modelId: pipe(string(), description("Usage model id.")),
505
- outputTokens: pipe(optional(number()), description("Optional output token count.")),
506
- providerId: pipe(string(), description("Usage provider id.")),
507
- ruleId: pipe(string(), description("Usage rule id.")),
508
- totalTokens: pipe(optional(number()), description("Optional total token count."))
509
- })), description("Inference usage records for the cache entry."))
510
- })), description("Cache entries keyed by cache key.")),
511
- files: pipe(unknown(), check((value) => typeof value === "object" && value !== null && !Array.isArray(value)), record(string(), object({
512
- contentHash: pipe(string(), description("Cached file content hash.")),
513
- entries: pipe(array(string()), description("Cache entry keys associated with the file.")),
514
- path: pipe(string(), description("Normalized cached file path."))
515
- })), description("Cached files keyed by normalized file path.")),
516
- schemaVersion: pipe(literal(CACHE_SCHEMA_VERSION), description("Cache schema version.")),
517
- updatedAt: pipe(string(), description("Cache file update timestamp."))
518
- }), description("Alint cache file."));
519
- function createCacheKey(input) {
520
- return stableHash(input);
521
- }
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
+ }));
522
445
  async function createCacheStore(options) {
523
446
  const location = resolveCacheLocation(options.cwd, options.location);
524
447
  if (!options.enabled) return createNoopCacheStore(location);
525
- 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;
526
453
  return {
527
- get: (key) => cacheFile.entries[key],
454
+ beginOwner: (owner) => beginOwner(body, owner, options.cwd),
528
455
  location,
529
- markFile: (filePath, contentHash, entries) => {
530
- const path = normalizeCachePath(options.cwd, filePath);
531
- cacheFile.files[path] = {
532
- contentHash,
533
- entries,
534
- path
535
- };
536
- },
537
456
  reconcile: async () => {
538
- await mkdir(dirname(location), { recursive: true });
539
- cacheFile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
540
- const tempPath = join(dirname(location), `.${DEFAULT_CACHE_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`);
541
- await writeFile(tempPath, `${JSON.stringify(cacheFile, null, 2)}\n`);
542
- await rename(tempPath, location);
543
- },
544
- set: (key, entry) => {
545
- 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);
546
460
  }
547
461
  };
548
462
  }
549
463
  function createTargetIdentityResolver(targets) {
550
464
  const baseCounts = /* @__PURE__ */ new Map();
465
+ const duplicateCandidateCounts = /* @__PURE__ */ new Map();
551
466
  for (const target of targets) {
552
467
  const base = createBaseTargetIdentity(target);
553
468
  baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
554
469
  }
555
- 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) => {
556
477
  const base = createBaseTargetIdentity(target);
557
478
  if ((baseCounts.get(base) ?? 0) <= 1) return base;
558
- if (target.range) return `${base}:${target.range.start}:${target.range.end}`;
559
- 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}`;
560
482
  };
561
483
  }
562
- function hashText(text) {
563
- return createHash("sha256").update(text).digest("hex");
564
- }
565
484
  function normalizeCachePath(cwd, filePath) {
566
- 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("/") || ".";
567
486
  }
568
487
  function normalizeRunnerCacheConfig(cache, cwd) {
569
488
  if (cache === false) return {
@@ -584,12 +503,50 @@ function resolveCacheLocation(cwd, location) {
584
503
  const resolved = isAbsolute$1(location) ? resolve$1(location) : resolve$1(cwd, location);
585
504
  if (location.endsWith("/") || location.endsWith("\\")) return join(resolved, DEFAULT_CACHE_FILE_NAME);
586
505
  try {
587
- if (statSyncIsDirectory(resolved)) return join(resolved, DEFAULT_CACHE_FILE_NAME);
506
+ if (statSync(resolved).isDirectory()) return join(resolved, DEFAULT_CACHE_FILE_NAME);
588
507
  } catch {}
589
508
  return resolved;
590
509
  }
591
- function stableHash(value) {
592
- 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
+ }
593
550
  }
594
551
  function createBaseTargetIdentity(target) {
595
552
  if (target.identity && (target.kind !== "file" || target.identity !== "file")) return target.filePath ? `${target.kind}:${target.filePath}:${target.identity}` : `${target.kind}:${target.identity}`;
@@ -598,53 +555,216 @@ function createBaseTargetIdentity(target) {
598
555
  if (target.range) return `${target.kind}:${target.range.start}:${target.range.end}`;
599
556
  return target.kind;
600
557
  }
601
- function createEmptyCacheFile() {
558
+ function createEmptyCacheBody() {
602
559
  const now = (/* @__PURE__ */ new Date()).toISOString();
603
560
  return {
604
561
  createdAt: now,
605
562
  entries: {},
606
- files: {},
607
- schemaVersion: CACHE_SCHEMA_VERSION,
563
+ owners: {},
608
564
  updatedAt: now
609
565
  };
610
566
  }
611
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
+ });
612
590
  return {
613
- get: () => void 0,
591
+ beginOwner: transaction,
614
592
  location,
615
- markFile: () => {},
616
- reconcile: async () => {},
617
- set: () => {}
593
+ reconcile: async () => {}
618
594
  };
619
595
  }
620
- function isCacheFile(value) {
621
- 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;
622
637
  }
623
- function isRecord(value) {
624
- return typeof value === "object" && value !== null && !Array.isArray(value);
638
+ function isJsonValue(input) {
639
+ return isJsonValueAt(input, /* @__PURE__ */ new WeakSet());
625
640
  }
626
- 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);
627
647
  try {
628
- const parsed = JSON.parse(await readFile(location, "utf8"));
629
- if (isCacheFile(parsed)) return parsed;
630
- } catch {}
631
- 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
+ }
632
659
  }
633
- function stableStringify(value) {
634
- if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
635
- if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => {
636
- const property = value[key];
637
- if (property === void 0) return;
638
- return `${JSON.stringify(key)}:${stableStringify(property)}`;
639
- }).filter((entry) => entry !== void 0).join(",")}}`;
640
- 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);
641
703
  }
642
- function statSyncIsDirectory(path) {
643
- return statSync(path).isDirectory();
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
+ }
715
+ }
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
+ });
644
727
  }
645
728
  //#endregion
646
- //#region package.json
647
- var version = "0.1.0";
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
+ }
648
768
  //#endregion
649
769
  //#region src/core/execution/job.ts
650
770
  var CacheReplayProcessingError = class {
@@ -653,76 +773,43 @@ var CacheReplayProcessingError = class {
653
773
  this.cause = cause;
654
774
  }
655
775
  };
656
- function createRuleJobs(plans) {
657
- const jobs = [];
658
- const total = plans.reduce((sum, plan) => sum + plan.planned, 0);
659
- const ruleTotals = /* @__PURE__ */ new Map();
660
- const ruleIndexes = /* @__PURE__ */ new Map();
661
- for (const plan of plans) for (const target of plan.targets) for (const execution of target.executions) {
662
- const ruleId = execution.runtime.enabledRule.id;
663
- ruleTotals.set(ruleId, (ruleTotals.get(ruleId) ?? 0) + 1);
664
- }
665
- for (const plan of plans) for (const target of plan.targets) for (const execution of target.executions) {
666
- const ruleId = execution.runtime.enabledRule.id;
667
- const index = jobs.length + 1;
668
- const ruleIndex = (ruleIndexes.get(ruleId) ?? 0) + 1;
669
- ruleIndexes.set(ruleId, ruleIndex);
670
- jobs.push({
671
- execution,
672
- job: {
673
- id: stableHash({
674
- index,
675
- planId: plan.id,
676
- ruleId,
677
- targetIdentity: target.identity
678
- }),
679
- index,
680
- inputPath: plan.path,
681
- ruleId,
682
- ruleIndex,
683
- ruleTotal: ruleTotals.get(ruleId) ?? 0,
684
- target: {
685
- identity: target.identity,
686
- kind: target.kind,
687
- name: target.name
688
- },
689
- total
690
- },
691
- target
692
- });
693
- }
694
- 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;
695
778
  }
696
779
  async function executeRuleJob(job, options) {
697
780
  const bucket = {
698
781
  diagnostics: [],
699
782
  usage: []
700
783
  };
701
- if (options.runSignal?.aborted) return finish(job, options, bucket, {
784
+ if (options.runSignal?.aborted) return finish(job, bucket, {
702
785
  cache: "miss",
703
786
  state: "cancelled"
704
787
  });
705
- const cacheKey = createExecutionCacheKey(job, options.cache);
706
- const cachedEntry = cacheKey ? options.cache.store.get(cacheKey) : void 0;
707
- if (cacheKey && cachedEntry) {
708
- 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) {
709
796
  try {
710
- replayCachedEntry(cachedEntry, bucket, job.job, options.progress);
797
+ replayCachedEntry(cachedEntry, bucket, job.jobRef, options.progress, options.runProgress);
711
798
  } catch (error) {
712
799
  if (!(error instanceof CacheReplayProcessingError)) throw error;
713
- return finish(job, options, bucket, {
800
+ job.target.cacheOwner?.discard(slot);
801
+ return finish(job, bucket, {
714
802
  cache: "hit",
715
- cause: error.cause,
716
803
  failure: failure(error.cause, job, "cache-replay"),
717
804
  state: "failed"
718
805
  });
719
806
  }
720
- return finish(job, options, bucket, {
807
+ return finish(job, bucket, {
721
808
  cache: "hit",
722
809
  state: "cached"
723
810
  });
724
811
  }
725
- if (options.cacheOnly) return finish(job, options, bucket, {
812
+ if (options.cacheOnly) return finish(job, bucket, {
726
813
  cache: "miss",
727
814
  state: "skipped"
728
815
  });
@@ -740,8 +827,10 @@ async function executeRuleJob(job, options) {
740
827
  const state = {
741
828
  activeFilePath: job.target.activeFilePath,
742
829
  bucket,
743
- job: job.job,
830
+ jobRef: job.jobRef,
744
831
  reporterFailed: false,
832
+ runProgress: options.runProgress,
833
+ sealed: false,
745
834
  signal: controller.signal
746
835
  };
747
836
  let handlerCause;
@@ -753,31 +842,29 @@ async function executeRuleJob(job, options) {
753
842
  handlerCause = cause;
754
843
  handlerFailed = true;
755
844
  } finally {
845
+ state.sealed = true;
756
846
  if (timer != null) clearTimeout(timer);
757
847
  options.runSignal?.removeEventListener("abort", forwardRunAbort);
758
848
  }
759
849
  if (state.reporterFailed) throw state.reporterCause;
760
- if (!timedOut && !handlerFailed && cacheKey) try {
761
- options.cache.store.set(cacheKey, createCacheEntry(job, options.cache, bucket));
762
- rememberCacheEntry(options.cache, job.target.cacheFilePaths, cacheKey);
763
- } catch {}
764
- if (options.runSignal?.aborted) return finish(job, options, bucket, {
850
+ if (options.runSignal?.aborted) return finish(job, bucket, {
765
851
  cache: "miss",
766
852
  state: "cancelled"
767
853
  });
768
- if (timedOut) return finish(job, options, bucket, {
854
+ if (timedOut) return finish(job, bucket, {
769
855
  cache: "miss",
770
- cause: deadlineError,
771
856
  failure: failure(deadlineError, job, "timeout"),
772
857
  state: "failed"
773
858
  });
774
- if (handlerFailed) return finish(job, options, bucket, {
859
+ if (handlerFailed) return finish(job, bucket, {
775
860
  cache: "miss",
776
- cause: handlerCause,
777
861
  failure: failure(handlerCause, job, "handler"),
778
862
  state: "failed"
779
863
  });
780
- 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, {
781
868
  cache: "miss",
782
869
  state: "completed"
783
870
  });
@@ -787,44 +874,44 @@ function resolveRuleExecutionTimeout(timeoutMs) {
787
874
  if (!Number.isFinite(timeoutMs) || !Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("Rule execution timeout must be a finite positive integer.");
788
875
  return timeoutMs;
789
876
  }
790
- function createCacheEntry(job, cache, bucket) {
877
+ function createCacheEntry(job, fingerprint, bucket) {
791
878
  return {
792
- diagnostics: bucket.diagnostics,
793
- filePath: normalizeCachePath(cache.cwd, job.job.inputPath),
794
- fingerprint: {
795
- alintVersion: version,
796
- configHash: job.target.configHash,
797
- modelHash: cache.modelHash,
798
- ruleHash: job.execution.runtime.ruleHash
799
- },
879
+ diagnostics: snapshotDiagnostics(bucket.diagnostics),
880
+ fingerprint,
800
881
  target: {
801
- hash: createTargetHash(job),
882
+ hash: fingerprint.targetHash,
802
883
  identity: job.target.identity,
803
884
  kind: job.target.kind,
804
- 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,
805
895
  name: job.target.name,
806
- range: job.target.range
896
+ range: job.target.range ? {
897
+ end: job.target.range.end,
898
+ start: job.target.range.start
899
+ } : void 0
807
900
  },
808
- usage: bucket.usage
901
+ usage: snapshotUsageRecords(bucket.usage)
809
902
  };
810
903
  }
811
- function createExecutionCacheKey(job, cache) {
812
- if (!cache.enabled || !job.execution.runtime.cacheable || job.target.cacheFilePaths.length === 0) return void 0;
813
- return createCacheKey({
814
- alintVersion: version,
904
+ function createFingerprint(job, modelHash) {
905
+ return {
815
906
  configHash: job.target.configHash,
816
- filePath: normalizeCachePath(cache.cwd, job.job.inputPath),
817
- modelHash: cache.modelHash,
907
+ modelHash,
818
908
  ruleHash: job.execution.runtime.ruleHash,
819
- schemaVersion: 1,
820
- targetHash: createTargetHash(job),
821
- targetIdentity: job.target.identity,
822
- targetKind: job.target.kind
823
- });
909
+ targetHash: createTargetHash(job)
910
+ };
824
911
  }
825
912
  function createTargetHash(job) {
826
913
  const target = job.target;
827
- return stableHash({
914
+ return target.cacheTargetHash ?? stableHash({
828
915
  language: target.language,
829
916
  loc: target.loc,
830
917
  metadata: target.metadata,
@@ -836,36 +923,32 @@ function createTargetHash(job) {
836
923
  }
837
924
  function failure(cause, job, kind) {
838
925
  return {
839
- job: job.job,
926
+ job: snapshotProgressJobRef(job.jobRef),
840
927
  kind,
841
- message: errorMessageFrom(cause) ?? String(cause)
928
+ message: failureMessage$1(cause)
842
929
  };
843
930
  }
844
- function finish(job, options, bucket, terminal) {
845
- const outcome = {
846
- bucket,
847
- job,
848
- ...terminal
849
- };
850
- options.progress?.onJobEnd?.({
851
- cache: outcome.cache,
852
- endedAt: options.clock(),
853
- failure: outcome.failure,
854
- job: job.job,
855
- startedAt: options.startedAt,
856
- state: outcome.state
857
- });
858
- return outcome;
859
- }
860
- function rememberCacheEntry(cache, filePaths, cacheKey) {
861
- for (const filePath of filePaths) {
862
- const normalizedPath = normalizeCachePath(cache.cwd, filePath);
863
- const entries = cache.fileEntryKeys.get(normalizedPath) ?? /* @__PURE__ */ new Set();
864
- entries.add(cacheKey);
865
- 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.";
866
936
  }
867
937
  }
868
- 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) {
869
952
  let diagnostics;
870
953
  let usage;
871
954
  try {
@@ -875,180 +958,1132 @@ function replayCachedEntry(entry, bucket, job, progress) {
875
958
  throw new CacheReplayProcessingError(cause);
876
959
  }
877
960
  for (const cachedDiagnostic of diagnostics) {
878
- const diagnostic = {
961
+ const diagnostic = snapshotDiagnostic({
879
962
  ...cachedDiagnostic,
880
963
  cached: true
881
- };
964
+ });
882
965
  bucket.diagnostics.push(diagnostic);
883
966
  progress?.onDiagnostic?.({
884
- diagnostic,
885
- job
967
+ diagnostic: snapshotDiagnostic(diagnostic),
968
+ job: snapshotProgressJobRef(job),
969
+ progress: runProgress.snapshot()
886
970
  });
887
971
  }
888
972
  for (const record of usage) {
889
- bucket.usage.push(record);
973
+ const usageRecord = snapshotUsage(record);
974
+ bucket.usage.push(usageRecord);
890
975
  progress?.onUsage?.({
891
- job,
892
- record
976
+ job: snapshotProgressJobRef(job),
977
+ progress: runProgress.snapshot(),
978
+ record: snapshotUsage(usageRecord)
893
979
  });
894
980
  }
895
981
  }
896
- //#endregion
897
- //#region src/core/targets/directory.ts
898
- function createDirectoryExecutionPlans(directories, sourcePlanCount) {
899
- return directories.map((directory, directoryOffset) => {
900
- const executions = directory.ruleRuntimes.map((runtime) => {
901
- if (runtime.handlers.onTargetWith) return {
902
- run: () => runtime.handlers.onTargetWith?.(directory.target),
903
- runtime
904
- };
905
- if (runtime.handlers.onTargetDirectory) return {
906
- run: () => runtime.handlers.onTargetDirectory?.(directory.target),
907
- runtime
908
- };
909
- }).filter((execution) => execution !== void 0);
910
- const targets = executions.length === 0 ? [] : [{
911
- activeFilePath: directory.target.path,
912
- cacheFilePaths: [],
913
- configHash: directory.configHash,
914
- executions,
915
- identity: directory.target.path,
916
- kind: "directory",
917
- language: "directory",
918
- text: directory.target.path
919
- }];
920
- return {
921
- id: `directory:${directory.target.path}`,
922
- index: sourcePlanCount + directoryOffset + 1,
923
- kind: "directory",
924
- path: directory.target.path,
925
- planned: executions.length,
926
- targets
927
- };
928
- });
982
+ function scopeRank(scope) {
983
+ if (scope === "source") return 0;
984
+ if (scope === "directory") return 1;
985
+ return 2;
929
986
  }
930
987
  //#endregion
931
- //#region src/core/targets/project.ts
932
- function createProjectExecutionPlan(options) {
933
- const project = createProjectTarget(options.root, options.files);
934
- const executions = options.ruleRuntimes.map((runtime) => {
935
- if (runtime.handlers.onTargetWith) return {
936
- run: () => runtime.handlers.onTargetWith?.(project),
937
- runtime
938
- };
939
- if (runtime.handlers.onTargetProject) return {
940
- run: () => runtime.handlers.onTargetProject?.(project),
941
- 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
942
1002
  };
943
- }).filter((execution) => execution !== void 0);
944
- if (executions.length === 0) return;
945
- const target = {
946
- cacheFilePaths: options.files.map((file) => file.file.path),
947
- configHash: options.configHash,
948
- executions,
949
- identity: "project",
950
- kind: "project",
951
- language: "project",
952
- text: stableHash({
953
- files: options.files.map((file) => ({
954
- configHash: file.configHash,
955
- contentHash: hashText(file.file.text),
956
- path: normalizeCachePath(options.root, file.file.path)
957
- })).sort((left, right) => left.path.localeCompare(right.path)),
958
- targets: project.targets.map((target) => ({
959
- filePath: normalizeCachePath(options.root, target.file.path),
960
- identity: target.identity,
961
- kind: target.kind,
962
- language: target.language,
963
- loc: target.loc,
964
- metadata: target.metadata,
965
- name: target.name,
966
- origin: target.origin,
967
- range: target.range,
968
- text: target.text
969
- })),
970
- tree: createProjectTreeShape(options.files, options.root)
971
- })
972
1003
  };
973
1004
  return {
974
- id: `project:${options.root}`,
975
- index: options.index,
976
- kind: "project",
977
- path: options.root,
978
- planned: executions.length,
979
- targets: [target]
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
+ }
980
1034
  };
981
1035
  }
982
- function createProjectTarget(root, files) {
983
- const preparedFiles = [...files].sort((left, right) => left.file.path.localeCompare(right.file.path));
1036
+ function assertMutable(final) {
1037
+ if (final) throw new Error("Cannot update finalized progress.");
1038
+ }
1039
+ function assertNonNegativeInteger(value, name) {
1040
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer.`);
1041
+ }
1042
+ function assertSafeSum(left, right, name) {
1043
+ if (!Number.isSafeInteger(left + right)) throw new TypeError(`${name} must remain a safe integer.`);
1044
+ }
1045
+ function createCounts() {
984
1046
  return {
985
- files: preparedFiles.map((file) => file.file),
986
- kind: "project",
987
- root,
988
- targets: preparedFiles.flatMap((file) => file.targets)
1047
+ cached: 0,
1048
+ cancelled: 0,
1049
+ completed: 0,
1050
+ failed: 0,
1051
+ planned: 0,
1052
+ queued: 0,
1053
+ running: 0,
1054
+ skipped: 0
1055
+ };
1056
+ }
1057
+ function terminalCount(execution) {
1058
+ return execution.cached + execution.cancelled + execution.completed + execution.failed + execution.skipped;
1059
+ }
1060
+ //#endregion
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);
1078
+ }
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);
1090
+ }
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 ?? {}
1135
+ },
1136
+ provider: {
1137
+ endpoint: provider.endpoint,
1138
+ headers: { ...provider.headers ?? {} },
1139
+ id: provider.id,
1140
+ type: provider.type
1141
+ },
1142
+ size: model.size
989
1143
  };
990
1144
  }
991
- function createProjectTreeShape(files, root) {
992
- const directories = /* @__PURE__ */ new Set();
993
- const paths = files.map((file) => normalizeCachePath(root, file.file.path)).sort();
994
- for (const path of paths) {
995
- const parts = path.split("/");
996
- for (let index = 1; index < parts.length; index += 1) directories.add(parts.slice(0, index).join("/"));
1145
+ //#endregion
1146
+ //#region src/core/execution/runtime.ts
1147
+ function createRuleRuntimes(options) {
1148
+ return options.rules.map(({ enabledRule, ruleIndex }) => {
1149
+ const executionState = new AsyncLocalStorage();
1150
+ const context = {
1151
+ agent: options.effectiveAgent ? withAgentRetry((request) => options.effectiveAgent({
1152
+ ...request,
1153
+ signal: combineAbortSignals(executionState.getStore()?.signal, request.signal)
1154
+ }), options.runOptions.runner?.agentRetries, { onRetry: ({ attempt, maxAttempts }) => {
1155
+ const state = executionState.getStore();
1156
+ if (!state || state.sealed) return;
1157
+ const startedAt = Date.now();
1158
+ try {
1159
+ options.progress?.onJobRetry?.({
1160
+ attempt,
1161
+ job: snapshotProgressJobRef(state.jobRef),
1162
+ maxAttempts,
1163
+ progress: state.runProgress.snapshot(),
1164
+ startedAt
1165
+ });
1166
+ } catch (cause) {
1167
+ if (!state.reporterFailed) {
1168
+ state.reporterCause = cause;
1169
+ state.reporterFailed = true;
1170
+ }
1171
+ throw cause;
1172
+ }
1173
+ } }) : void 0,
1174
+ cwd: options.cwd,
1175
+ id: enabledRule.id,
1176
+ localId: enabledRule.localId,
1177
+ logger: { debug: () => {} },
1178
+ metering: { recordUsage: (record) => {
1179
+ const state = executionState.getStore();
1180
+ if (!state) throw new Error("Cannot record usage outside an active rule job.");
1181
+ if (state.sealed) return;
1182
+ const usageRecord = snapshotUsage({
1183
+ ...record,
1184
+ ruleId: record.ruleId ?? enabledRule.id
1185
+ });
1186
+ state.bucket.usage.push(usageRecord);
1187
+ try {
1188
+ options.progress?.onUsage?.({
1189
+ job: snapshotProgressJobRef(state.jobRef),
1190
+ progress: state.runProgress.snapshot(),
1191
+ record: snapshotUsage(usageRecord)
1192
+ });
1193
+ } catch (cause) {
1194
+ if (!state.reporterFailed) {
1195
+ state.reporterCause = cause;
1196
+ state.reporterFailed = true;
1197
+ }
1198
+ throw cause;
1199
+ }
1200
+ } },
1201
+ model: async (selector) => {
1202
+ const request = options.runOptions.modelOverride ?? (typeof selector === "string" ? selector : void 0);
1203
+ const requirement = mergeModelRequirement(enabledRule.rule.model, typeof selector === "string" ? void 0 : selector);
1204
+ const resolvedModel = resolveModel(options.setupConfig, {
1205
+ request,
1206
+ requirement,
1207
+ ruleId: enabledRule.id
1208
+ });
1209
+ const state = executionState.getStore();
1210
+ if (state && !state.sealed) state.currentModel = toDiagnosticModel(resolvedModel, request);
1211
+ return resolvedModel;
1212
+ },
1213
+ options: enabledRule.options,
1214
+ outputLanguage: options.runOptions.outputLanguage,
1215
+ report: (descriptor) => {
1216
+ const state = executionState.getStore();
1217
+ if (!state) throw new Error("Cannot report a diagnostic outside an active rule job.");
1218
+ if (state.sealed) return;
1219
+ const filePath = descriptor.filePath ?? state.activeFilePath;
1220
+ if (!filePath) throw new Error(`Diagnostic for rule "${enabledRule.id}" is missing filePath.`);
1221
+ const diagnosticModel = state.currentModel ? { ...state.currentModel } : void 0;
1222
+ state.currentModel = void 0;
1223
+ const diagnostic = snapshotDiagnostic({
1224
+ evidence: descriptor.evidence,
1225
+ filePath,
1226
+ loc: descriptor.loc,
1227
+ message: descriptor.message,
1228
+ model: diagnosticModel,
1229
+ ruleId: enabledRule.id,
1230
+ severity: enabledRule.severity
1231
+ });
1232
+ state.bucket.diagnostics.push(diagnostic);
1233
+ try {
1234
+ options.progress?.onDiagnostic?.({
1235
+ diagnostic: snapshotDiagnostic(diagnostic),
1236
+ job: snapshotProgressJobRef(state.jobRef),
1237
+ progress: state.runProgress.snapshot()
1238
+ });
1239
+ } catch (cause) {
1240
+ if (!state.reporterFailed) {
1241
+ state.reporterCause = cause;
1242
+ state.reporterFailed = true;
1243
+ }
1244
+ throw cause;
1245
+ }
1246
+ },
1247
+ settings: options.effectiveSettings,
1248
+ get signal() {
1249
+ return executionState.getStore()?.signal;
1250
+ },
1251
+ src: options.src
1252
+ };
1253
+ return {
1254
+ cacheable: enabledRule.rule.cache !== false,
1255
+ enabledRule,
1256
+ executionState,
1257
+ handlers: enabledRule.rule.create(context),
1258
+ ruleHash: stableHash({
1259
+ cache: enabledRule.rule.cache ?? true,
1260
+ cacheKey: enabledRule.rule.cacheKey,
1261
+ create: String(enabledRule.rule.create),
1262
+ id: enabledRule.id,
1263
+ localId: enabledRule.localId,
1264
+ model: enabledRule.rule.model,
1265
+ options: enabledRule.options,
1266
+ severity: enabledRule.severity
1267
+ }),
1268
+ ruleIndex
1269
+ };
1270
+ });
1271
+ }
1272
+ function mergeCapabilities(base, extra) {
1273
+ if (!base && !extra) return void 0;
1274
+ return [.../* @__PURE__ */ new Set([...base ?? [], ...extra ?? []])];
1275
+ }
1276
+ function mergeMinContextWindow(base, extra) {
1277
+ if (base === void 0) return extra;
1278
+ if (extra === void 0) return base;
1279
+ return Math.max(base, extra);
1280
+ }
1281
+ function mergeModelRequirement(base, extra) {
1282
+ if (!base && !extra) return void 0;
1283
+ const capabilities = mergeCapabilities(base?.capabilities, extra?.capabilities);
1284
+ const params = {
1285
+ ...base?.params ?? {},
1286
+ ...extra?.params ?? {}
1287
+ };
1288
+ return {
1289
+ capabilities,
1290
+ minContextWindow: mergeMinContextWindow(base?.minContextWindow, extra?.minContextWindow),
1291
+ params: Object.keys(params).length > 0 ? params : void 0,
1292
+ size: extra?.size ?? base?.size
1293
+ };
1294
+ }
1295
+ function toDiagnosticModel(model, request) {
1296
+ return {
1297
+ providerId: model.provider.id,
1298
+ requested: request,
1299
+ resolvedId: model.id
1300
+ };
1301
+ }
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;
1553
+ if (!Number.isFinite(ruleConcurrency) || !Number.isInteger(ruleConcurrency) || ruleConcurrency <= 0) throw new TypeError("Rule execution concurrency must be a finite positive integer.");
1554
+ return ruleConcurrency;
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;
997
2068
  }
2069
+ }
2070
+ function fileFailure(input, kind, message) {
998
2071
  return {
999
- directories: [...directories].sort(),
1000
- files: paths
2072
+ file: {
2073
+ index: input.fileIndex,
2074
+ path: input.path
2075
+ },
2076
+ kind,
2077
+ message
1001
2078
  };
1002
2079
  }
1003
- //#endregion
1004
- //#region src/core/targets/source.ts
1005
- function createSourceExecutionPlans(files, cwd) {
1006
- return files.map((preparedFile, fileOffset) => {
1007
- const targets = collectExecutionTargets(preparedFile);
1008
- const resolveTargetIdentity = createTargetIdentityResolver(targets.map((target) => toTargetIdentityInput(cwd, preparedFile.file.path, target)));
1009
- for (const target of targets) target.identity = resolveTargetIdentity(toTargetIdentityInput(cwd, preparedFile.file.path, target));
1010
- const filePlan = {
1011
- id: `source:${preparedFile.file.path}`,
1012
- index: fileOffset + 1,
1013
- kind: "source",
1014
- path: preparedFile.file.path,
1015
- planned: 0,
1016
- preparedFile,
1017
- targets
1018
- };
1019
- filePlan.planned = calculateFilePlanExecutions(filePlan);
1020
- return filePlan;
1021
- });
1022
- }
1023
- function calculateFilePlanExecutions(filePlan) {
1024
- return filePlan.targets.reduce((total, target) => total + target.executions.length, 0);
1025
- }
1026
- function collectExecutionTargets(preparedFile) {
1027
- const targets = [];
1028
- for (const sourceTarget of preparedFile.targets) {
1029
- const executions = preparedFile.ruleRuntimes.map((runtime) => {
1030
- return createSourceTargetExecution(runtime, sourceTarget);
1031
- }).filter((execution) => execution !== void 0);
1032
- if (executions.length === 0) continue;
1033
- targets.push({
1034
- activeFilePath: preparedFile.file.path,
1035
- cacheFilePaths: [preparedFile.file.path],
1036
- configHash: preparedFile.configHash,
1037
- executions,
1038
- identity: sourceTarget.identity,
1039
- kind: sourceTarget.kind,
1040
- language: sourceTarget.language,
1041
- loc: sourceTarget.loc,
1042
- metadata: sourceTarget.metadata,
1043
- name: sourceTarget.name,
1044
- origin: sourceTarget.origin,
1045
- range: sourceTarget.range,
1046
- text: sourceTarget.text
1047
- });
1048
- }
1049
- return targets;
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);
1050
2085
  }
1051
- function createSourceTargetExecution(runtime, target) {
2086
+ function sourceExecution(runtime, target) {
1052
2087
  if (runtime.handlers.onTargetWith) return {
1053
2088
  run: () => runtime.handlers.onTargetWith?.(target),
1054
2089
  runtime
@@ -1066,13 +2101,56 @@ function createSourceTargetExecution(runtime, target) {
1066
2101
  runtime
1067
2102
  };
1068
2103
  }
1069
- function toTargetIdentityInput(cwd, filePath, target) {
1070
- return {
1071
- filePath: target.kind === "file" ? normalizeCachePath(cwd, filePath) : void 0,
1072
- identity: target.identity,
1073
- kind: target.kind,
1074
- name: target.name,
1075
- range: target.range
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
1076
2154
  };
1077
2155
  }
1078
2156
  //#endregion
@@ -1103,327 +2181,160 @@ var AlintRunError = class extends Error {
1103
2181
  }
1104
2182
  };
1105
2183
  async function runAlint(options = {}) {
2184
+ const clock = Date.now;
1106
2185
  const timeoutMs = resolveRuleExecutionTimeout(options.runner?.timeoutMs);
1107
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
+ });
1108
2201
  const cwd$1 = options.cwd ?? cwd();
1109
- const config = options.config ?? [];
1110
2202
  const setupConfig = options.setupConfig ?? {
1111
2203
  providers: [],
1112
2204
  version: 1
1113
2205
  };
1114
- const clock = Date.now;
1115
2206
  const src = createSourceRuntime();
1116
2207
  const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
1117
2208
  const cacheStore = await createCacheStore({
1118
2209
  cwd: cwd$1,
1119
2210
  enabled: normalizedCacheConfig.enabled,
1120
- location: normalizedCacheConfig.location
2211
+ location: normalizedCacheConfig.location,
2212
+ readOnly: options.cacheOnly
1121
2213
  });
1122
- const cacheContext = {
1123
- cwd: cwd$1,
1124
- enabled: normalizedCacheConfig.enabled,
1125
- fileEntryKeys: /* @__PURE__ */ new Map(),
1126
- modelHash: stableHash({
1127
- modelOverride: options.modelOverride,
1128
- outputLanguage: options.outputLanguage,
1129
- setupConfig
1130
- }),
1131
- store: cacheStore
1132
- };
1133
- const pendingFiles = [...options.files ?? []];
1134
- const pendingDirectories = [...options.directories ?? []];
1135
- const files = (await Promise.all(pendingFiles.map(async (filePath) => {
1136
- const file = await src.readFile(resolve(cwd$1, filePath));
1137
- const resolvedConfig = resolveConfigForFile(file.path, config, { cwd: cwd$1 });
1138
- if (resolvedConfig.ignored) return;
1139
- const effectiveConfig = resolvedConfig.config;
1140
- const languageRegistry = createBuiltInLanguageRegistry();
1141
- for (const plugin of Object.values(effectiveConfig.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(languageRegistry, language);
1142
- const language = resolveLanguage(file, languageRegistry, { language: effectiveConfig.language });
1143
- const targets = await language.extract(file, {
1144
- cwd: cwd$1,
1145
- languageOptions: effectiveConfig.languageOptions,
1146
- src
1147
- });
1148
- const registry = buildRuleRegistry(effectiveConfig);
1149
- const ruleRuntimes = createRuleRuntimes({
1150
- cwd: cwd$1,
1151
- effectiveAgent: effectiveConfig.agent,
1152
- effectiveSettings: effectiveConfig.settings,
1153
- options,
1154
- progress: options.progress,
1155
- registry,
1156
- setupConfig,
1157
- src
1158
- });
1159
- return {
1160
- configHash: stableHash({
1161
- language: effectiveConfig.language,
1162
- languageOptions: effectiveConfig.languageOptions,
1163
- processor: effectiveConfig.processor,
1164
- resolvedLanguage: language.name,
1165
- settings: effectiveConfig.settings
1166
- }),
1167
- file,
1168
- ruleRuntimes,
1169
- targets
1170
- };
1171
- }))).filter((file) => file !== void 0);
1172
- const directories = pendingDirectories.map((directoryPath) => {
1173
- const target = {
1174
- kind: "directory",
1175
- path: resolve(cwd$1, directoryPath)
1176
- };
1177
- const resolvedConfig = resolveConfigForDirectory(target.path, config, { cwd: cwd$1 });
1178
- if (resolvedConfig.ignored) return;
1179
- const effectiveConfig = resolvedConfig.config;
1180
- const registry = buildRuleRegistry(effectiveConfig);
1181
- return {
1182
- configHash: stableHash({ settings: effectiveConfig.settings }),
1183
- ruleRuntimes: createRuleRuntimes({
1184
- cwd: cwd$1,
1185
- effectiveAgent: effectiveConfig.agent,
1186
- effectiveSettings: effectiveConfig.settings,
1187
- options,
1188
- progress: options.progress,
1189
- registry,
1190
- setupConfig,
1191
- src
1192
- }),
1193
- target
1194
- };
1195
- }).filter((directory) => directory !== void 0);
1196
- const resolvedProjectConfig = resolveConfigForProject(cwd$1, config, { cwd: cwd$1 });
1197
- const projectConfig = resolvedProjectConfig.config;
1198
- const projectRuleRuntimes = resolvedProjectConfig.ignored ? [] : createRuleRuntimes({
2214
+ const cacheContext = { modelHash: stableHash({
2215
+ modelOverride: options.modelOverride,
2216
+ outputLanguage: options.outputLanguage,
2217
+ setupConfig
2218
+ }) };
2219
+ const createRuntimes = (input) => createRuleRuntimes({
1199
2220
  cwd: cwd$1,
1200
- effectiveAgent: projectConfig.agent,
1201
- effectiveSettings: projectConfig.settings,
1202
- options,
2221
+ effectiveAgent: input.agent,
2222
+ effectiveSettings: input.settings,
1203
2223
  progress: options.progress,
1204
- registry: buildRuleRegistry(projectConfig),
2224
+ rules: input.rules,
2225
+ runOptions: options,
2226
+ runProgress,
1205
2227
  setupConfig,
1206
2228
  src
1207
2229
  });
1208
- const filePlans = createSourceExecutionPlans(files, cwd$1);
1209
- const directoryPlans = createDirectoryExecutionPlans(directories, filePlans.length);
1210
- const projectPlan = resolvedProjectConfig.ignored || options.projectTargets === false ? void 0 : createProjectExecutionPlan({
1211
- configHash: stableHash({ settings: projectConfig.settings }),
1212
- files,
1213
- index: filePlans.length + directoryPlans.length + 1,
1214
- root: cwd$1,
1215
- ruleRuntimes: projectRuleRuntimes
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
1216
2244
  });
1217
- const jobs = createRuleJobs([
1218
- ...filePlans,
1219
- ...directoryPlans,
1220
- ...projectPlan ? [projectPlan] : []
1221
- ]);
1222
- const runStartedAt = clock();
1223
- const settledOutcomes = new Array(jobs.length);
2245
+ const outcomes = [];
2246
+ const fileFailures = [];
1224
2247
  let infrastructureError;
1225
2248
  let infrastructureFailed = false;
1226
- options.progress?.onRunStart?.({
1227
- jobsTotal: jobs.length,
1228
- startedAt: runStartedAt
1229
- });
1230
- for (const job of jobs) options.progress?.onJobQueued?.({ job: job.job });
2249
+ let admissionFailed = false;
1231
2250
  try {
1232
- let cursor = 0;
1233
- const worker = async () => {
1234
- while (cursor < jobs.length) {
1235
- const index = cursor;
1236
- cursor += 1;
1237
- const job = jobs[index];
1238
- if (options.signal?.aborted) {
1239
- settledOutcomes[index] = {
1240
- bucket: {
1241
- diagnostics: [],
1242
- usage: []
1243
- },
1244
- cache: "miss",
1245
- job,
1246
- state: "cancelled"
1247
- };
1248
- continue;
1249
- }
1250
- try {
1251
- const startedAt = clock();
1252
- options.progress?.onJobStart?.({
1253
- job: job.job,
1254
- startedAt
1255
- });
1256
- settledOutcomes[index] = await executeRuleJob(job, {
1257
- cache: cacheContext,
1258
- cacheOnly: options.cacheOnly,
1259
- clock,
1260
- progress: options.progress,
1261
- runSignal: options.signal,
1262
- startedAt,
1263
- timeoutMs
1264
- });
1265
- } catch (error) {
1266
- if (!infrastructureFailed) {
1267
- infrastructureError = error;
1268
- infrastructureFailed = true;
1269
- }
1270
- }
1271
- }
1272
- };
1273
- await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, worker));
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;
1274
2297
  } finally {
1275
- if (!options.cacheOnly) await reconcileCache(filePlans, cacheContext);
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 {}
1276
2307
  }
1277
- const outcomes = Array.from({ length: jobs.length }, (_, index) => settledOutcomes[index] ?? {
1278
- bucket: {
1279
- diagnostics: [],
1280
- usage: []
1281
- },
1282
- cache: "miss",
1283
- job: jobs[index],
1284
- state: "cancelled"
1285
- });
1286
- const failedOutcomes = outcomes.filter((outcome) => outcome.state === "failed");
1287
- const failures = failedOutcomes.map((outcome) => outcome.failure);
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];
1288
2313
  const result = {
1289
- diagnostics: outcomes.flatMap((outcome) => outcome.bucket.diagnostics),
2314
+ diagnostics: outcomes.flatMap((outcome) => outcome.diagnostics),
1290
2315
  execution: executionCounts(outcomes),
1291
2316
  usage: runUsage(outcomes)
1292
2317
  };
2318
+ options.progress?.onExecuteEnd?.({
2319
+ endedAt: clock(),
2320
+ progress: runProgress.snapshot()
2321
+ });
1293
2322
  options.progress?.onRunEnd?.({
1294
- diagnostics: result.diagnostics,
2323
+ diagnostics: snapshotDiagnostics(result.diagnostics),
1295
2324
  endedAt: clock(),
1296
- execution: result.execution,
2325
+ execution: { ...result.execution },
2326
+ progress: runProgress.snapshot(),
1297
2327
  startedAt: runStartedAt,
1298
- usage: result.usage
2328
+ usage: snapshotRunUsage(result.usage)
1299
2329
  });
1300
2330
  if (infrastructureFailed) throw infrastructureError;
1301
2331
  if (options.signal?.aborted) throw new AlintAbortError(result, { cause: options.signal.reason });
1302
- if (failures.length > 0) throw new AlintRunError(`${failures.length} rule execution${failures.length === 1 ? "" : "s"} failed.`, result, {
1303
- cause: new AggregateError(failedOutcomes.map((outcome) => outcome.cause), "Rule execution failures."),
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."),
1304
2334
  failures
1305
2335
  });
1306
2336
  return result;
1307
2337
  }
1308
- function createRuleRuntimes(options) {
1309
- return options.registry.enabledRules.map((enabledRule) => {
1310
- const executionState = new AsyncLocalStorage();
1311
- const context = {
1312
- agent: options.effectiveAgent ? withAgentRetry((request) => options.effectiveAgent({
1313
- ...request,
1314
- signal: combineAbortSignals(executionState.getStore()?.signal, request.signal)
1315
- }), options.options.runner?.agentRetries, { onRetry: ({ attempt, maxAttempts }) => {
1316
- const state = executionState.getStore();
1317
- if (!state) return;
1318
- const startedAt = Date.now();
1319
- try {
1320
- options.progress?.onJobRetry?.({
1321
- attempt,
1322
- job: state.job,
1323
- maxAttempts,
1324
- startedAt
1325
- });
1326
- } catch (cause) {
1327
- if (!state.reporterFailed) {
1328
- state.reporterCause = cause;
1329
- state.reporterFailed = true;
1330
- }
1331
- throw cause;
1332
- }
1333
- } }) : void 0,
1334
- cwd: options.cwd,
1335
- id: enabledRule.id,
1336
- localId: enabledRule.localId,
1337
- logger: { debug: () => {} },
1338
- metering: { recordUsage: (record) => {
1339
- const state = executionState.getStore();
1340
- if (!state) throw new Error("Cannot record usage outside an active rule job.");
1341
- const usageRecord = {
1342
- ...record,
1343
- ruleId: record.ruleId ?? enabledRule.id
1344
- };
1345
- state.bucket.usage.push(usageRecord);
1346
- try {
1347
- options.progress?.onUsage?.({
1348
- job: state.job,
1349
- record: usageRecord
1350
- });
1351
- } catch (cause) {
1352
- if (!state.reporterFailed) {
1353
- state.reporterCause = cause;
1354
- state.reporterFailed = true;
1355
- }
1356
- throw cause;
1357
- }
1358
- } },
1359
- model: async (selector) => {
1360
- const request = options.options.modelOverride ?? (typeof selector === "string" ? selector : void 0);
1361
- const requirement = mergeModelRequirement(enabledRule.rule.model, typeof selector === "string" ? void 0 : selector);
1362
- const resolvedModel = resolveModel(options.setupConfig, {
1363
- request,
1364
- requirement,
1365
- ruleId: enabledRule.id
1366
- });
1367
- const state = executionState.getStore();
1368
- if (state) state.currentModel = toDiagnosticModel(resolvedModel, request);
1369
- return resolvedModel;
1370
- },
1371
- options: enabledRule.options,
1372
- outputLanguage: options.options.outputLanguage,
1373
- report: (descriptor) => {
1374
- const state = executionState.getStore();
1375
- if (!state) throw new Error("Cannot report a diagnostic outside an active rule job.");
1376
- const filePath = descriptor.filePath ?? state.activeFilePath;
1377
- if (!filePath) throw new Error(`Diagnostic for rule "${enabledRule.id}" is missing filePath.`);
1378
- const diagnosticModel = state.currentModel ? { ...state.currentModel } : void 0;
1379
- state.currentModel = void 0;
1380
- const diagnostic = {
1381
- evidence: descriptor.evidence,
1382
- filePath,
1383
- loc: descriptor.loc,
1384
- message: descriptor.message,
1385
- model: diagnosticModel,
1386
- ruleId: enabledRule.id,
1387
- severity: enabledRule.severity
1388
- };
1389
- state.bucket.diagnostics.push(diagnostic);
1390
- try {
1391
- options.progress?.onDiagnostic?.({
1392
- diagnostic,
1393
- job: state.job
1394
- });
1395
- } catch (cause) {
1396
- if (!state.reporterFailed) {
1397
- state.reporterCause = cause;
1398
- state.reporterFailed = true;
1399
- }
1400
- throw cause;
1401
- }
1402
- },
1403
- settings: options.effectiveSettings,
1404
- get signal() {
1405
- return executionState.getStore()?.signal;
1406
- },
1407
- src: options.src
1408
- };
1409
- return {
1410
- cacheable: enabledRule.rule.cache !== false,
1411
- enabledRule,
1412
- executionState,
1413
- handlers: enabledRule.rule.create(context),
1414
- ruleHash: stableHash({
1415
- cache: enabledRule.rule.cache ?? true,
1416
- cacheKey: enabledRule.rule.cacheKey,
1417
- create: String(enabledRule.rule.create),
1418
- id: enabledRule.id,
1419
- localId: enabledRule.localId,
1420
- model: enabledRule.rule.model,
1421
- options: enabledRule.options,
1422
- severity: enabledRule.severity
1423
- })
1424
- };
1425
- });
1426
- }
1427
2338
  function executionCounts(outcomes) {
1428
2339
  const counts = {
1429
2340
  cached: 0,
@@ -1438,59 +2349,23 @@ function executionCounts(outcomes) {
1438
2349
  for (const outcome of outcomes) counts[outcome.state] += 1;
1439
2350
  return counts;
1440
2351
  }
1441
- function mergeCapabilities(base, extra) {
1442
- if (!base && !extra) return;
1443
- return [.../* @__PURE__ */ new Set([...base ?? [], ...extra ?? []])];
1444
- }
1445
- function mergeMinContextWindow(base, extra) {
1446
- if (base === void 0) return extra;
1447
- if (extra === void 0) return base;
1448
- return Math.max(base, extra);
1449
- }
1450
- function mergeModelRequirement(base, extra) {
1451
- if (!base && !extra) return;
1452
- const capabilities = mergeCapabilities(base?.capabilities, extra?.capabilities);
1453
- const params = {
1454
- ...base?.params ?? {},
1455
- ...extra?.params ?? {}
1456
- };
1457
- return {
1458
- capabilities,
1459
- minContextWindow: mergeMinContextWindow(base?.minContextWindow, extra?.minContextWindow),
1460
- params: Object.keys(params).length > 0 ? params : void 0,
1461
- size: extra?.size ?? base?.size
1462
- };
1463
- }
1464
- async function reconcileCache(filePlans, cacheContext) {
1465
- try {
1466
- for (const filePlan of filePlans) {
1467
- const file = filePlan.preparedFile.file;
1468
- const normalizedPath = normalizeCachePath(cacheContext.cwd, file.path);
1469
- const entries = [...cacheContext.fileEntryKeys.get(normalizedPath) ?? []];
1470
- cacheContext.store.markFile(file.path, hashText(file.text), entries);
1471
- }
1472
- await cacheContext.store.reconcile();
1473
- } catch {}
1474
- }
1475
- function resolveRuleConcurrency(ruleConcurrency) {
1476
- if (ruleConcurrency === void 0) return 4;
1477
- if (!Number.isFinite(ruleConcurrency) || !Number.isInteger(ruleConcurrency) || ruleConcurrency <= 0) throw new TypeError("Rule execution concurrency must be a finite positive integer.");
1478
- return ruleConcurrency;
1479
- }
1480
2352
  function runUsage(outcomes) {
1481
2353
  const live = [];
1482
2354
  const cached = [];
1483
- 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);
1484
2356
  return {
1485
2357
  ...usageTotals(live),
1486
2358
  ...cached.length > 0 ? { cached: usageTotals(cached) } : {}
1487
2359
  };
1488
2360
  }
1489
- function toDiagnosticModel(model, request) {
2361
+ function snapshotRunUsage(usage) {
1490
2362
  return {
1491
- providerId: model.provider.id,
1492
- requested: request,
1493
- resolvedId: model.id
2363
+ ...usage,
2364
+ ...usage.cached ? { cached: {
2365
+ ...usage.cached,
2366
+ records: snapshotUsageRecords(usage.cached.records)
2367
+ } } : {},
2368
+ records: snapshotUsageRecords(usage.records)
1494
2369
  };
1495
2370
  }
1496
2371
  function usageTotals(records) {