@alint-js/core 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,17 +1,18 @@
1
1
  import { o as withAgentRetry, t as combineAbortSignals } from "./agent-B6GG68NX.mjs";
2
2
  import { n as definePlugin, r as defineRule, t as defineConfig } from "./define-Da342Fcx.mjs";
3
- import { a as sliceRange, i as sliceLines, n as createSourceFile, r as createSourceRuntime, t as extractJsSourceTargets } from "./extract-CIWssxNK.mjs";
3
+ import { a as createSourceFile, c as sliceRange, d as stableHash, i as SourceChangedError, l as withLanguage, n as targetIdentity, o as createSourceRuntime, r as withStableIdentities, s as sliceLines, t as extractJsSourceTargets, u as createStableHasher } from "./extract-BVbjWXMC.mjs";
4
4
  import { minimatch } from "minimatch";
5
- import { isAbsolute, relative, resolve } from "pathe";
5
+ import { basename, extname, isAbsolute, relative, resolve } from "pathe";
6
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";
7
+ import { basename as basename$1, dirname, extname as extname$1, isAbsolute as isAbsolute$1, join, relative as relative$1, resolve as resolve$1, sep } from "node:path";
8
+ import { randomUUID } from "node:crypto";
8
9
  import process, { cwd } from "node:process";
10
+ import { array, boolean, check, custom, finite, getDotPath, literal, number, object, optional, parse, pipe, record, safeParse, string, union, unknown } from "valibot";
9
11
  import { Buffer } from "node:buffer";
10
- import { createHash, randomUUID } from "node:crypto";
11
12
  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
14
  import { AsyncLocalStorage } from "node:async_hooks";
15
+ import { streamText } from "@xsai/stream-text";
15
16
  //#region src/config/config-array.ts
16
17
  const effectiveBasePathSymbol = Symbol("effectiveBasePath");
17
18
  const inheritedMatcherScopesSymbol = Symbol("inheritedMatcherScopes");
@@ -242,16 +243,9 @@ const javascriptLanguage = {
242
243
  ".jsx",
243
244
  ".mjs"
244
245
  ],
245
- extract: (file) => extractJsSourceTargets(withLanguage$1(file, "javascript")),
246
+ extract: (file) => extractJsSourceTargets(withLanguage(file, "javascript")),
246
247
  name: "javascript"
247
248
  };
248
- function withLanguage$1(file, language) {
249
- if (file.language === language) return file;
250
- return {
251
- ...file,
252
- language
253
- };
254
- }
255
249
  //#endregion
256
250
  //#region src/core/languages/registry.ts
257
251
  function createLanguageRegistry$1() {
@@ -278,11 +272,11 @@ const textLanguage = {
278
272
  file,
279
273
  identity: "file",
280
274
  kind: "file",
281
- language: "text/plain",
275
+ language: "plaintext",
282
276
  origin: { physicalPath: file.path },
283
277
  text: file.text
284
278
  }],
285
- name: "text/plain"
279
+ name: "plaintext"
286
280
  };
287
281
  //#endregion
288
282
  //#region src/core/languages/ts/index.ts
@@ -296,20 +290,13 @@ const typescriptLanguage = {
296
290
  extract: (file) => extractJsSourceTargets(withLanguage(file, "typescript")),
297
291
  name: "typescript"
298
292
  };
299
- function withLanguage(file, language) {
300
- if (file.language === language) return file;
301
- return {
302
- ...file,
303
- language
304
- };
305
- }
306
293
  //#endregion
307
294
  //#region src/core/languages/resolve.ts
308
295
  function resolveLanguage(file, registry, options) {
309
296
  return resolveLanguageForPath(file.path, registry, options);
310
297
  }
311
298
  function resolveLanguageForPath(filePath, registry, options) {
312
- const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname(filePath)) ?? "text/plain";
299
+ const languageName = options.language ?? options.processedLanguage ?? registry.byExtension.get(extname$1(filePath)) ?? "plaintext";
313
300
  const language = registry.languages.get(languageName);
314
301
  if (!language) throw new Error(`Unknown language "${languageName}".`);
315
302
  return language;
@@ -324,41 +311,298 @@ function createBuiltInLanguageRegistry() {
324
311
  return registry;
325
312
  }
326
313
  //#endregion
327
- //#region package.json
328
- var version = "0.1.4";
314
+ //#region src/dsl/registry.ts
315
+ function buildRuleRegistry(config) {
316
+ const rules = /* @__PURE__ */ new Map();
317
+ const localIds = /* @__PURE__ */ new Map();
318
+ const enabledRules = [];
319
+ for (const [alias, plugin] of Object.entries(config.plugins)) for (const [localId, rule] of Object.entries(plugin.rules ?? {})) {
320
+ const id = `${alias}/${localId}`;
321
+ if (rules.has(id)) throw new Error(`Duplicate rule id "${id}".`);
322
+ rules.set(id, rule);
323
+ localIds.set(id, localId);
324
+ }
325
+ for (const [id, entry] of Object.entries(config.rules)) {
326
+ const rule = rules.get(id);
327
+ if (!rule) throw new Error(`Unknown rule "${id}".`);
328
+ const normalizedEntry = normalizeRuleConfigEntry(entry);
329
+ const severity = normalizedEntry.severity;
330
+ if (severity === "off") continue;
331
+ enabledRules.push({
332
+ id,
333
+ localId: localIds.get(id) ?? id,
334
+ options: parseRuleOptions(id, rule.options, normalizedEntry.options),
335
+ rule,
336
+ severity
337
+ });
338
+ }
339
+ return {
340
+ enabledRules,
341
+ rules
342
+ };
343
+ }
344
+ function formatRuleOptionIssues(index, issues) {
345
+ return issues.map((issue) => {
346
+ const path = getDotPath(issue);
347
+ return path === null ? `"${index}": ${issue.message}` : `"${index}.${path}": ${issue.message}`;
348
+ }).join("; ");
349
+ }
350
+ function isRuleConfigTuple(entry) {
351
+ return Array.isArray(entry);
352
+ }
353
+ function normalizeRuleConfigEntry(entry) {
354
+ if (isRuleConfigTuple(entry)) {
355
+ const [severity = "warn", ...options] = entry;
356
+ return {
357
+ options,
358
+ severity
359
+ };
360
+ }
361
+ return {
362
+ options: [],
363
+ severity: entry ?? "warn"
364
+ };
365
+ }
366
+ function parseRuleOption(id, index, schema, value) {
367
+ const result = safeParse(schema, value);
368
+ if (result.success) return result.output;
369
+ if (value === void 0) {
370
+ const emptyObjectResult = safeParse(schema, {});
371
+ if (emptyObjectResult.success) return emptyObjectResult.output;
372
+ }
373
+ throw new TypeError(`Invalid options for rule "${id}": ${formatRuleOptionIssues(index, result.issues)}`);
374
+ }
375
+ function parseRuleOptions(id, schema, options) {
376
+ if (!schema) {
377
+ if (options.length > 0) throw new TypeError(`Rule "${id}" does not accept options.`);
378
+ return [];
379
+ }
380
+ if (options.length > schema.length) throw new TypeError(`Invalid options for rule "${id}": Unexpected option at index ${schema.length}.`);
381
+ return schema.map((optionSchema, index) => parseRuleOption(id, index, optionSchema, options[index]));
382
+ }
329
383
  //#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
