@drkmttr/adhere 0.9.4 → 0.9.5
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/README.md +15 -3
- package/package.json +6 -6
- package/src/contradictions.ts +4 -2
- package/src/mechanical.ts +2 -2
- package/src/presets.ts +3 -0
- package/src/pricing.ts +14 -0
- package/src/rules.ts +10 -1
- package/src/services/AdhereConfig.ts +5 -2
- package/src/services/Jev.ts +17 -0
- package/src/wording.ts +2 -2
- package/src/workflows/audit.ts +38 -10
- package/src/workflows/format.ts +43 -3
package/README.md
CHANGED
|
@@ -66,7 +66,10 @@ Found 1 error.
|
|
|
66
66
|
42 files, 3 judged, 39 cached.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
The header is the rule id, Jev's probability, and the rule's description.
|
|
69
|
+
The header is the rule id, Jev's probability, and the rule's description. A
|
|
70
|
+
preset's rule has the preset's name first, as in
|
|
71
|
+
`effect/basics/external-calls-are-resilient`, so a report that mixes presets
|
|
72
|
+
with a repo's own rules says where each came from.
|
|
70
73
|
Under it is the line Jev points at, underlined, with the code around it: the
|
|
71
74
|
largest statement around the line that is 30 lines or fewer, usually the whole
|
|
72
75
|
function, and up to 3 lines of whole statements on either side. The
|
|
@@ -111,9 +114,17 @@ what judging takes:
|
|
|
111
114
|
```text
|
|
112
115
|
200 files and 14 rules: 2800 checks, 1400 cached.
|
|
113
116
|
Judging the other 1400 takes 200 requests to Jev, plus 1 or more for each file with a finding.
|
|
114
|
-
|
|
117
|
+
Those carry about 1.9 million input tokens: about $0.08 at $0.042 per million, and more for locating findings.
|
|
118
|
+
? Send 200 requests to Jev, about $0.08? › (Y/n)
|
|
115
119
|
```
|
|
116
120
|
|
|
121
|
+
The cost is adhere's estimate of the input tokens times the model's price.
|
|
122
|
+
Jev charges only for input tokens: $0.042 a million for `jev-latest`, per
|
|
123
|
+
[TypeSafe AI's models page](https://docs.typesafe.ai/models) in September 2026.
|
|
124
|
+
The estimate runs about a tenth high, and locating findings, which depends on
|
|
125
|
+
what judging finds, comes on top. For a model adhere has no price for, the plan
|
|
126
|
+
gives the tokens alone.
|
|
127
|
+
|
|
117
128
|
It asks only with a terminal on stdin and stdout, and never when the cache
|
|
118
129
|
answers every check; `--yes` sends without asking. While it runs, a counter on
|
|
119
130
|
stderr shows the files done, the requests sent, and the findings so far, and
|
|
@@ -377,7 +388,8 @@ A preset has the shape of a config without `presets`: `rules`, and optionally
|
|
|
377
388
|
`model` and `threshold`. Name presets in the config's `presets`, or on the
|
|
378
389
|
command line with `--preset`, repeated or separated by commas, as in
|
|
379
390
|
`--preset effect,alchemy`, in which case the config file is optional. Presets
|
|
380
|
-
named in both places apply together.
|
|
391
|
+
named in both places apply together. Reports name a preset's rule with the
|
|
392
|
+
preset first, as in `alchemy/secrets/…`. There are two:
|
|
381
393
|
|
|
382
394
|
- `effect`: Markdown rules in [`presets/effect/`](./presets/effect/), lifted
|
|
383
395
|
from the [effect-solutions](https://github.com/kitlangton/effect-solutions)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@drkmttr/adhere-darwin-arm64": "0.9.
|
|
72
|
-
"@drkmttr/adhere-darwin-x64": "0.9.
|
|
73
|
-
"@drkmttr/adhere-linux-arm64": "0.9.
|
|
74
|
-
"@drkmttr/adhere-linux-x64": "0.9.
|
|
75
|
-
"@drkmttr/adhere-win32-x64": "0.9.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.9.5",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.9.5",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.9.5",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.9.5",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.9.5"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/contradictions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RuleEntry, shownId } from "#rules.ts";
|
|
2
2
|
import { Jev, type Pair } from "#services/Jev.ts";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
|
|
@@ -69,7 +69,9 @@ export const findContradictions = Effect.fn("findContradictions")(function* (
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** Where a rule lives: a preset's names the preset in its id; the project's own rule names its file. */
|
|
73
|
+
const locationOf = (entry: RuleEntry): string =>
|
|
74
|
+
entry.preset === undefined ? `${entry.id} (${entry.file ?? entry.scope})` : shownId(entry);
|
|
73
75
|
|
|
74
76
|
export const formatContradictions = (contradictions: ReadonlyArray<Contradiction>): string => {
|
|
75
77
|
if (contradictions.length === 0) {
|
package/src/mechanical.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Rule } from "#config.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type RuleEntry, shownId } from "#rules.ts";
|
|
3
3
|
import { AuditCache, sha256, type Tally } from "#services/AuditCache.ts";
|
|
4
4
|
import { linterQuestion } from "#services/Jev.ts";
|
|
5
5
|
import { Effect } from "effect";
|
|
@@ -76,7 +76,7 @@ export const formatLinterCheck = (
|
|
|
76
76
|
: [
|
|
77
77
|
`${flagged.length} ${flagged.length === 1 ? "rule" : "rules"} flagged by the linter check:`,
|
|
78
78
|
...flagged.flatMap((one) => [
|
|
79
|
-
` ${one.entry
|
|
79
|
+
` ${shownId(one.entry)}${one.entry.file === undefined ? "" : ` (${displayPath(one.entry.file, root)})`}`,
|
|
80
80
|
` ${adviceOf(one)}`,
|
|
81
81
|
]),
|
|
82
82
|
]),
|
package/src/presets.ts
CHANGED
|
@@ -57,4 +57,7 @@ export const presets: Readonly<Record<WholePreset, BuiltInPreset>> = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
/** The directory each whole preset reads, for checking `topics` against it. */
|
|
60
|
+
/** The whole preset a name belongs to: effect, for effect and for effect/basics alike. */
|
|
61
|
+
export const wholePresetOf = (name: PresetName): string => name.split("/")[0] ?? name;
|
|
62
|
+
|
|
60
63
|
export const topicsOf = (preset: WholePreset): ReadonlyArray<string> => topics[preset];
|
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev's price for input tokens, in dollars per million, by the name a config
|
|
3
|
+
* sends as its model. Output tokens are free. From TypeSafe AI's models page,
|
|
4
|
+
* https://docs.typesafe.ai/models, on 2026-09-25, when jev-latest and
|
|
5
|
+
* jev-preview both pointed to jev-1.13.0.
|
|
6
|
+
*/
|
|
7
|
+
const DOLLARS_PER_MILLION: Readonly<Record<string, number>> = {
|
|
8
|
+
"jev-1.13.0": 0.042,
|
|
9
|
+
"jev-latest": 0.042,
|
|
10
|
+
"jev-preview": 0.042,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** The model's price per million input tokens, when adhere knows it. */
|
|
14
|
+
export const priceOf = (model: string): number | undefined => DOLLARS_PER_MILLION[model];
|
package/src/rules.ts
CHANGED
|
@@ -20,10 +20,19 @@ export interface RuleEntry {
|
|
|
20
20
|
readonly rule: Rule;
|
|
21
21
|
readonly scope: string;
|
|
22
22
|
readonly file?: string;
|
|
23
|
-
/** The built-in
|
|
23
|
+
/** The built-in preset the rule came from, by its whole name (effect for effect/basics), when it is not the project's own. */
|
|
24
24
|
readonly preset?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A rule's id as output shows it: a preset's rule with the preset's name
|
|
29
|
+
* first, as in effect/basics/external-calls-are-resilient, so a report that
|
|
30
|
+
* mixes presets with the project's own rules says where each came from. The
|
|
31
|
+
* project's own rules show their `.adhere/` ids as they are.
|
|
32
|
+
*/
|
|
33
|
+
export const shownId = (entry: { readonly id: RuleId; readonly preset?: string }): string =>
|
|
34
|
+
entry.preset === undefined ? entry.id : `${entry.preset}/${entry.id}`;
|
|
35
|
+
|
|
27
36
|
export type RuleSet = ReadonlyArray<RuleEntry>;
|
|
28
37
|
|
|
29
38
|
const ADHERE_SEGMENT = ".adhere";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ResolvedConfig,
|
|
12
12
|
resolveConfig,
|
|
13
13
|
} from "#config.ts";
|
|
14
|
-
import { type PresetName, presetOf } from "#presets.ts";
|
|
14
|
+
import { type PresetName, presetOf, wholePresetOf } from "#presets.ts";
|
|
15
15
|
import {
|
|
16
16
|
applicableRules,
|
|
17
17
|
globalRuleSet,
|
|
@@ -95,7 +95,10 @@ export const AdhereConfigLive = (overrides: Overrides) =>
|
|
|
95
95
|
).pipe(Effect.map(Record.fromEntries));
|
|
96
96
|
const resolved = resolveConfig({ ...config, rules }, overrides, registry);
|
|
97
97
|
const presetEntries = Object.entries(registry).flatMap(([name, preset]) =>
|
|
98
|
-
globalRuleSet(preset.rules, cwd).map((entry) => ({
|
|
98
|
+
globalRuleSet(preset.rules, cwd).map((entry) => ({
|
|
99
|
+
...entry,
|
|
100
|
+
preset: wholePresetOf(name as PresetName),
|
|
101
|
+
})),
|
|
99
102
|
);
|
|
100
103
|
yield* Effect.logDebug(
|
|
101
104
|
`config: ${configFile ?? "no config file"}; presets ${Object.keys(registry).join(", ") || "none"}; ${presetEntries.length} preset and ${projectEntries.length} project rules; model ${resolved.model}, threshold ${resolved.threshold}${resolved.rpm === undefined ? "" : `, at most ${resolved.rpm} requests a minute`}`,
|
package/src/services/Jev.ts
CHANGED
|
@@ -344,6 +344,23 @@ export const requestsOf = (body: Body): ReadonlyArray<Body> => {
|
|
|
344
344
|
return requests.map(({ questions }) => ({ ...body, questions }));
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* The requests judging `rules` over a file takes, and about how many input
|
|
349
|
+
* tokens they carry, by `tokensOf`: the judge body, split to fit, as `judge`
|
|
350
|
+
* sends it, each request with the file again.
|
|
351
|
+
*/
|
|
352
|
+
export const judgeLoad = (
|
|
353
|
+
lines: Lines,
|
|
354
|
+
rules: Rules,
|
|
355
|
+
sampled: ReadonlyArray<RuleId> = [],
|
|
356
|
+
): { readonly requests: number; readonly tokens: number } => {
|
|
357
|
+
const requests = requestsOf(judgeBody("", lines, rules, sampled));
|
|
358
|
+
return {
|
|
359
|
+
requests: requests.length,
|
|
360
|
+
tokens: requests.reduce((sum, request) => sum + tokensOf(request), 0),
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
|
|
347
364
|
/** The requests judging `rules` over a file takes: the judge body, split to fit, as `judge` sends it. */
|
|
348
365
|
export const judgeRequests = (
|
|
349
366
|
lines: Lines,
|
package/src/wording.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Example, examplesOf, type Rule } from "#config.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type RuleEntry, shownId } from "#rules.ts";
|
|
3
3
|
|
|
4
4
|
type Word = Example["word"];
|
|
5
5
|
|
|
@@ -83,7 +83,7 @@ export const formatStrays = (strays: ReadonlyArray<Stray>, root: string): Readon
|
|
|
83
83
|
: [
|
|
84
84
|
`${strays.length} ${strays.length === 1 ? "rule is" : "rules are"} not worded as the rule writing tips recommend:`,
|
|
85
85
|
...strays.flatMap(({ entry, straying }) => [
|
|
86
|
-
` ${entry
|
|
86
|
+
` ${shownId(entry)}${entry.file === undefined ? "" : ` (${displayPath(entry.file, root)})`}`,
|
|
87
87
|
...straying.map((one) => ` ${sentenceOf(one)}`),
|
|
88
88
|
]),
|
|
89
89
|
`Rule writing tips: ${TIPS_URL}`,
|
package/src/workflows/audit.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
JevUnavailable,
|
|
10
10
|
type Judged,
|
|
11
11
|
judgeQuestion,
|
|
12
|
+
judgeLoad,
|
|
12
13
|
judgeRequests,
|
|
13
14
|
linterQuestion,
|
|
14
15
|
locateRequests,
|
|
@@ -18,11 +19,14 @@ import {
|
|
|
18
19
|
import { SourceWalker } from "#services/SourceWalker.ts";
|
|
19
20
|
import { clearingStatus, counted, countOf, Status } from "#services/Status.ts";
|
|
20
21
|
import { SAMPLE, tallyKeyOf } from "#mechanical.ts";
|
|
22
|
+
import { priceOf } from "#pricing.ts";
|
|
21
23
|
import { applicableRules } from "#rules.ts";
|
|
22
24
|
import { type Crypto, Duration, Effect, Record, Result, Semaphore } from "effect";
|
|
23
25
|
|
|
24
26
|
export interface Finding {
|
|
25
27
|
readonly rule: RuleId;
|
|
28
|
+
/** The preset the rule came from, when it is not the project's own. */
|
|
29
|
+
readonly preset?: string;
|
|
26
30
|
readonly description: string;
|
|
27
31
|
readonly examples: Examples;
|
|
28
32
|
readonly file: string;
|
|
@@ -60,6 +64,8 @@ interface PreparedRule {
|
|
|
60
64
|
readonly rule: Rule;
|
|
61
65
|
readonly fingerprint: string;
|
|
62
66
|
readonly threshold: number;
|
|
67
|
+
/** The preset the rule came from, when it is not the project's own. */
|
|
68
|
+
readonly preset?: string;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
const fileResult = (
|
|
@@ -108,6 +114,10 @@ export interface AuditPlan {
|
|
|
108
114
|
readonly deferred: number;
|
|
109
115
|
/** Requests to Jev that judging the other checks takes. Locating findings adds 1 or more per file. */
|
|
110
116
|
readonly requests: number;
|
|
117
|
+
/** About how many input tokens those requests carry, which Jev charges for. */
|
|
118
|
+
readonly tokens: number;
|
|
119
|
+
/** The model's price per million input tokens, in dollars, when adhere knows it. */
|
|
120
|
+
readonly price?: number;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
/** A file finished, for a progress counter: the requests it took and the findings it has. */
|
|
@@ -194,11 +204,18 @@ export const planAudit = (
|
|
|
194
204
|
const prepared: Record<RuleId, PreparedRule> = yield* Effect.forEach(
|
|
195
205
|
Object.entries(rules),
|
|
196
206
|
([id, rule]) =>
|
|
197
|
-
Effect.map(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
Effect.map(fingerprintOf(config.model, rule), (fingerprint) => {
|
|
208
|
+
const preset = presetOfRule.get(rule);
|
|
209
|
+
return [
|
|
210
|
+
id,
|
|
211
|
+
{
|
|
212
|
+
rule,
|
|
213
|
+
fingerprint,
|
|
214
|
+
threshold: rule.threshold ?? config.threshold,
|
|
215
|
+
...(preset === undefined ? {} : { preset }),
|
|
216
|
+
},
|
|
217
|
+
] as const;
|
|
218
|
+
}),
|
|
202
219
|
).pipe(Effect.map(Record.fromEntries));
|
|
203
220
|
const answers = yield* cache.get(hash, file.path);
|
|
204
221
|
const kept: Record<RuleId, Judgment> = Record.filterMap(prepared, ({ fingerprint }) => {
|
|
@@ -221,6 +238,12 @@ export const planAudit = (
|
|
|
221
238
|
} satisfies FilePlan;
|
|
222
239
|
});
|
|
223
240
|
|
|
241
|
+
// Each preset's rule, with the preset it came from, for its findings to name.
|
|
242
|
+
const presetOfRule = new Map(
|
|
243
|
+
(config.scopedRules ?? []).flatMap((entry) =>
|
|
244
|
+
entry.preset === undefined ? [] : [[entry.rule, entry.preset] as const],
|
|
245
|
+
),
|
|
246
|
+
);
|
|
224
247
|
// Preset rules are not the project's to change, so the linter check leaves them out.
|
|
225
248
|
const presetRules = new Set(
|
|
226
249
|
(config.scopedRules ?? []).flatMap((entry) =>
|
|
@@ -304,6 +327,12 @@ export const planAudit = (
|
|
|
304
327
|
options.limit === undefined ? everything : withinLimit(everything, options.limit);
|
|
305
328
|
const planned = yield* withSamples(limited);
|
|
306
329
|
const judged = planned.filter((plan) => !plan.skipped);
|
|
330
|
+
const loads = judged.map((plan) =>
|
|
331
|
+
isEmpty(plan.pending)
|
|
332
|
+
? { requests: 0, tokens: 0 }
|
|
333
|
+
: judgeLoad(plan.file.lines, rulesOf(plan.pending), Object.keys(plan.sampled)),
|
|
334
|
+
);
|
|
335
|
+
const price = priceOf(config.model);
|
|
307
336
|
const total = (count: (plan: FilePlan) => number) =>
|
|
308
337
|
judged.reduce((sum, plan) => sum + count(plan), 0);
|
|
309
338
|
return {
|
|
@@ -313,11 +342,9 @@ export const planAudit = (
|
|
|
313
342
|
cached: total((plan) => sizeOf(plan.kept)),
|
|
314
343
|
skipped: planned.length - judged.length,
|
|
315
344
|
deferred: total((plan) => plan.deferred),
|
|
316
|
-
requests:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
: judgeRequests(plan.file.lines, rulesOf(plan.pending), Object.keys(plan.sampled)),
|
|
320
|
-
),
|
|
345
|
+
requests: loads.reduce((sum, load) => sum + load.requests, 0),
|
|
346
|
+
tokens: loads.reduce((sum, load) => sum + load.tokens, 0),
|
|
347
|
+
...(price === undefined ? {} : { price }),
|
|
321
348
|
};
|
|
322
349
|
});
|
|
323
350
|
|
|
@@ -417,6 +444,7 @@ export const executeAudit = (
|
|
|
417
444
|
? [
|
|
418
445
|
{
|
|
419
446
|
rule: id,
|
|
447
|
+
...(k.preset === undefined ? {} : { preset: k.preset }),
|
|
420
448
|
description: k.rule.description,
|
|
421
449
|
examples: examplesOf(k.rule),
|
|
422
450
|
file: file.path,
|
package/src/workflows/format.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Kind, tokenize } from "#highlight.ts";
|
|
2
|
+
import { shownId } from "#rules.ts";
|
|
2
3
|
import type { AuditPlan, AuditResult, FileDone, Finding } from "#workflows/audit.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -91,7 +92,7 @@ const header = (finding: Finding): Line => [
|
|
|
91
92
|
span(" "),
|
|
92
93
|
span("×", "error"),
|
|
93
94
|
span(" "),
|
|
94
|
-
span(finding.rule, "error"),
|
|
95
|
+
span(shownId({ id: finding.rule, preset: finding.preset }), "error"),
|
|
95
96
|
span(" ("),
|
|
96
97
|
span(finding.probability.toFixed(2), "probability"),
|
|
97
98
|
span("): "),
|
|
@@ -224,6 +225,41 @@ const durationAt = (requests: number, rpm: number): string => {
|
|
|
224
225
|
: counted(Math.round(seconds / 60), "minute", "minutes");
|
|
225
226
|
};
|
|
226
227
|
|
|
228
|
+
/** A number to one decimal place, without a trailing .0: 3.2, 43. */
|
|
229
|
+
const tenths = (n: number): string => Number(n.toFixed(1)).toLocaleString("en-US");
|
|
230
|
+
|
|
231
|
+
/** A token count as the plan says it: 612, 41,000, 3.2 million, 43 million, 1.1 billion. */
|
|
232
|
+
const tokensSaid = (tokens: number): string =>
|
|
233
|
+
tokens >= 1e9
|
|
234
|
+
? `${tenths(tokens / 1e9)} billion`
|
|
235
|
+
: tokens >= 1e6
|
|
236
|
+
? `${tenths(tokens / 1e6)} million`
|
|
237
|
+
: tokens >= 1e3
|
|
238
|
+
? (Math.round(tokens / 1e3) * 1e3).toLocaleString("en-US")
|
|
239
|
+
: tokens.toLocaleString("en-US");
|
|
240
|
+
|
|
241
|
+
/** An estimated cost as the plan says it: under $0.01, about $0.14, about $1,234. */
|
|
242
|
+
const dollars = (amount: number): string =>
|
|
243
|
+
amount < 0.01
|
|
244
|
+
? "under $0.01"
|
|
245
|
+
: amount < 100
|
|
246
|
+
? `about $${amount.toFixed(2)}`
|
|
247
|
+
: `about $${Math.round(amount).toLocaleString("en-US")}`;
|
|
248
|
+
|
|
249
|
+
/** What judging the plan costs at the model's price, when adhere knows it. */
|
|
250
|
+
const costOf = (plan: AuditPlan): number | undefined =>
|
|
251
|
+
plan.price === undefined ? undefined : (plan.tokens / 1e6) * plan.price;
|
|
252
|
+
|
|
253
|
+
/** The plan's input tokens and their cost; locating findings comes on top, since it depends on the findings. */
|
|
254
|
+
const costLine = (plan: AuditPlan): string => {
|
|
255
|
+
const cost = costOf(plan);
|
|
256
|
+
const priced =
|
|
257
|
+
cost === undefined || plan.price === undefined
|
|
258
|
+
? ""
|
|
259
|
+
: `: ${dollars(cost)} at $${plan.price} per million`;
|
|
260
|
+
return `Those carry about ${tokensSaid(plan.tokens)} input tokens${priced}, and more for locating findings.`;
|
|
261
|
+
};
|
|
262
|
+
|
|
227
263
|
export const describePlan = (plan: AuditPlan, limits: RunLimits = {}): ReadonlyArray<string> => {
|
|
228
264
|
const cached = plan.cached > 0 ? `, ${plan.cached} cached` : "";
|
|
229
265
|
const skipped =
|
|
@@ -260,12 +296,16 @@ export const describePlan = (plan: AuditPlan, limits: RunLimits = {}): ReadonlyA
|
|
|
260
296
|
return [
|
|
261
297
|
found,
|
|
262
298
|
`Judging ${rest} takes ${counted(plan.requests, "request", "requests")} to Jev, plus 1 or more for each file with a finding.${pace}${waiting}`,
|
|
299
|
+
...(plan.tokens > 0 ? [costLine(plan)] : []),
|
|
263
300
|
];
|
|
264
301
|
};
|
|
265
302
|
|
|
266
303
|
/** The question before a run sends anything. */
|
|
267
|
-
export const sendQuestion = (plan: AuditPlan): string =>
|
|
268
|
-
|
|
304
|
+
export const sendQuestion = (plan: AuditPlan): string => {
|
|
305
|
+
const cost = costOf(plan);
|
|
306
|
+
const priced = cost === undefined || plan.tokens === 0 ? "" : `, ${dollars(cost)}`;
|
|
307
|
+
return `Send ${counted(plan.requests, "request", "requests")} to Jev${priced}?`;
|
|
308
|
+
};
|
|
269
309
|
|
|
270
310
|
/** A run's progress so far: every file it finished, with the requests and findings they added up to. */
|
|
271
311
|
export interface Progress {
|