@drkmttr/adhere 0.21.2 → 0.23.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.
@@ -0,0 +1,611 @@
1
+ /**
2
+ * A draft rule trialled before it is written: the real lint runtime judges
3
+ * exactly one draft rule over the repository, or the project's own oxlint
4
+ * runs it, and a report the writing agent can read records what happened.
5
+ * Nothing is installed, no rule is written, and no cache is pruned.
6
+ *
7
+ * The draft's config carries the project's runtime settings (model,
8
+ * thresholds, reads, type awareness, comments, excludes) and exactly the
9
+ * draft rule; its preset and project rules never judge anything here.
10
+ */
11
+ import {
12
+ AdhereConfig as AdhereConfigSchema,
13
+ decodeConfig,
14
+ presetsOf,
15
+ type ResolvedConfig,
16
+ resolveConfig,
17
+ type Rule,
18
+ } from "#config.ts";
19
+ import {
20
+ type AdhereTrialDraft,
21
+ oxlintIdOf,
22
+ oxlintProblems,
23
+ runOxlintDraft,
24
+ TrialUnavailable,
25
+ type TrialDraft,
26
+ type TrialFinding,
27
+ type TrialOptions,
28
+ type TrialPreview,
29
+ type TrialRun,
30
+ type TrialScope,
31
+ } from "#create/oxlint-trial.ts";
32
+ import { parseRuleMarkdown } from "#markdown.ts";
33
+ import { importRule, judgesFile } from "#rules.ts";
34
+ import {
35
+ AdhereConfig,
36
+ findConfigFile,
37
+ loadConfigFile,
38
+ loadPreset,
39
+ } from "#services/AdhereConfig.ts";
40
+ import { AuditCache, AuditCacheAt, sha256 } from "#services/AuditCache.ts";
41
+ import { Credentials, CredentialsLive } from "#services/Credentials.ts";
42
+ import { Jev } from "#services/Jev.ts";
43
+ import { JevLive } from "#services/Jev.http.ts";
44
+ import { SourceReads, SourceReadsLive } from "#services/SourceReads.ts";
45
+ import { SourceWalker, SourceWalkerLive, type SourceFile } from "#services/SourceWalker.ts";
46
+ import { workspaceDependentsOf, workspacePackagesOf } from "#workspaces.ts";
47
+ import {
48
+ type AuditPlan,
49
+ type AuditResult,
50
+ executeAudit,
51
+ type Finding,
52
+ planAudit,
53
+ } from "#workflows/audit.ts";
54
+ import { renderJson } from "#workflows/format.ts";
55
+ import { Context, Effect, FileSystem, Layer, Option, Path, Record } from "effect";
56
+ import { relative, sep } from "node:path";
57
+
58
+ export {
59
+ type AdhereTrialDraft,
60
+ type NativeTrialDraft,
61
+ type OxlintTrialDraft,
62
+ type PluginTrialDraft,
63
+ TrialUnavailable,
64
+ type TrialDraft,
65
+ type TrialFinding,
66
+ type TrialOptions,
67
+ type TrialPreview,
68
+ type TrialRange,
69
+ type TrialRun,
70
+ type TrialScope,
71
+ type TrialUnavailableReason,
72
+ } from "#create/oxlint-trial.ts";
73
+
74
+ /** A lower-level trial seam for tests: the real services unless a layer is supplied. */
75
+ export interface TrialServices {
76
+ readonly cache?: Layer.Layer<AuditCache>;
77
+ readonly jev?: Layer.Layer<Jev>;
78
+ readonly sourceReads?: Layer.Layer<SourceReads>;
79
+ }
80
+
81
+ export interface TrialRunOptions extends TrialOptions {
82
+ readonly services?: TrialServices;
83
+ /** The checkout to trial, when it is not the working directory. */
84
+ readonly root?: string;
85
+ }
86
+
87
+ const REPORT_DIRECTORY = ".adhere/trial";
88
+ const DEFAULT_TRIAL_LIMIT = 100;
89
+
90
+ const trialError = (
91
+ reason: Parameters<typeof TrialUnavailable.make>[0]["reason"],
92
+ message: string,
93
+ ) => TrialUnavailable.make({ reason, message });
94
+
95
+ /** A path from the working directory, `/`-separated, as a report names it. */
96
+ const display = (root: string, file: string): string => relative(root, file).split(sep).join("/");
97
+
98
+ const normalized = (file: string): string => file.replaceAll("\\", "/").replace(/^\.\//, "");
99
+
100
+ /** The report's path for a draft, relative to the working directory. */
101
+ export const trialReportPath = (id: string): string =>
102
+ `${REPORT_DIRECTORY}/${encodeURIComponent(id)}.json`;
103
+
104
+ type ProjectConfig = typeof AdhereConfigSchema.Type;
105
+
106
+ const projectConfig = (root: string) =>
107
+ Effect.gen(function* () {
108
+ const path = yield* Path.Path;
109
+ const file = yield* findConfigFile(root);
110
+ return file === undefined
111
+ ? yield* decodeConfig({})
112
+ : yield* loadConfigFile(path.join(root, file));
113
+ });
114
+
115
+ /** The draft rule: a RULE.md parsed, or a RULE.ts imported from a scoped temporary directory. */
116
+ const draftRule = (draft: AdhereTrialDraft) =>
117
+ Effect.gen(function* () {
118
+ if (draft.format === "markdown") {
119
+ return yield* parseRuleMarkdown(draft.source, `${draft.id}/RULE.md`).pipe(
120
+ Effect.mapError((problem) => trialError("draft", problem.message)),
121
+ );
122
+ }
123
+ const fs = yield* FileSystem.FileSystem;
124
+ const path = yield* Path.Path;
125
+ const directory = yield* fs.makeTempDirectoryScoped({ prefix: "adhere-trial-rule-" });
126
+ const file = path.join(directory, "RULE.ts");
127
+ yield* fs.writeFileString(file, draft.source);
128
+ return yield* importRule(file).pipe(
129
+ Effect.mapError((problem) =>
130
+ trialError("draft", problem.message.replaceAll(file, `${draft.id}/RULE.ts`)),
131
+ ),
132
+ );
133
+ });
134
+
135
+ /**
136
+ * The project's runtime settings, with exactly the draft rule in scope.
137
+ * Presets still supply a model or threshold, as they do for lint; their rules
138
+ * and the project's own never enter the trial.
139
+ */
140
+ const draftConfig = (root: string, raw: ProjectConfig, id: string, rule: Rule) =>
141
+ Effect.gen(function* () {
142
+ const registry = Record.fromEntries(
143
+ yield* Effect.forEach(presetsOf(raw, {}), (name) => loadPreset(name, root)),
144
+ );
145
+ const resolved = resolveConfig({ ...raw, rules: {} }, {}, registry);
146
+ const workspacePackages =
147
+ resolved.reads.includes("workspacePackages") || resolved.reads.includes("dependents")
148
+ ? yield* workspacePackagesOf(root)
149
+ : undefined;
150
+ const workspaceDependents =
151
+ resolved.reads.includes("dependents") && workspacePackages !== undefined
152
+ ? yield* workspaceDependentsOf(root, workspacePackages)
153
+ : undefined;
154
+ return {
155
+ ...resolved,
156
+ // Presets still set the runtime settings; their rules never judge here.
157
+ rules: {},
158
+ scopedRules: [{ id, rule, scope: root }],
159
+ ...(workspacePackages === undefined ? {} : { workspacePackages }),
160
+ ...(workspaceDependents === undefined ? {} : { workspaceDependents }),
161
+ } satisfies ResolvedConfig;
162
+ });
163
+
164
+ /** The project's excludes, for a walk that judges nothing: enough to pick oxlint's files. */
165
+ const neutralConfig = (raw: ProjectConfig): ResolvedConfig =>
166
+ resolveConfig({ ...raw, rules: {} }, {});
167
+
168
+ /** Every TypeScript file the project's own walk would read, from the checkout's root. */
169
+ const eligibleFiles = (root: string, config: ResolvedConfig) =>
170
+ Effect.gen(function* () {
171
+ const walker = yield* SourceWalker.pipe(
172
+ Effect.provide(SourceWalkerLive([], ["."], root)),
173
+ Effect.provideService(AdhereConfig, config),
174
+ );
175
+ return { walker, files: yield* walker.files };
176
+ });
177
+
178
+ interface Selection {
179
+ readonly files: ReadonlyArray<SourceFile>;
180
+ /** Sorted repo-relative paths, exactly as run and as the report names them. */
181
+ readonly paths: ReadonlyArray<string>;
182
+ /** Applicable files before the cap. */
183
+ readonly eligible: number;
184
+ }
185
+
186
+ /** The cap is on files, cached ones included; a pinned rerun keeps its exact set. */
187
+ const selectionOf = (
188
+ root: string,
189
+ eligible: ReadonlyArray<SourceFile>,
190
+ applies: (file: SourceFile) => boolean,
191
+ scope: TrialScope,
192
+ pinned: ReadonlyArray<string> | undefined,
193
+ ): Effect.Effect<Selection, TrialUnavailable> =>
194
+ Effect.gen(function* () {
195
+ const sorted = [...eligible].filter(applies).sort((a, b) => a.path.localeCompare(b.path));
196
+ const paths = sorted.map((file) => display(root, file.path));
197
+ if (pinned !== undefined) {
198
+ const wanted = [...new Set(pinned.map(normalized))];
199
+ const known = new Map(sorted.map((file, index) => [paths[index]!, file]));
200
+ const missing = wanted.filter((file) => !known.has(file));
201
+ if (missing.length > 0) {
202
+ return yield* trialError(
203
+ "selection",
204
+ `The pinned trial files are no longer eligible: ${missing.join(", ")}. Run the trial again without pinned files.`,
205
+ );
206
+ }
207
+ return {
208
+ files: wanted.map((file) => known.get(file)!),
209
+ paths: wanted,
210
+ eligible: sorted.length,
211
+ };
212
+ }
213
+ const limit = scope.kind === "files" ? scope.limit : undefined;
214
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
215
+ return yield* trialError(
216
+ "selection",
217
+ `The file cap must be a whole number, zero or more; got ${limit}.`,
218
+ );
219
+ }
220
+ const chosen = limit === undefined ? sorted : sorted.slice(0, limit);
221
+ return {
222
+ files: chosen,
223
+ paths: chosen.map((file) => display(root, file.path)),
224
+ eligible: sorted.length,
225
+ };
226
+ });
227
+
228
+ /** Planning over exactly the selected files, with the draft config and the live cache, never pruning. */
229
+ const planTrial = (
230
+ selection: Selection,
231
+ walker: SourceWalker["Service"],
232
+ context: Context.Context<AdhereConfig | AuditCache>,
233
+ root: string,
234
+ ) =>
235
+ Effect.gen(function* () {
236
+ const selected = Layer.succeed(SourceWalker, {
237
+ files: Effect.succeed(selection.files),
238
+ read: walker.read,
239
+ });
240
+ return yield* planAudit({ root }).pipe(
241
+ Effect.provide(selected),
242
+ Effect.provideContext(context),
243
+ );
244
+ });
245
+
246
+ const requestsOf = (plan: AuditPlan): number => plan.requests + (plan.cachedLocateRequests ?? 0);
247
+
248
+ const previewOf = (plan: AuditPlan, eligible: number, selected: number): TrialPreview => ({
249
+ eligible,
250
+ selected,
251
+ requests: requestsOf(plan),
252
+ tokens: plan.tokens,
253
+ ...(plan.price === undefined ? {} : { price: plan.price }),
254
+ cached: plan.cached,
255
+ deferred: plan.deferred,
256
+ skipped: plan.skipped,
257
+ model: plan.model,
258
+ ...(plan.provider === undefined ? {} : { provider: plan.provider }),
259
+ });
260
+
261
+ /** No request is sent when nothing is pending; a cached run needs no key either. */
262
+ const preflightProvider = (
263
+ config: ResolvedConfig,
264
+ plan: AuditPlan,
265
+ services: TrialServices | undefined,
266
+ ): Effect.Effect<void, TrialUnavailable, never> =>
267
+ Effect.gen(function* () {
268
+ if (requestsOf(plan) === 0) return;
269
+ const provider = config.provider;
270
+ // A provider without a saved key authenticates itself; nothing to check here.
271
+ if (provider !== undefined && provider.savedKey === undefined) return;
272
+ // A supplied Jev (tests, or another provider) already decides how it asks.
273
+ if (services?.jev !== undefined) return;
274
+ const credentials = yield* Effect.serviceOption(Credentials);
275
+ if (Option.isNone(credentials)) return;
276
+ yield* credentials.value
277
+ .key(provider?.savedKey ?? "typesafe")
278
+ .pipe(Effect.mapError((problem) => trialError("provider", problem.message)));
279
+ });
280
+
281
+ /** A finding as the trial reports it: this rule's, with its section and excerpt. */
282
+ const trialFinding = (root: string, finding: Finding): TrialFinding => ({
283
+ rule: finding.preset === undefined ? finding.rule : `${finding.preset}/${finding.rule}`,
284
+ file: display(root, finding.file),
285
+ section: finding.section,
286
+ ...(finding.lines === undefined ? {} : { lines: finding.lines }),
287
+ excerpt: finding.excerpt.lines.join("\n"),
288
+ probability: finding.probability,
289
+ threshold: finding.threshold,
290
+ ...(finding.context === undefined ? {} : { context: finding.context }),
291
+ });
292
+
293
+ const revisionOf = (parts: unknown) => sha256(JSON.stringify(parts));
294
+
295
+ const reportPathFor = (id: string) => trialReportPath(id);
296
+
297
+ /** Writes the report under `.adhere/trial/`, never `.adhere/reports/`. */
298
+ const writeReport = (root: string, id: string, artifact: unknown) =>
299
+ Effect.gen(function* () {
300
+ const fs = yield* FileSystem.FileSystem;
301
+ const path = yield* Path.Path;
302
+ const directory = path.join(root, REPORT_DIRECTORY);
303
+ yield* fs.makeDirectory(directory, { recursive: true });
304
+ const text = `${JSON.stringify(artifact, null, 2)}\n`;
305
+ yield* fs.writeFileString(path.join(root, trialReportPath(id)), text);
306
+ yield* fs.writeFileString(path.join(directory, "report.json"), text);
307
+ }).pipe(
308
+ Effect.mapError((problem) =>
309
+ trialError(
310
+ "other",
311
+ `Could not write the trial report: ${problem instanceof Error ? problem.message : String(problem)}`,
312
+ ),
313
+ ),
314
+ );
315
+
316
+ /**
317
+ * What a trial would do, without sending anything, writing a report, or
318
+ * touching the cache. An adhere draft is really planned, so requests, tokens,
319
+ * cached checks, and the exact selected files are the run's.
320
+ */
321
+ export const previewTrial = (draft: TrialDraft, scope: TrialScope, options: TrialRunOptions = {}) =>
322
+ Effect.scoped(
323
+ Effect.gen(function* () {
324
+ const path = yield* Path.Path;
325
+ const root = path.resolve(options.root ?? ".");
326
+ if (draft.kind !== "adhere") {
327
+ const problem = oxlintProblems(draft, root)[0];
328
+ if (problem !== undefined) {
329
+ return yield* trialError(problem.reason, problem.message);
330
+ }
331
+ const raw = yield* projectConfig(root);
332
+ const config = neutralConfig(raw);
333
+ const { files } = yield* eligibleFiles(root, config);
334
+ const selected = yield* selectionOf(root, files, () => true, scope, options.files);
335
+ return {
336
+ eligible: selected.eligible,
337
+ selected: selected.paths.length,
338
+ requests: 0,
339
+ tokens: 0,
340
+ cached: 0,
341
+ deferred: 0,
342
+ skipped: 0,
343
+ } satisfies TrialPreview;
344
+ }
345
+ const raw = yield* projectConfig(root);
346
+ const rule = yield* draftRule(draft);
347
+ const config = yield* draftConfig(root, raw, draft.id, rule);
348
+ const { walker, files } = yield* eligibleFiles(root, config);
349
+ const selected = yield* selectionOf(
350
+ root,
351
+ files,
352
+ (file) => judgesFile(rule, file.test === true),
353
+ scope,
354
+ options.files,
355
+ );
356
+ const cache = options.services?.cache ?? AuditCacheAt(root);
357
+ const context = yield* Layer.build(
358
+ Layer.mergeAll(Layer.succeed(AdhereConfig, config), cache),
359
+ );
360
+ const plan = yield* planTrial(selected, walker, context, root);
361
+ return previewOf(plan, selected.eligible, selected.paths.length);
362
+ }),
363
+ ).pipe(
364
+ Effect.catchTag("ConfigUnavailable", (problem) =>
365
+ Effect.fail(trialError("other", problem.message)),
366
+ ),
367
+ Effect.catchTag("WalkUnavailable", (problem) =>
368
+ Effect.fail(
369
+ trialError(
370
+ /Native TypeScript|typescript|compiler/i.test(problem.message) ? "compiler" : "other",
371
+ problem.message,
372
+ ),
373
+ ),
374
+ ),
375
+ Effect.catch((problem) =>
376
+ problem instanceof TrialUnavailable
377
+ ? Effect.fail(problem)
378
+ : Effect.fail(
379
+ trialError(
380
+ "other",
381
+ `The trial could not plan: ${problem instanceof Error ? problem.message : String(problem)}`,
382
+ ),
383
+ ),
384
+ ),
385
+ );
386
+
387
+ /**
388
+ * Runs the draft over exactly the selected files: adhere's plan and execution
389
+ * for a rule, the project's own oxlint for a native or plugin rule. Reports
390
+ * what happened, never that the rule is valid; the report is in
391
+ * `.adhere/trial/` for the writing agent to read.
392
+ */
393
+ export const runTrial = (draft: TrialDraft, scope: TrialScope, options: TrialRunOptions = {}) =>
394
+ Effect.scoped(
395
+ Effect.gen(function* () {
396
+ const path = yield* Path.Path;
397
+ const root = path.resolve(options.root ?? ".");
398
+ const write = options.writeReport !== false;
399
+ if (draft.kind !== "adhere") {
400
+ const raw = yield* projectConfig(root);
401
+ const config = neutralConfig(raw);
402
+ const { files } = yield* eligibleFiles(root, config);
403
+ const selected = yield* selectionOf(root, files, () => true, scope, options.files);
404
+ const run = yield* runOxlintDraft(root, draft, selected.paths);
405
+ const id = oxlintIdOf(draft);
406
+ const revision = yield* revisionOf({
407
+ kind: draft.kind,
408
+ draft,
409
+ scope,
410
+ selectedFiles: selected.paths,
411
+ });
412
+ const reportPath = reportPathFor(id);
413
+ const notes = [
414
+ ...run.notes,
415
+ ...(selected.paths.length === 0
416
+ ? ["No applicable files were selected for this scope."]
417
+ : []),
418
+ ];
419
+ if (write) {
420
+ yield* writeReport(root, id, {
421
+ kind: "oxlint",
422
+ mode: draft.kind,
423
+ revision,
424
+ draft,
425
+ scope,
426
+ eligible: selected.eligible,
427
+ selectedFiles: selected.paths,
428
+ lint: {
429
+ binary: run.binary,
430
+ judged: run.judged,
431
+ diagnostics: run.diagnostics,
432
+ },
433
+ findings: run.findings,
434
+ notes,
435
+ });
436
+ }
437
+ return {
438
+ draft,
439
+ scope,
440
+ revision,
441
+ selectedFiles: selected.paths,
442
+ files: selected.paths.length,
443
+ judged: run.judged,
444
+ cached: 0,
445
+ skipped: 0,
446
+ waiting: 0,
447
+ findings: run.findings,
448
+ reportPath,
449
+ notes,
450
+ } satisfies TrialRun;
451
+ }
452
+
453
+ const raw = yield* projectConfig(root);
454
+ const rule = yield* draftRule(draft);
455
+ const config = yield* draftConfig(root, raw, draft.id, rule);
456
+ const { walker, files } = yield* eligibleFiles(root, config);
457
+ const selected = yield* selectionOf(
458
+ root,
459
+ files,
460
+ (file) => judgesFile(rule, file.test === true),
461
+ scope,
462
+ options.files,
463
+ );
464
+ const cache = options.services?.cache ?? AuditCacheAt(root);
465
+ const jev =
466
+ options.services?.jev ??
467
+ JevLive.pipe(
468
+ Layer.provideMerge(CredentialsLive),
469
+ Layer.provide(Layer.succeed(AdhereConfig, config)),
470
+ );
471
+ const sourceReads = options.services?.sourceReads ?? SourceReadsLive(root);
472
+ const context = yield* Layer.build(
473
+ Layer.mergeAll(Layer.succeed(AdhereConfig, config), cache, jev, sourceReads),
474
+ );
475
+ const plan = yield* planTrial(selected, walker, context, root);
476
+ yield* preflightProvider(config, plan, options.services).pipe(Effect.provideContext(context));
477
+ const result: AuditResult = yield* executeAudit(plan).pipe(
478
+ Effect.provide(
479
+ Layer.succeed(SourceWalker, {
480
+ files: Effect.succeed(selected.files),
481
+ read: walker.read,
482
+ }),
483
+ ),
484
+ Effect.provideContext(context),
485
+ );
486
+ const revision = yield* revisionOf({
487
+ kind: "adhere",
488
+ id: draft.id,
489
+ format: draft.format,
490
+ source: draft.source,
491
+ scope,
492
+ selectedFiles: selected.paths,
493
+ runtime: {
494
+ model: config.model,
495
+ provider: config.provider?.model,
496
+ threshold: config.threshold,
497
+ sufficiencyThreshold: config.sufficiencyThreshold,
498
+ reads: config.reads,
499
+ typeAware: config.typeAware,
500
+ includeComments: config.includeComments === true,
501
+ },
502
+ });
503
+ const reportPath = reportPathFor(draft.id);
504
+ // The rule declares what would help; the config still decides what runs,
505
+ // so lint warns and this trial judged the files without that context.
506
+ const missingReads = (rule.reads ?? []).filter((read) => !config.reads.includes(read));
507
+ const notes = [
508
+ ...(missingReads.length > 0
509
+ ? [
510
+ `The rule declares reads ${missingReads.join(", ")}, which this run does not enable; it judged the files without that context and may be less accurate until the config enables it.`,
511
+ ]
512
+ : []),
513
+ ...(result.cached > 0
514
+ ? [
515
+ `${result.cached} selected files were answered from cache for this exact rule text; editing the draft re-judges only what the edit changes.`,
516
+ ]
517
+ : []),
518
+ ...(result.waiting > 0
519
+ ? [`${result.waiting} selected files were left for a later run.`]
520
+ : []),
521
+ ...(result.skipped > 0
522
+ ? [
523
+ `${result.skipped} selected files were too long to judge with this rule and its context.`,
524
+ ]
525
+ : []),
526
+ ...(config.typeAware
527
+ ? [
528
+ "Type-aware reads were requested; symbols and references came from the installed TypeScript compiler.",
529
+ ]
530
+ : []),
531
+ ...(requestsOf(plan) === 0
532
+ ? ["No requests were needed for this selection."]
533
+ : [`The plan estimated ${requestsOf(plan)} requests.`]),
534
+ ];
535
+ if (write) {
536
+ yield* writeReport(root, draft.id, {
537
+ kind: "adhere",
538
+ revision,
539
+ draft,
540
+ scope,
541
+ eligible: selected.eligible,
542
+ selectedFiles: selected.paths,
543
+ plan: {
544
+ model: plan.model,
545
+ ...(plan.provider === undefined ? {} : { provider: plan.provider }),
546
+ threshold: plan.threshold,
547
+ sufficiencyThreshold: plan.sufficiencyThreshold,
548
+ requests: requestsOf(plan),
549
+ tokens: plan.tokens,
550
+ ...(plan.price === undefined ? {} : { price: plan.price }),
551
+ rules: plan.rules,
552
+ checks: plan.checks,
553
+ cached: plan.cached,
554
+ skipped: plan.skipped,
555
+ deferred: plan.deferred,
556
+ },
557
+ result: JSON.parse(
558
+ renderJson(result, {
559
+ root,
560
+ threshold: plan.threshold,
561
+ sufficiencyThreshold: plan.sufficiencyThreshold,
562
+ ...(plan.provider === undefined ? {} : { provider: plan.provider }),
563
+ }),
564
+ ),
565
+ notes,
566
+ });
567
+ }
568
+ return {
569
+ draft,
570
+ scope,
571
+ revision,
572
+ selectedFiles: selected.paths,
573
+ files: result.files,
574
+ judged: result.judged,
575
+ cached: result.cached,
576
+ skipped: result.skipped,
577
+ waiting: result.waiting,
578
+ findings: result.findings.map((finding) => trialFinding(root, finding)),
579
+ reportPath,
580
+ ...(plan.provider === undefined ? {} : { provider: plan.provider }),
581
+ notes,
582
+ } satisfies TrialRun;
583
+ }),
584
+ ).pipe(
585
+ Effect.catchTag("ConfigUnavailable", (problem) =>
586
+ Effect.fail(trialError("other", problem.message)),
587
+ ),
588
+ Effect.catchTag("WalkUnavailable", (problem) =>
589
+ Effect.fail(
590
+ trialError(
591
+ /Native TypeScript|typescript|compiler/i.test(problem.message) ? "compiler" : "other",
592
+ problem.message,
593
+ ),
594
+ ),
595
+ ),
596
+ Effect.catchTag("JevUnavailable", (problem) =>
597
+ Effect.fail(trialError("provider", problem.message)),
598
+ ),
599
+ Effect.catch((problem) =>
600
+ problem instanceof TrialUnavailable
601
+ ? Effect.fail(problem)
602
+ : Effect.fail(
603
+ trialError(
604
+ "other",
605
+ `The trial could not finish: ${problem instanceof Error ? problem.message : String(problem)}`,
606
+ ),
607
+ ),
608
+ ),
609
+ );
610
+
611
+ export { DEFAULT_TRIAL_LIMIT };