- }
384
+ //#region src/core/languages/diagnostics.ts
385
+ /** A rule named a language and nothing registered it. This is a builtin rule. Not a real rule. */
386
+ const MISSING_LANGUAGE_RULE = "alint/missing-language";
387
+ /** A linted file's extension belongs to no registered language. This is a builtin rule. Not a real rule either. */
388
+ const UNREGISTERED_LANGUAGE_RULE = "alint/unregistered-language";
389
+ /**
390
+ * One diagnostic per missing language, always an error: a rule scoped to a language nothing provides
391
+ * matches no file, so leaving it at a warning would let a run pass having silently checked nothing.
392
+ */
393
+ function missingLanguageDiagnostics(missing) {
394
+ return [...missing.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([languageId, entry]) => {
395
+ const ruleIds = [...entry.ruleIds].sort();
396
+ const subject = ruleIds.length === 1 ? `Rule "${ruleIds[0]}" needs` : `Rules ${ruleIds.map((id) => `"${id}"`).join(", ")} need`;
397
+ return {
398
+ filePath: entry.filePath,
399
+ message: `${subject} the "${languageId}" language, which no plugin registered. Add a plugin that provides "${languageId}", or turn the rule off.`,
400
+ ruleId: MISSING_LANGUAGE_RULE,
401
+ severity: "error"
402
+ };
403
+ });
404
+ }
405
+ /** Records one rule that named a language the config never registered, grouped by language id. */
406
+ function recordMissingLanguage(into, languageId, ruleId, path) {
407
+ const existing = into.get(languageId);
408
+ if (existing === void 0) {
409
+ into.set(languageId, {
410
+ filePath: path,
411
+ ruleIds: /* @__PURE__ */ new Set([ruleId])
412
+ });
413
+ return;
414
+ }
415
+ existing.ruleIds.add(ruleId);
416
+ if (path < existing.filePath) existing.filePath = path;
417
+ }
418
+ /**
419
+ * Records one linted file no language claimed, grouped by extension so a missing pack is one
420
+ * diagnostic, not one per file. `'off'` records nothing.
421
+ */
422
+ function recordUnregistered(into, path, severity) {
423
+ if (severity === "off") return;
424
+ const key = extname(path) || basename(path);
425
+ const existing = into.get(key);
426
+ into.set(key, {
427
+ count: (existing?.count ?? 0) + 1,
428
+ filePath: existing !== void 0 && existing.filePath < path ? existing.filePath : path,
429
+ severity: existing?.severity === "error" ? "error" : severity
430
+ });
431
+ }
432
+ /** One diagnostic per extension, sorted, for the run to prepend to its results. */
433
+ function unregisteredLanguageDiagnostics(unregistered) {
434
+ return [...unregistered.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([extension, entry]) => ({
435
+ filePath: entry.filePath,
436
+ message: `No language is registered for "${extension}": ${entry.count} ${entry.count === 1 ? "file was" : "files were"} handled as plain text. Register one through a plugin's "languages", or pin \`language: 'plaintext'\` to accept plain text.`,
437
+ ruleId: UNREGISTERED_LANGUAGE_RULE,
438
+ severity: entry.severity
439
+ }));
440
+ }
441
+ function unregisteredLanguageSeverity(linterOptions) {
442
+ const value = linterOptions.reportUnregisteredLanguages;
443
+ return value === "error" || value === "off" ? value : "warn";
444
+ }
445
+ //#endregion
446
+ //#region src/core/languages/rule-languages.ts
447
+ /**
448
+ * The language a file falls back to when no registered language claims its extension. Not a real
449
+ * language, which is why `'any'` excludes it.
450
+ */
451
+ const PLAIN_TEXT = "plaintext";
452
+ /**
453
+ * Whether a rule reads this target.
454
+ *
455
+ * A rule with language off still reads file targets, because reading raw text, or asking a model about a
456
+ * whole file, needs no language. However, it never reads the function and class targets extraction produces.
457
+ * `'file'` is the one kind every language yields, plain text included, so it is the dividing line.
458
+ *
459
+ * `'any'` means any real language, so a file that fell back to plain text because nothing claimed
460
+ * its extension is withheld: the rule asked for structure and there is none to give. The run says so
461
+ * once per extension through `alint/unregistered-language` rather than letting each rule report an
462
+ * empty file.
463
+ */
464
+ function isTargetLanguageAccepted(languages, targetKind, targetLanguage) {
465
+ if (languages.kind === "off") return targetKind === "file";
466
+ if (languages.kind === "any") return targetLanguage !== PLAIN_TEXT;
467
+ return languages.ids.includes(targetLanguage);
468
+ }
469
+ function resolveRuleLanguages(languages) {
470
+ if (languages === void 0) return { kind: "off" };
471
+ if (languages === "any") return { kind: "any" };
472
+ if ("ids" in languages) return {
473
+ ids: languages.ids,
474
+ kind: "list",
475
+ skipMissing: languages.skipMissing ?? false
476
+ };
477
+ return {
478
+ ids: languages,
479
+ kind: "list",
480
+ skipMissing: false
481
+ };
482
+ }
483
+ //#endregion
484
+ //#region src/core/preparation.ts
485
+ /**
486
+ * The `ctx.src.extract` a run hands to rules, for parsing files it was never asked to lint, which
487
+ * an index builder sweeping the workspace needs.
488
+ *
489
+ * It resolves each file's OWN config rather than a caller's, because the language a file resolves to
490
+ * is a config decision and two config groups may register different plugins. It extracts on demand
491
+ * and holds nothing: source planning releases rich extractor values after compact jobs are admitted,
492
+ * and a run-wide parse memo here would put that retention back. A caller keeps only what it derives.
493
+ *
494
+ * `getSrc` defers reading the runtime because the runtime is built around this closure. It is assigned
495
+ * before any rule runs, so by the first call it is present.
496
+ */
497
+ function createSourceExtractor(cwd, config, getSrc) {
498
+ return async (filePath, options = {}) => {
499
+ const path = resolve(cwd, filePath);
500
+ const resolvedConfig = resolveConfigForFile(path, config, { cwd });
501
+ if (resolvedConfig.ignored) return [];
502
+ const effectiveConfig = resolvedConfig.config;
503
+ const language = resolveLanguageForPath(path, createLanguageRegistry(effectiveConfig), { language: options.language ?? effectiveConfig.language });
504
+ const src = getSrc();
505
+ const file = await src.readFile(path);
506
+ return language.extract(file, {
507
+ cwd,
508
+ languageOptions: effectiveConfig.languageOptions,
509
+ src
510
+ });
511
+ };
512
+ }
513
+ function prepareRun(options = {}) {
514
+ const cwd$2 = options.cwd ?? cwd();
515
+ const config = options.config ?? [];
516
+ const files = [];
517
+ const directories = [];
518
+ const missingLanguages = /* @__PURE__ */ new Map();
519
+ const unregisteredLanguages = /* @__PURE__ */ new Map();
520
+ for (const filePath of options.files ?? []) {
521
+ const path = resolve(cwd$2, filePath);
522
+ const resolvedConfig = resolveConfigForFile(path, config, { cwd: cwd$2 });
523
+ if (resolvedConfig.ignored) continue;
524
+ const effectiveConfig = resolvedConfig.config;
525
+ const languageRegistry = createLanguageRegistry(effectiveConfig);
526
+ const language = resolveLanguageForPath(path, languageRegistry, { language: effectiveConfig.language });
527
+ const rules = prepareRules(effectiveConfig);
528
+ recordMissingLanguages(missingLanguages, rules, languageRegistry, path);
529
+ if (effectiveConfig.language === void 0 && language.name === "plaintext" && rules.some(({ enabledRule }) => !isTargetLanguageAccepted(resolveRuleLanguages(enabledRule.rule.languages), "file", "plaintext"))) recordUnregistered(unregisteredLanguages, path, unregisteredLanguageSeverity(effectiveConfig.linterOptions));
530
+ files.push({
531
+ agent: effectiveConfig.agent,
532
+ configHash: stableHash({
533
+ language: effectiveConfig.language,
534
+ languageOptions: effectiveConfig.languageOptions,
535
+ processor: effectiveConfig.processor,
536
+ resolvedLanguage: language.name,
537
+ settings: effectiveConfig.settings
538
+ }),
539
+ fileIndex: files.length,
540
+ language,
541
+ languageOptions: effectiveConfig.languageOptions,
542
+ path,
543
+ rules,
544
+ settings: effectiveConfig.settings
545
+ });
546
+ }
547
+ for (const directoryPath of options.directories ?? []) {
548
+ const path = resolve(cwd$2, directoryPath);
549
+ const resolvedConfig = resolveConfigForDirectory(path, config, { cwd: cwd$2 });
550
+ if (resolvedConfig.ignored) continue;
551
+ const effectiveConfig = resolvedConfig.config;
552
+ directories.push({
553
+ agent: effectiveConfig.agent,
554
+ configHash: stableHash({ settings: effectiveConfig.settings }),
555
+ directoryIndex: directories.length,
556
+ rules: prepareRules(effectiveConfig),
557
+ settings: effectiveConfig.settings,
558
+ target: {
559
+ kind: "directory",
560
+ path
561
+ }
562
+ });
563
+ }
564
+ return {
565
+ directories,
566
+ files,
567
+ missingLanguages,
568
+ project: options.projectTargets === false ? void 0 : prepareProject(cwd$2, config),
569
+ unregisteredLanguages
341
570
  };
342
- return api;
343
571
  }
344
- function hashText(text) {
345
- return createHash("sha256").update(text).digest("hex");
572
+ function createLanguageRegistry(config) {
573
+ const registry = createBuiltInLanguageRegistry();
574
+ for (const plugin of Object.values(config.plugins)) for (const language of Object.values(plugin.languages ?? {})) registerLanguage(registry, language);
575
+ return registry;
576
+ }
577
+ function prepareProject(root, config) {
578
+ const resolvedConfig = resolveConfigForProject(root, config, { cwd: root });
579
+ if (resolvedConfig.ignored) return void 0;
580
+ const effectiveConfig = resolvedConfig.config;
581
+ return {
582
+ agent: effectiveConfig.agent,
583
+ configHash: stableHash({ settings: effectiveConfig.settings }),
584
+ root,
585
+ rules: prepareRules(effectiveConfig),
586
+ settings: effectiveConfig.settings
587
+ };
346
588
  }
347
- function stableHash(value) {
348
- return hashText(stableStringify(value));
589
+ function prepareRules(config) {
590
+ return buildRuleRegistry(config).enabledRules.map((enabledRule, ruleIndex) => ({
591
+ enabledRule,
592
+ ruleIndex
593
+ }));
349
594
  }
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(",")}}`;
595
+ function recordMissingLanguages(into, rules, registry, path) {
596
+ for (const { enabledRule } of rules) {
597
+ const languages = resolveRuleLanguages(enabledRule.rule.languages);
598
+ if (languages.kind !== "list" || languages.skipMissing) continue;
599
+ for (const languageId of languages.ids) if (!registry.languages.has(languageId)) recordMissingLanguage(into, languageId, enabledRule.id, path);
358
600
  }
359
- return JSON.stringify(value);
360
601
  }
361
602
  //#endregion
603
+ //#region package.json
604
+ var version = "0.2.0";
605
+ //#endregion
362
606
  //#region src/core/cache/types.ts
363
607
  const CACHE_MAGIC = "ALINT_CACHE";
364
608
  //#endregion
@@ -704,7 +948,7 @@ function parseCacheBody(text) {
704
948
  async function persistCacheBody(location, header, body, writeFile) {
705
949
  const validatedBody = parse(CacheFileBodySchema, body);
706
950
  await mkdir(dirname(location), { recursive: true });
707
- const tempPath = join(dirname(location), `.${basename(location)}.${process.pid}.${randomUUID()}.tmp`);
951
+ const tempPath = join(dirname(location), `.${basename$1(location)}.${process.pid}.${randomUUID()}.tmp`);
708
952
  try {
709
953
  await writeFile(tempPath, `${header}\n${JSON.stringify(validatedBody)}\n`);
710
954
  await rename(tempPath, location);
@@ -836,7 +1080,7 @@ async function executeRuleJob(job, options) {
836
1080
  let handlerCause;
837
1081
  let handlerFailed = false;
838
1082
  try {
839
- await job.execution.runtime.executionState.run(state, job.execution.run);
1083
+ await job.execution.runtime.executionState.run(state, () => runHandler(job));
840
1084
  } catch (cause) {
841
1085
  if (state.reporterFailed) throw state.reporterCause;
842
1086
  handlerCause = cause;
@@ -858,7 +1102,7 @@ async function executeRuleJob(job, options) {
858
1102
  });
859
1103
  if (handlerFailed) return finish(job, bucket, {
860
1104
  cache: "miss",
861
- failure: failure(handlerCause, job, "handler"),
1105
+ failure: failure(handlerCause, job, handlerCause instanceof SourceChangedError ? "source-changed" : "handler"),
862
1106
  state: "failed"
863
1107
  });
864
1108
  if (slot && fingerprint) try {
@@ -910,16 +1154,7 @@ function createFingerprint(job, modelHash) {
910
1154
  };
911
1155
  }
912
1156
  function createTargetHash(job) {
913
- const target = job.target;
914
- return target.cacheTargetHash ?? stableHash({
915
- language: target.language,
916
- loc: target.loc,
917
- metadata: target.metadata,
918
- name: target.name,
919
- origin: target.origin,
920
- range: target.range,
921
- text: target.text
922
- });
1157
+ return job.target.cacheTargetHash;
923
1158
  }
924
1159
  function failure(cause, job, kind) {
925
1160
  return {
@@ -979,6 +1214,44 @@ function replayCachedEntry(entry, bucket, job, progress, runProgress) {
979
1214
  });
980
1215
  }
981
1216
  }
1217
+ /**
1218
+ * Dispatches one compact job descriptor to its registered rule handler.
1219
+ *
1220
+ * Triggering workflow:
1221
+ *
1222
+ * {@link executeRuleJob}
1223
+ * -> {@link runHandler}
1224
+ * -> `RuleHandlers.onTarget*`
1225
+ *
1226
+ * Upstream:
1227
+ * - {@link executeRuleJob}
1228
+ *
1229
+ * Downstream:
1230
+ * - `RuleRuntime.handlers`
1231
+ */
1232
+ function runHandler(job) {
1233
+ const { handlers } = job.execution.runtime;
1234
+ const target = job.target.descriptor;
1235
+ switch (job.execution.handler) {
1236
+ case "class":
1237
+ if (target.kind !== "class") throw new TypeError(`Expected class target, received "${target.kind}".`);
1238
+ return handlers.onTargetClass?.(target);
1239
+ case "directory":
1240
+ if (target.kind !== "directory") throw new TypeError(`Expected directory target, received "${target.kind}".`);
1241
+ return handlers.onTargetDirectory?.(target);
1242
+ case "file":
1243
+ if (target.kind !== "file") throw new TypeError(`Expected file target, received "${target.kind}".`);
1244
+ return handlers.onTargetFile?.(target);
1245
+ case "function":
1246
+ if (target.kind !== "function") throw new TypeError(`Expected function target, received "${target.kind}".`);
1247
+ return handlers.onTargetFunction?.(target);
1248
+ case "language-declaration-error": throw new Error(`Rule "${job.execution.runtime.enabledRule.id}" handles function or class targets but declares no "languages", so it receives none. Declare \`languages: 'any'\`, or list the language ids it handles.`);
1249
+ case "project":
1250
+ if (target.kind !== "project") throw new TypeError(`Expected project target, received "${target.kind}".`);
1251
+ return handlers.onTargetProject?.(target);
1252
+ case "with": return handlers.onTargetWith?.(target);
1253
+ }
1254
+ }
982
1255
  function scopeRank(scope) {
983
1256
  if (scope === "source") return 0;
984
1257
  if (scope === "directory") return 1;
@@ -989,33 +1262,44 @@ function scopeRank(scope) {
989
1262
  function createRunProgress(filesTotal) {
990
1263
  assertNonNegativeInteger(filesTotal, "File total");
991
1264
  const execution = createCounts();
992
- let final = false;
1265
+ let filesPlanned = 0;
1266
+ let planningComplete = false;
993
1267
  const snapshot = () => {
994
1268
  const jobsCompleted = terminalCount(execution);
995
1269
  return {
996
1270
  execution: { ...execution },
1271
+ filesPlanned,
997
1272
  filesTotal,
998
- final,
999
1273
  jobsCompleted,
1000
1274
  jobsStarted: jobsCompleted + execution.running,
1001
- jobsTotal: execution.planned
1275
+ jobsTotal: execution.planned,
1276
+ planningComplete
1002
1277
  };
1003
1278
  };
1004
1279
  return {
1280
+ completeFilePlanning: () => {
1281
+ if (planningComplete) throw new Error("Cannot complete file planning after planning is complete.");
1282
+ if (filesPlanned >= filesTotal) throw new Error("Cannot complete planning for more files than the file total.");
1283
+ filesPlanned += 1;
1284
+ return snapshot();
1285
+ },
1286
+ completePlanning: () => {
1287
+ planningComplete = true;
1288
+ return snapshot();
1289
+ },
1005
1290
  finalize: () => {
1006
1291
  if (execution.queued !== 0 || execution.running !== 0) throw new Error("Cannot finalize progress while jobs are queued or running.");
1007
- final = true;
1292
+ if (!planningComplete) throw new Error("Cannot finalize progress before planning is complete.");
1008
1293
  return snapshot();
1009
1294
  },
1010
1295
  finish: (from, to) => {
1011
- assertMutable(final);
1012
1296
  if (execution[from] <= 0) throw new Error(`Cannot finish a job without a ${from} job.`);
1013
1297
  execution[from] -= 1;
1014
1298
  execution[to] += 1;
1015
1299
  return snapshot();
1016
1300
  },
1017
1301
  queue: (count = 1) => {
1018
- assertMutable(final);
1302
+ if (planningComplete) throw new Error("Cannot queue work after planning is complete.");
1019
1303
  assertNonNegativeInteger(count, "Queued job count");
1020
1304
  assertSafeSum(execution.planned, count, "Planned job count");
1021
1305
  assertSafeSum(execution.queued, count, "Queued job count");
@@ -1025,7 +1309,6 @@ function createRunProgress(filesTotal) {
1025
1309
  },
1026
1310
  snapshot,
1027
1311
  start: () => {
1028
- assertMutable(final);
1029
1312
  if (execution.queued <= 0) throw new Error("Cannot start a job without a queued job.");
1030
1313
  execution.queued -= 1;
1031
1314
  execution.running += 1;
@@ -1033,9 +1316,6 @@ function createRunProgress(filesTotal) {
1033
1316
  }
1034
1317
  };
1035
1318
  }
1036
- function assertMutable(final) {
1037
- if (final) throw new Error("Cannot update finalized progress.");
1038
- }
1039
1319
  function assertNonNegativeInteger(value, name) {
1040
1320
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer.`);
1041
1321
  }
@@ -1334,6 +1614,9 @@ var RuleScheduler = class {
1334
1614
  this.settleClose();
1335
1615
  return this.closePromise;
1336
1616
  }
1617
+ completeFilePlanning() {
1618
+ return this.options.progress.completeFilePlanning();
1619
+ }
1337
1620
  schedule(jobs) {
1338
1621
  if (!this.accepting || jobs.length === 0) return {
1339
1622
  jobsAdded: 0,
@@ -1568,152 +1851,6 @@ function createLane() {
1568
1851
  };
1569
1852
  }
1570
1853
  //#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
1854
  //#region src/core/project/index.ts
1718
1855
  var ProjectIndexBuilder = class {
1719
1856
  files = [];
@@ -1767,7 +1904,7 @@ var ProjectIndexBuilder = class {
1767
1904
  }
1768
1905
  };
1769
1906
  function createProjectJobs(options) {
1770
- const executions = options.runtimes.map((runtime) => projectExecution(runtime, options.project.target)).filter((execution) => execution !== void 0);
1907
+ const executions = options.runtimes.map((runtime) => projectExecution(runtime)).filter((execution) => execution !== void 0);
1771
1908
  if (executions.length === 0) return { jobs: [] };
1772
1909
  const owner = options.cacheStore.beginOwner({
1773
1910
  kind: "project",
@@ -1777,10 +1914,9 @@ function createProjectJobs(options) {
1777
1914
  cacheOwner: owner,
1778
1915
  cacheTargetHash: options.project.hash,
1779
1916
  configHash: options.configHash,
1917
+ descriptor: options.project.target,
1780
1918
  identity: "project",
1781
- kind: "project",
1782
- language: "project",
1783
- text: options.project.hash
1919
+ kind: "project"
1784
1920
  };
1785
1921
  return {
1786
1922
  jobs: executions.map((execution) => {
@@ -1845,125 +1981,108 @@ function copyTargetSnapshot(target) {
1845
1981
  semanticHash: target.semanticHash
1846
1982
  };
1847
1983
  }
1848
- function projectExecution(runtime, target) {
1984
+ function projectExecution(runtime) {
1849
1985
  if (runtime.handlers.onTargetWith) return {
1850
- run: () => runtime.handlers.onTargetWith?.(target),
1986
+ handler: "with",
1851
1987
  runtime
1852
1988
  };
1853
1989
  if (runtime.handlers.onTargetProject) return {
1854
- run: () => runtime.handlers.onTargetProject?.(target),
1990
+ handler: "project",
1855
1991
  runtime
1856
1992
  };
1857
1993
  }
1858
- async function executeSourceSession(input, options) {
1859
- openSession(options.metrics);
1994
+ //#endregion
1995
+ //#region src/core/source/planner.ts
1996
+ /**
1997
+ * Converts one rich extractor result into detached descriptors and admits its compact jobs.
1998
+ *
1999
+ * The returned outcome promise captures only cache ownership and hashes. This function does not
2000
+ * await rule execution, so its `SourceFile`, parser output, and rich targets become unreachable
2001
+ * when planning returns.
2002
+ */
2003
+ async function planSource(input, options) {
2004
+ if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
2005
+ let file;
1860
2006
  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({
2007
+ file = await options.src.readFile(input.path);
2008
+ } catch (error) {
2009
+ reportFilePlanningComplete(input, 0, options);
2010
+ return {
2011
+ failure: fileFailure(input, "read", failureMessage(error, "Failed to read source file.")),
2012
+ outcomes: Promise.resolve([])
2013
+ };
2014
+ }
2015
+ if (options.signal?.aborted) return { outcomes: Promise.resolve([]) };
2016
+ let targets;
2017
+ try {
2018
+ targets = await input.language.extract(file, {
2019
+ cwd: options.cwd,
2020
+ languageOptions: input.languageOptions,
2021
+ src: options.src
2022
+ });
2023
+ } catch (error) {
2024
+ reportFilePlanningComplete(input, 0, options);
2025
+ return {
2026
+ failure: fileFailure(input, "extract", failureMessage(error, "Failed to extract source targets.")),
2027
+ outcomes: Promise.resolve([])
2028
+ };
2029
+ }
2030
+ const { contentHash } = file;
2031
+ let cacheOwner;
2032
+ let jobs;
2033
+ let project;
2034
+ try {
2035
+ project = options.projectSnapshots === false ? void 0 : createProjectSnapshot(input, file.language, file.path, contentHash, targets);
2036
+ cacheOwner = options.cacheStore.beginOwner({
1888
2037
  kind: "file",
1889
2038
  path: file.path
1890
2039
  });
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 ? {
2040
+ const baseFile = {
2041
+ contentHash,
2042
+ language: file.language,
2043
+ path: file.path
2044
+ };
2045
+ jobs = createSourceJobs(input, createExecutionTargets(input, targets, baseFile, options.ruleRuntimes, options.cwd, cacheOwner));
2046
+ } catch (error) {
2047
+ reportFilePlanningComplete(input, 0, options);
2048
+ return {
2049
+ failure: fileFailure(input, "extract", failureMessage(error, "Failed to plan source targets.")),
2050
+ outcomes: Promise.resolve([])
2051
+ };
2052
+ }
2053
+ if (options.signal?.aborted) {
2054
+ cacheOwner.commit({
1916
2055
  contentHash,
1917
2056
  mode: "merge"
1918
- } : { contentHash });
2057
+ });
1919
2058
  return {
1920
- outcomes,
2059
+ outcomes: Promise.resolve([]),
1921
2060
  project
1922
2061
  };
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));
2062
+ }
2063
+ const batch = options.scheduler.schedule(jobs);
2064
+ reportFilePlanningComplete(input, batch.jobsAdded, options);
2065
+ return {
2066
+ outcomes: batch.outcomes.then((settled) => {
2067
+ cacheOwner.commit(options.signal?.aborted ? {
2068
+ contentHash,
2069
+ mode: "merge"
2070
+ } : { contentHash });
2071
+ return settled;
2072
+ }),
2073
+ project
2074
+ };
1960
2075
  }
1961
- function closeSession(metrics) {
1962
- if (!metrics) return;
1963
- metrics.active -= 1;
1964
- metrics.closed += 1;
2076
+ /**
2077
+ * Starts every input plan independently and preserves input-ordered results.
2078
+ */
2079
+ async function planSources(inputs, options) {
2080
+ return Promise.all(inputs.map((input) => planSource(input, {
2081
+ ...options,
2082
+ ruleRuntimes: options.createRuleRuntimes(input)
2083
+ })));
1965
2084
  }
1966
- function createExecutionTargets(input, targets, runtimes, cwd, cacheOwner) {
2085
+ function createExecutionTargets(input, targets, file, runtimes, cwd, cacheOwner) {
1967
2086
  const resolveIdentity = createTargetIdentityResolver(targets.map((target) => ({
1968
2087
  filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
1969
2088
  identity: target.identity,
@@ -1971,32 +2090,50 @@ function createExecutionTargets(input, targets, runtimes, cwd, cacheOwner) {
1971
2090
  name: target.name,
1972
2091
  range: target.range
1973
2092
  })));
1974
- return targets.map((target, targetIndex) => {
2093
+ return targets.flatMap((target, targetIndex) => {
2094
+ const executions = runtimes.map((runtime) => sourceExecution(runtime, target.kind, target.language)).filter((execution) => execution !== void 0);
2095
+ if (executions.length === 0) return [];
2096
+ const identity = resolveIdentity({
2097
+ filePath: target.kind === "file" ? normalizeCachePath(cwd, input.path) : void 0,
2098
+ identity: target.identity,
2099
+ kind: target.kind,
2100
+ name: target.name,
2101
+ range: target.range
2102
+ }, targetIndex);
2103
+ const plannedTarget = {
2104
+ file,
2105
+ identity: target.identity,
2106
+ kind: target.kind,
2107
+ language: target.language,
2108
+ loc: target.loc && {
2109
+ end: { ...target.loc.end },
2110
+ start: { ...target.loc.start }
2111
+ },
2112
+ metadata: snapshotMetadata(target.metadata),
2113
+ name: target.name,
2114
+ origin: target.origin && {
2115
+ ...target.origin,
2116
+ range: target.origin.range && { ...target.origin.range }
2117
+ },
2118
+ range: target.range && { ...target.range }
2119
+ };
1975
2120
  return {
1976
- executions: runtimes.map((runtime) => sourceExecution(runtime, target)).filter((execution) => execution !== void 0),
2121
+ executions,
1977
2122
  target: {
1978
2123
  activeFilePath: input.path,
1979
2124
  cacheOwner,
2125
+ cacheTargetHash: targetSemanticHash(target),
1980
2126
  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),
2127
+ descriptor: plannedTarget,
2128
+ identity,
1988
2129
  kind: target.kind,
1989
- language: target.language,
1990
2130
  loc: target.loc,
1991
- metadata: target.metadata,
1992
2131
  name: target.name,
1993
- origin: target.origin,
1994
- range: target.range,
1995
- text: target.text
2132
+ range: target.range
1996
2133
  },
1997
2134
  targetIndex
1998
2135
  };
1999
- }).filter((target) => target.executions.length > 0);
2136
+ });
2000
2137
  }
2001
2138
  function createProjectSnapshot(input, language, path, contentHash, targets) {
2002
2139
  return {
@@ -2016,15 +2153,7 @@ function createProjectSnapshot(input, language, path, contentHash, targets) {
2016
2153
  ...target.name === void 0 ? {} : { name: target.name },
2017
2154
  ...target.range === void 0 ? {} : { range: { ...target.range } }
2018
2155
  },
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
- })
2156
+ semanticHash: targetSemanticHash(target)
2028
2157
  }))
2029
2158
  };
2030
2159
  }
@@ -2077,36 +2206,81 @@ function fileFailure(input, kind, message) {
2077
2206
  message
2078
2207
  };
2079
2208
  }
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);
2209
+ function reportFilePlanningComplete(input, jobsAdded, options) {
2210
+ try {
2211
+ const progress = options.scheduler.completeFilePlanning();
2212
+ options.progress?.onFileReady?.({
2213
+ fileIndex: input.fileIndex,
2214
+ inputPath: input.path,
2215
+ jobsAdded,
2216
+ progress
2217
+ });
2218
+ } catch (error) {
2219
+ options.scheduler.cancelWithError(error);
2220
+ throw error;
2221
+ }
2085
2222
  }
2086
- function sourceExecution(runtime, target) {
2223
+ function snapshotMetadata(metadata) {
2224
+ if (metadata === void 0) return void 0;
2225
+ return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, snapshotMetadataValue(value, /* @__PURE__ */ new WeakSet())]));
2226
+ }
2227
+ function snapshotMetadataValue(value, ancestors) {
2228
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
2229
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2230
+ if (typeof value !== "object") throw new TypeError("Source target metadata must contain only finite JSON data.");
2231
+ if (ancestors.has(value)) throw new TypeError("Source target metadata must not contain cycles.");
2232
+ ancestors.add(value);
2233
+ try {
2234
+ if (Array.isArray(value)) return value.map((item) => snapshotMetadataValue(item, ancestors));
2235
+ const prototype = Object.getPrototypeOf(value);
2236
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("Source target metadata must contain only plain objects and arrays.");
2237
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshotMetadataValue(item, ancestors)]));
2238
+ } finally {
2239
+ ancestors.delete(value);
2240
+ }
2241
+ }
2242
+ function sourceExecution(runtime, kind, language) {
2243
+ const languages = resolveRuleLanguages(runtime.enabledRule.rule.languages);
2244
+ if (languages.kind === "off" && kind === "file" && (runtime.handlers.onTargetClass ?? runtime.handlers.onTargetFunction)) return {
2245
+ handler: "language-declaration-error",
2246
+ runtime
2247
+ };
2248
+ if (!isTargetLanguageAccepted(languages, kind, language)) return void 0;
2087
2249
  if (runtime.handlers.onTargetWith) return {
2088
- run: () => runtime.handlers.onTargetWith?.(target),
2250
+ handler: "with",
2089
2251
  runtime
2090
2252
  };
2091
- if (target.kind === "class" && runtime.handlers.onTargetClass) return {
2092
- run: () => runtime.handlers.onTargetClass?.(target),
2253
+ if (kind === "class" && runtime.handlers.onTargetClass) return {
2254
+ handler: "class",
2093
2255
  runtime
2094
2256
  };
2095
- if (target.kind === "file" && runtime.handlers.onTargetFile) return {
2096
- run: () => runtime.handlers.onTargetFile?.(target),
2257
+ if (kind === "file" && runtime.handlers.onTargetFile) return {
2258
+ handler: "file",
2097
2259
  runtime
2098
2260
  };
2099
- if (target.kind === "function" && runtime.handlers.onTargetFunction) return {
2100
- run: () => runtime.handlers.onTargetFunction?.(target),
2261
+ if (kind === "function" && runtime.handlers.onTargetFunction) return {
2262
+ handler: "function",
2101
2263
  runtime
2102
2264
  };
2103
2265
  }
2266
+ /** Keeps per-rule cache fingerprints and compact project snapshots on one semantic target identity. */
2267
+ function targetSemanticHash(target) {
2268
+ return stableHash({
2269
+ language: target.language,
2270
+ loc: target.loc,
2271
+ metadata: target.metadata,
2272
+ name: target.name,
2273
+ origin: target.origin,
2274
+ range: target.range,
2275
+ text: target.text
2276
+ });
2277
+ }
2104
2278
  //#endregion
2105
2279
  //#region src/core/targets/directory.ts
2106
2280
  function createDirectoryJobs(input, runtimes) {
2107
2281
  const target = input.target;
2108
2282
  return runtimes.flatMap((runtime) => {
2109
- const execution = directoryExecution(runtime, target);
2283
+ const execution = directoryExecution(runtime);
2110
2284
  if (!execution) return [];
2111
2285
  const ruleId = runtime.enabledRule.id;
2112
2286
  return [{
@@ -2134,22 +2308,22 @@ function createDirectoryJobs(input, runtimes) {
2134
2308
  },
2135
2309
  target: {
2136
2310
  activeFilePath: target.path,
2311
+ cacheTargetHash: stableHash(target),
2137
2312
  configHash: input.configHash,
2313
+ descriptor: target,
2138
2314
  identity: target.path,
2139
- kind: "directory",
2140
- language: "directory",
2141
- text: target.path
2315
+ kind: "directory"
2142
2316
  }
2143
2317
  }];
2144
2318
  });
2145
2319
  }
2146
- function directoryExecution(runtime, target) {
2320
+ function directoryExecution(runtime) {
2147
2321
  if (runtime.handlers.onTargetWith) return {
2148
- run: () => runtime.handlers.onTargetWith?.(target),
2322
+ handler: "with",
2149
2323
  runtime
2150
2324
  };
2151
2325
  if (runtime.handlers.onTargetDirectory) return {
2152
- run: () => runtime.handlers.onTargetDirectory?.(target),
2326
+ handler: "directory",
2153
2327
  runtime
2154
2328
  };
2155
2329
  }
@@ -2199,11 +2373,12 @@ async function runAlint(options = {}) {
2199
2373
  startedAt: runStartedAt
2200
2374
  });
2201
2375
  const cwd$1 = options.cwd ?? cwd();
2376
+ const config = options.config ?? [];
2202
2377
  const setupConfig = options.setupConfig ?? {
2203
2378
  providers: [],
2204
2379
  version: 1
2205
2380
  };
2206
- const src = createSourceRuntime();
2381
+ const src = createSourceRuntime({ extract: createSourceExtractor(cwd$1, config, () => src) });
2207
2382
  const normalizedCacheConfig = normalizeRunnerCacheConfig(options.runner?.cache, cwd$1);
2208
2383
  const cacheStore = await createCacheStore({
2209
2384
  cwd: cwd$1,
@@ -2250,7 +2425,7 @@ async function runAlint(options = {}) {
2250
2425
  try {
2251
2426
  const projectRuntimes = preparation.project ? createRuntimes(preparation.project) : [];
2252
2427
  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, {
2428
+ const sourceResults = await planSources(preparation.files, {
2254
2429
  cacheStore,
2255
2430
  createRuleRuntimes: createRuntimes,
2256
2431
  cwd: cwd$1,
@@ -2258,20 +2433,18 @@ async function runAlint(options = {}) {
2258
2433
  projectSnapshots: projectBuilder !== void 0,
2259
2434
  scheduler,
2260
2435
  signal: options.signal,
2261
- sourceWindow: resolveSourceWindow(concurrency),
2262
2436
  src
2263
2437
  });
2264
- for (const result of sourceResults) {
2265
- outcomes.push(...result.outcomes);
2266
- if (result.failure) fileFailures.push(result.failure);
2267
- }
2438
+ for (const result of sourceResults) if (result.failure) fileFailures.push(result.failure);
2439
+ const pendingOutcomes = sourceResults.map((result) => result.outcomes);
2268
2440
  if (projectBuilder) {
2269
2441
  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
2442
  }
2271
2443
  const directoryBatches = preparation.directories.map((input) => {
2272
2444
  return scheduler.schedule(createDirectoryJobs(input, createRuntimes(input)));
2273
2445
  });
2274
- outcomes.push(...(await Promise.all(directoryBatches.map((batch) => batch.outcomes))).flat());
2446
+ pendingOutcomes.push(...directoryBatches.map((batch) => batch.outcomes));
2447
+ let clearEmptyProjectOwner = false;
2275
2448
  if (fileFailures.length === 0 && preparation.project && !options.signal?.aborted) {
2276
2449
  if (projectBuilder) {
2277
2450
  const project = createProjectJobs({
@@ -2282,19 +2455,26 @@ async function runAlint(options = {}) {
2282
2455
  });
2283
2456
  const projectOwner = project.owner;
2284
2457
  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" });
2458
+ pendingOutcomes.push(batch.outcomes.then((settled) => {
2459
+ projectOwner?.commit({ mode: options.signal?.aborted ? "merge" : "replace" });
2460
+ return settled;
2461
+ }));
2462
+ } else if (!options.cacheOnly) clearEmptyProjectOwner = true;
2291
2463
  }
2464
+ const planningProgress = runProgress.completePlanning();
2465
+ options.progress?.onPlanningEnd?.({ progress: planningProgress });
2466
+ outcomes.push(...(await Promise.all(pendingOutcomes)).flat());
2467
+ if (clearEmptyProjectOwner) cacheStore.beginOwner({
2468
+ kind: "project",
2469
+ path: preparation.project.root
2470
+ }).commit({ mode: "replace" });
2292
2471
  } catch (error) {
2293
2472
  scheduler.cancelWithError(error);
2294
2473
  infrastructureError = error;
2295
2474
  infrastructureFailed = true;
2296
2475
  admissionFailed = true;
2297
2476
  } finally {
2477
+ runProgress.completePlanning();
2298
2478
  try {
2299
2479
  await scheduler.close();
2300
2480
  } catch (error) {
@@ -2311,7 +2491,11 @@ async function runAlint(options = {}) {
2311
2491
  const ruleFailures = outcomes.flatMap((outcome) => outcome.state === "failed" ? [outcome.failure] : []);
2312
2492
  const failures = [...fileFailures.sort((left, right) => left.file.index - right.file.index), ...ruleFailures];
2313
2493
  const result = {
2314
- diagnostics: outcomes.flatMap((outcome) => outcome.diagnostics),
2494
+ diagnostics: [
2495
+ ...missingLanguageDiagnostics(preparation.missingLanguages),
2496
+ ...unregisteredLanguageDiagnostics(preparation.unregisteredLanguages),
2497
+ ...outcomes.flatMap((outcome) => outcome.diagnostics)
2498
+ ],
2315
2499
  execution: executionCounts(outcomes),
2316
2500
  usage: runUsage(outcomes)
2317
2501
  };
@@ -2385,4 +2569,303 @@ function usageTotals(records) {
2385
2569
  };
2386
2570
  }
2387
2571
  //#endregion
2388
- export { AlintAbortError, AlintRunCancelledError, AlintRunError, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange };
2572
+ //#region src/models/benchmark-scheduler.ts
2573
+ /** Runs FIFO job queues independently so one provider's cap cannot throttle or overload another provider. */
2574
+ async function scheduleProviderJobs(jobs, concurrencyByProvider, execute) {
2575
+ const queues = /* @__PURE__ */ new Map();
2576
+ for (const [index, job] of jobs.entries()) {
2577
+ const queue = queues.get(job.providerId) ?? [];
2578
+ queue.push({
2579
+ index,
2580
+ job
2581
+ });
2582
+ queues.set(job.providerId, queue);
2583
+ }
2584
+ const limits = /* @__PURE__ */ new Map();
2585
+ for (const providerId of queues.keys()) {
2586
+ const limit = concurrencyByProvider[providerId] ?? 1;
2587
+ if (!Number.isInteger(limit) || limit < 1) throw new RangeError(`Benchmark concurrency for provider "${providerId}" must be a positive integer.`);
2588
+ limits.set(providerId, limit);
2589
+ }
2590
+ const results = Array.from({ length: jobs.length });
2591
+ const workers = [];
2592
+ for (const [providerId, queue] of queues) {
2593
+ const workerCount = Math.min(limits.get(providerId), queue.length);
2594
+ let nextJobIndex = 0;
2595
+ for (let workerIndex = 0; workerIndex < workerCount; workerIndex += 1) workers.push((async () => {
2596
+ while (nextJobIndex < queue.length) {
2597
+ const scheduled = queue[nextJobIndex];
2598
+ nextJobIndex += 1;
2599
+ if (scheduled !== void 0) results[scheduled.index] = { value: await execute(scheduled.job, scheduled.index) };
2600
+ }
2601
+ })());
2602
+ }
2603
+ await Promise.all(workers);
2604
+ return results.map((result, index) => {
2605
+ if (result === void 0) throw new Error(`Benchmark scheduler did not complete job ${index}.`);
2606
+ return result.value;
2607
+ });
2608
+ }
2609
+ //#endregion
2610
+ //#region src/models/benchmark.ts
2611
+ const defaultSampleCount = 3;
2612
+ const defaultTimeoutMs = 6e4;
2613
+ const maxOutputTokens = 128;
2614
+ /**
2615
+ * Benchmarks model jobs concurrently within independent provider limits.
2616
+ *
2617
+ * Each model's repeat warm-up, measured repeats, and non-repeats remain serial so concurrent samples cannot
2618
+ * distort that model's cache behavior. Non-repeat prompts put their nonce before the shared corpus.
2619
+ */
2620
+ async function benchmarkModels(config, options = {}) {
2621
+ const request = options.request ?? measureModel;
2622
+ const sampleCount = options.sampleCount ?? defaultSampleCount;
2623
+ const seed = options.seed ?? Math.floor(Date.now() / 1e3);
2624
+ const timeoutMs = options.timeoutMs ?? config.runner?.timeoutMs ?? defaultTimeoutMs;
2625
+ const jobs = config.providers.flatMap((provider) => provider.models.map((model) => ({
2626
+ identity: `${provider.id}/${model.id}`,
2627
+ model,
2628
+ providerId: provider.id
2629
+ })));
2630
+ const results = Array.from({ length: jobs.length });
2631
+ const active = /* @__PURE__ */ new Map();
2632
+ const modelsTotal = jobs.length;
2633
+ if (!Number.isInteger(sampleCount) || sampleCount < 1) throw new RangeError("Benchmark sample count must be a positive integer.");
2634
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("Benchmark timeout must be greater than zero.");
2635
+ return scheduleProviderJobs(jobs, options.providerConcurrency ?? {}, async (job, modelIndex) => {
2636
+ const baseResult = {
2637
+ modelId: job.model.id,
2638
+ providerId: job.providerId,
2639
+ success: {
2640
+ attempted: 0,
2641
+ completed: 0
2642
+ }
2643
+ };
2644
+ let model;
2645
+ try {
2646
+ model = resolveModel(config, { request: job.identity });
2647
+ } catch (error) {
2648
+ const result = {
2649
+ ...baseResult,
2650
+ error: errorMessageFrom(error) ?? "Could not resolve model."
2651
+ };
2652
+ results[modelIndex] = result;
2653
+ reportProgress(options, modelsTotal, results, active);
2654
+ return result;
2655
+ }
2656
+ const result = await benchmarkModel(model, modelIndex, seed, sampleCount, timeoutMs, request, (current) => {
2657
+ active.set(modelIndex, current);
2658
+ reportProgress(options, modelsTotal, results, active);
2659
+ });
2660
+ active.delete(modelIndex);
2661
+ results[modelIndex] = result;
2662
+ reportProgress(options, modelsTotal, results, active);
2663
+ return result;
2664
+ });
2665
+ }
2666
+ async function attempt(result, request, timeoutMs, onStarted) {
2667
+ result.success.attempted += 1;
2668
+ onStarted();
2669
+ const measurement = await request(AbortSignal.timeout(timeoutMs));
2670
+ result.success.completed += 1;
2671
+ return measurement;
2672
+ }
2673
+ async function benchmarkModel(model, modelIndex, seed, sampleCount, timeoutMs, request, onProgress) {
2674
+ const result = {
2675
+ modelId: model.id,
2676
+ providerId: model.provider.id,
2677
+ success: {
2678
+ attempted: 0,
2679
+ completed: 0
2680
+ }
2681
+ };
2682
+ const repeatPrompt = benchmarkPrompt(seed, model, 0, true);
2683
+ const repeatMeasurements = [];
2684
+ const nonRepeatMeasurements = [];
2685
+ try {
2686
+ await attempt(result, (signal) => request({
2687
+ model,
2688
+ prompt: repeatPrompt,
2689
+ signal
2690
+ }), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "repeat", 0, sampleCount, true)));
2691
+ for (let index = 0; index < sampleCount; index += 1) repeatMeasurements.push(await attempt(result, (signal) => request({
2692
+ model,
2693
+ prompt: repeatPrompt,
2694
+ signal
2695
+ }), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "repeat", index + 1, sampleCount, false))));
2696
+ for (let index = 0; index < sampleCount; index += 1) {
2697
+ const prompt = benchmarkPrompt(seed, model, index + 1, false);
2698
+ nonRepeatMeasurements.push(await attempt(result, (signal) => request({
2699
+ model,
2700
+ prompt,
2701
+ signal
2702
+ }), timeoutMs, () => onProgress(currentProgress(model, modelIndex, result, "non-repeat", index + 1, sampleCount, false))));
2703
+ }
2704
+ } catch (error) {
2705
+ return {
2706
+ ...result,
2707
+ error: errorMessageFrom(error) ?? "Benchmark request failed."
2708
+ };
2709
+ }
2710
+ return {
2711
+ ...result,
2712
+ nonRepeatMs: median(nonRepeatMeasurements.map((measurement) => measurement.durationMs)),
2713
+ repeatMs: median(repeatMeasurements.map((measurement) => measurement.durationMs)),
2714
+ throughput: medianOptional(nonRepeatMeasurements.map(throughputOf))
2715
+ };
2716
+ }
2717
+ function benchmarkPrompt(seed, model, sample, repeat) {
2718
+ const nonce = repeat ? `${seed}:${model.provider.id}/${model.id}:repeat` : `${seed}:${model.provider.id}/${model.id}:non-repeat:${sample}`;
2719
+ const corpus = sampleCorpus(seed + sample);
2720
+ return [
2721
+ `[alint benchmark nonce: ${nonce}]`,
2722
+ "Read the following source review corpus. Return exactly eight concise bullet points summarizing its behavior, risks, and maintainability. Do not quote the nonce.",
2723
+ "",
2724
+ corpus
2725
+ ].join("\n");
2726
+ }
2727
+ function currentProgress(model, modelIndex, result, phase, sample, samples, warmup) {
2728
+ return {
2729
+ modelId: model.id,
2730
+ modelIndex,
2731
+ phase,
2732
+ providerId: model.provider.id,
2733
+ sample,
2734
+ samples,
2735
+ success: { ...result.success },
2736
+ warmup
2737
+ };
2738
+ }
2739
+ async function measureModel({ model, prompt, signal }) {
2740
+ const messages = [{
2741
+ content: prompt,
2742
+ role: "user"
2743
+ }];
2744
+ const startedAt = performance.now();
2745
+ let firstOutputAt;
2746
+ const result = streamText({
2747
+ ...model.params,
2748
+ abortSignal: signal,
2749
+ baseURL: model.provider.endpoint,
2750
+ headers: model.provider.headers,
2751
+ maxTokens: maxOutputTokens,
2752
+ messages,
2753
+ model: model.id,
2754
+ streamOptions: { includeUsage: true },
2755
+ temperature: 0
2756
+ });
2757
+ const completion = Promise.allSettled([
2758
+ result.messages,
2759
+ result.steps,
2760
+ result.totalUsage,
2761
+ result.usage
2762
+ ]);
2763
+ let settled;
2764
+ try {
2765
+ for await (const event of result.eventStream) if (firstOutputAt === void 0 && (event.type === "reasoning.delta" || event.type === "text.delta")) firstOutputAt = performance.now();
2766
+ } finally {
2767
+ settled = await completion;
2768
+ }
2769
+ const endedAt = performance.now();
2770
+ const usageResult = settled[3];
2771
+ if (usageResult.status === "rejected") throw usageResult.reason;
2772
+ return {
2773
+ durationMs: endedAt - startedAt,
2774
+ firstOutputMs: firstOutputAt === void 0 ? void 0 : firstOutputAt - startedAt,
2775
+ outputTokens: usageResult.value?.outputTokens
2776
+ };
2777
+ }
2778
+ function median(values) {
2779
+ const sorted = [...values].sort((left, right) => left - right);
2780
+ const middle = Math.floor(sorted.length / 2);
2781
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
2782
+ }
2783
+ function medianOptional(values) {
2784
+ const present = values.filter((value) => value !== void 0);
2785
+ return present.length === 0 ? void 0 : median(present);
2786
+ }
2787
+ function reportProgress(options, modelsTotal, results, active) {
2788
+ options.onProgress?.({
2789
+ active: [...active.values()].sort((left, right) => left.modelIndex - right.modelIndex).map((current) => ({
2790
+ ...current,
2791
+ success: { ...current.success }
2792
+ })),
2793
+ modelsTotal,
2794
+ results: results.map((result) => result === void 0 ? void 0 : {
2795
+ ...result,
2796
+ success: { ...result.success }
2797
+ })
2798
+ });
2799
+ }
2800
+ function throughputOf(measurement) {
2801
+ if (measurement.firstOutputMs === void 0 || measurement.outputTokens === void 0 || measurement.durationMs <= measurement.firstOutputMs) return;
2802
+ return measurement.outputTokens / ((measurement.durationMs - measurement.firstOutputMs) / 1e3);
2803
+ }
2804
+ const corpusLines = [
2805
+ "type QueueEntry = { id: string, attempts: number, payload: string }",
2806
+ "type QueueState = { active: Map<string, QueueEntry>, pending: QueueEntry[] }",
2807
+ "",
2808
+ "export function createQueue(limit: number) {",
2809
+ " const state: QueueState = { active: new Map(), pending: [] }",
2810
+ " let closed = false",
2811
+ "",
2812
+ " function enqueue(entry: QueueEntry): boolean {",
2813
+ " if (closed || state.active.has(entry.id)) return false",
2814
+ " state.pending.push({ ...entry })",
2815
+ " drain()",
2816
+ " return true",
2817
+ " }",
2818
+ "",
2819
+ " function drain(): void {",
2820
+ " while (!closed && state.active.size < limit && state.pending.length > 0) {",
2821
+ " const next = state.pending.shift()",
2822
+ " if (!next) break",
2823
+ " state.active.set(next.id, next)",
2824
+ " void execute(next).finally(() => {",
2825
+ " state.active.delete(next.id)",
2826
+ " drain()",
2827
+ " })",
2828
+ " }",
2829
+ " }",
2830
+ "",
2831
+ " async function execute(entry: QueueEntry): Promise<void> {",
2832
+ " const response = await fetch(\"/jobs/\" + entry.id, {",
2833
+ " body: JSON.stringify(entry.payload),",
2834
+ " headers: { \"content-type\": \"application/json\" },",
2835
+ " method: \"POST\",",
2836
+ " })",
2837
+ " if (!response.ok && entry.attempts < 3) {",
2838
+ " state.pending.push({ ...entry, attempts: entry.attempts + 1 })",
2839
+ " }",
2840
+ " }",
2841
+ "",
2842
+ " function close(): void {",
2843
+ " closed = true",
2844
+ " state.pending.length = 0",
2845
+ " }",
2846
+ "",
2847
+ " return { close, enqueue }",
2848
+ "}",
2849
+ "",
2850
+ "The queue accepts independent jobs and limits concurrent requests.",
2851
+ "Its retry path changes ordering because failed jobs return at the tail.",
2852
+ "Closing drops pending work but does not cancel requests already in flight.",
2853
+ "Callers receive admission status but cannot observe completion or failure.",
2854
+ "The mutable map prevents duplicate active identifiers, not duplicate pending identifiers.",
2855
+ "A synchronous drain call establishes ownership before asynchronous execution starts.",
2856
+ "Network failures reject execute and are not retried by the response-status branch.",
2857
+ "The relative URL assumes a runtime with an application-specific fetch base.",
2858
+ "Payload serialization can throw before a request is created.",
2859
+ "The limit must be positive or pending work can remain stuck forever."
2860
+ ];
2861
+ function sampleCorpus(seed) {
2862
+ const offset = Math.abs(seed) % corpusLines.length;
2863
+ const rotated = [...corpusLines.slice(offset), ...corpusLines.slice(0, offset)].join("\n");
2864
+ return [
2865
+ 1,
2866
+ 2,
2867
+ 3
2868
+ ].map((section) => `--- corpus section ${section} ---\n${rotated}`).join("\n\n");
2869
+ }
2870
+ //#endregion
2871
+ export { AlintAbortError, AlintRunCancelledError, AlintRunError, benchmarkModels, buildRuleRegistry, createBuiltInLanguageRegistry, createSourceExtractor, createSourceFile, createSourceRuntime, defineConfig, definePlugin, defineRule, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, registerLanguage, resolveConfigForDirectory, resolveConfigForFile, resolveConfigForProject, resolveLanguage, resolveModel, runAlint, sliceLines, sliceRange, targetIdentity, withLanguage, withStableIdentities };