@drkmttr/adhere 0.17.5 → 0.18.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/README.md +63 -24
- package/package.json +6 -6
- package/src/config.ts +13 -0
- package/src/services/Jev.http.ts +20 -2
- package/src/services/Jev.provider.test.ts +280 -4
- package/src/services/Jev.reads.test.ts +95 -18
- package/src/services/Jev.ts +76 -12
- package/src/services/SourceReads.test.ts +290 -41
- package/src/services/SourceReads.ts +52 -29
- package/src/workflows/audit.ts +80 -9
package/README.md
CHANGED
|
@@ -572,11 +572,42 @@ finding its matchers dropped, with their scores.
|
|
|
572
572
|
|
|
573
573
|
Beside the file's code, Jev reads three kinds of context, for every rule:
|
|
574
574
|
|
|
575
|
-
| Name | What Jev reads
|
|
576
|
-
| ------------------- |
|
|
577
|
-
| `workspacePackages` | The repo's own packages the file imports, each name with its directory, as in `{ "orders-core": "packages/orders" }`. A package it imports that is not listed is an installed one.
|
|
578
|
-
| `symbols` | The compiler's type blocks for each name, plus compiler documentation and JSDoc tags; not literal editor hover text.
|
|
579
|
-
| `references` |
|
|
575
|
+
| Name | What Jev reads |
|
|
576
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
577
|
+
| `workspacePackages` | The repo's own packages the file imports, each name with its directory, as in `{ "orders-core": "packages/orders" }`. A package it imports that is not listed is an installed one. |
|
|
578
|
+
| `symbols` | The compiler's type blocks for each name, plus compiler documentation and JSDoc tags; not literal editor hover text. |
|
|
579
|
+
| `references` | Whole declarations of symbols the code uses from other files of the repo, keyed by `symbol:repository-relative pathname`: a function with its body, a method without the rest of its class. |
|
|
580
|
+
|
|
581
|
+
`symbols` omits standard-library and Node/Bun runtime built-ins, including their
|
|
582
|
+
methods. Project and third-party symbols remain, even when they share a built-in
|
|
583
|
+
name. Built-in type names can still appear inside those symbols' type signatures.
|
|
584
|
+
|
|
585
|
+
`references` is a flat string-to-string map. Keys keep the symbol's name as used
|
|
586
|
+
in the target code, including import aliases, and the pathname's original casing.
|
|
587
|
+
Values are complete declaration excerpts, not incoming reference call sites.
|
|
588
|
+
Overlapping declarations under one key are merged; disjoint excerpts are joined
|
|
589
|
+
with `…` between them. For example, code importing `twice as scale` can receive:
|
|
590
|
+
|
|
591
|
+
```json
|
|
592
|
+
{
|
|
593
|
+
"references": {
|
|
594
|
+
"__comment": "Declarations of symbols used by the target code, provided only as context. Keys are symbol:repository-relative pathname; values are source excerpts, not incoming call sites.",
|
|
595
|
+
"scale:src/Math.ts": "export function twice(value: number): number {\n return value * 2;\n}"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
When sent to Jev, each selected `references`, `symbols`, and `workspacePackages`
|
|
601
|
+
map includes a reserved `__comment` explaining its contents. These comments are
|
|
602
|
+
included before `appendState` hooks and restored after hooks. Top-level guidance
|
|
603
|
+
also tells Jev that these maps are context only: judge the target `code`, not the
|
|
604
|
+
additional declarations or metadata.
|
|
605
|
+
|
|
606
|
+
Each symbol's complete resolved content is truncated to 256 characters by
|
|
607
|
+
default. Set `maxSymbolLength` in the config to change this limit. Documentation
|
|
608
|
+
and JSDoc tags are first capped separately by `maxSymbolDocumentationLength`, also
|
|
609
|
+
256 by default; `0` omits documentation and tags. Smaller limits reduce request
|
|
610
|
+
size but can cut off type signatures, overloads, useful details, or examples.
|
|
580
611
|
|
|
581
612
|
To read fewer, list the ones you want in the config's `reads`:
|
|
582
613
|
|
|
@@ -750,6 +781,8 @@ export default defineConfig({
|
|
|
750
781
|
sufficiencyThreshold: 0.6,
|
|
751
782
|
reads: ["workspacePackages", "symbols", "references"],
|
|
752
783
|
presets: ["effect"],
|
|
784
|
+
maxSymbolDocumentationLength: 256,
|
|
785
|
+
maxSymbolLength: 256,
|
|
753
786
|
exclude: ["**/generated/**"],
|
|
754
787
|
overrides: { "effect/basics/instrument-with-pipe": "off" },
|
|
755
788
|
rules: {
|
|
@@ -767,18 +800,20 @@ type UserId = typeof UserId.Type
|
|
|
767
800
|
});
|
|
768
801
|
```
|
|
769
802
|
|
|
770
|
-
| Key
|
|
771
|
-
|
|
|
772
|
-
| `model`
|
|
773
|
-
| `threshold`
|
|
774
|
-
| `sufficiencyThreshold`
|
|
775
|
-
| `presets`
|
|
776
|
-
| `rules`
|
|
777
|
-
| `exclude`
|
|
778
|
-
| `overrides`
|
|
779
|
-
| `includeComments`
|
|
780
|
-
| `reads`
|
|
781
|
-
| `
|
|
803
|
+
| Key | Default | Meaning |
|
|
804
|
+
| ------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
805
|
+
| `model` | `"jev-latest"` | The model id sent to TypeSafe. |
|
|
806
|
+
| `threshold` | `0.8` | Report a rule when Jev's probability is above this. |
|
|
807
|
+
| `sufficiencyThreshold` | `0.6` | Below it, a finding warns that Jev may not see enough to decide. |
|
|
808
|
+
| `presets` | none | Built-in rule sets, or their topics. See [Presets](#presets). |
|
|
809
|
+
| `rules` | `.adhere/rules/` | Rules inline, as above, or a directory (see [Where rules live](#where-rules-live)). Either replaces `.adhere/rules/`: none of its rules, root or nested, is read then. |
|
|
810
|
+
| `exclude` | none | Globs, relative to the working directory, of files no rule judges. |
|
|
811
|
+
| `overrides` | none | Settings for a rule by its id. See [Overrides](#overrides). |
|
|
812
|
+
| `includeComments` | `false` | Send every comment to Jev. See [Comments](#comments). |
|
|
813
|
+
| `reads` | `["workspacePackages", "symbols", "references"]` | What Jev reads beside each file's code, for every rule. `[]` is the code alone. See [What Jev reads](#what-jev-reads). |
|
|
814
|
+
| `maxSymbolDocumentationLength` | `256` | Maximum characters of each symbol's combined documentation and JSDoc tags, before `maxSymbolLength` applies. A non-negative integer; `0` omits them. |
|
|
815
|
+
| `maxSymbolLength` | `256` | Maximum characters of each complete `symbols[name]` string, including type signatures and documentation. A non-negative integer; `0` leaves names with empty content. |
|
|
816
|
+
| `provider` | TypeSafe's Jev | Where the questions go instead, with its own model. See [Providers](#providers). |
|
|
782
817
|
|
|
783
818
|
- An invalid shape refuses the run.
|
|
784
819
|
- A config can import other files by relative path, but no packages besides
|
|
@@ -866,9 +901,10 @@ export default defineConfig({ provider: mine });
|
|
|
866
901
|
`choice`'s key. An id left out goes unanswered: its rule is not reported, and
|
|
867
902
|
is asked again on the next run.
|
|
868
903
|
- A rejection stops the run, but for three errors adhere exports:
|
|
869
|
-
`ContextOverflow`
|
|
870
|
-
|
|
871
|
-
|
|
904
|
+
`ContextOverflow` gets one retry without `references` when that map is present;
|
|
905
|
+
if the request still overflows, the file is skipped. `RequestBlocked` reports
|
|
906
|
+
it blocked, and `RequestFailed` is tried again, up to three times, when its
|
|
907
|
+
`status` is 0, for no answer, or a 408, 429, or 5xx.
|
|
872
908
|
- adhere paces calls with `--rpm`, aborts `signal` after 30 seconds, and splits
|
|
873
909
|
requests to fit Jev's 64,000 tokens. A model that takes less splits further in
|
|
874
910
|
its `ask`.
|
|
@@ -1422,10 +1458,13 @@ about three bytes a token, and leaves 1k of room for what Jev wraps around it.
|
|
|
1422
1458
|
|
|
1423
1459
|
- When a file's questions would not fit in one request, they are split across
|
|
1424
1460
|
several.
|
|
1425
|
-
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1461
|
+
- If a file's state and longest question would not fit together, or a provider
|
|
1462
|
+
reports a context overflow, adhere retries once without `references` when that
|
|
1463
|
+
map is present. Target code, `symbols`, and other context remain intact; no
|
|
1464
|
+
declaration excerpt is shortened to fit. Removing references may still leave
|
|
1465
|
+
the request too large, in which case the file is skipped and counted in the
|
|
1466
|
+
summary. A file with more than 255 code sections is also skipped, and the rest
|
|
1467
|
+
of the run goes on.
|
|
1429
1468
|
- So is a file Jev itself counts as over its context, which text denser than the
|
|
1430
1469
|
estimate, such as CJK, can cause.
|
|
1431
1470
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,10 +90,10 @@
|
|
|
90
90
|
"--force"
|
|
91
91
|
],
|
|
92
92
|
"optionalDependencies": {
|
|
93
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
94
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
95
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
96
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
97
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
93
|
+
"@drkmttr/adhere-darwin-arm64": "0.18.0",
|
|
94
|
+
"@drkmttr/adhere-darwin-x64": "0.18.0",
|
|
95
|
+
"@drkmttr/adhere-linux-arm64": "0.18.0",
|
|
96
|
+
"@drkmttr/adhere-linux-x64": "0.18.0",
|
|
97
|
+
"@drkmttr/adhere-win32-x64": "0.18.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/src/config.ts
CHANGED
|
@@ -222,6 +222,10 @@ export const AdhereConfig = Schema.Struct({
|
|
|
222
222
|
),
|
|
223
223
|
/** Shared context for every audited file and all its rules. Unset: all built-in reads; []: code only. */
|
|
224
224
|
reads: Schema.optionalKey(Reads),
|
|
225
|
+
/** Maximum characters of each symbol's complete resolved content. Defaults to 256. */
|
|
226
|
+
maxSymbolLength: Schema.optionalKey(Schema.Natural),
|
|
227
|
+
/** Maximum characters of documentation and JSDoc tags per symbol. Defaults to 256; 0 omits them. */
|
|
228
|
+
maxSymbolDocumentationLength: Schema.optionalKey(Schema.Natural),
|
|
225
229
|
/**
|
|
226
230
|
* A record of rules, or a directory path relative to the config file,
|
|
227
231
|
* which holds its rules as `.adhere/rules/` does. Unset: the rules in
|
|
@@ -330,6 +334,8 @@ export const DEFAULT_MODEL = "jev-latest";
|
|
|
330
334
|
export const DEFAULT_THRESHOLD = 0.8;
|
|
331
335
|
export const DEFAULT_SUFFICIENCY_THRESHOLD = 0.6;
|
|
332
336
|
export const DEFAULT_READS: typeof Reads.Type = ["workspacePackages", "symbols", "references"];
|
|
337
|
+
export const DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH = 256;
|
|
338
|
+
export const DEFAULT_MAX_SYMBOL_LENGTH = 256;
|
|
333
339
|
|
|
334
340
|
/** A config with its presets folded in and every default applied. */
|
|
335
341
|
export interface ResolvedConfig {
|
|
@@ -349,6 +355,10 @@ export interface ResolvedConfig {
|
|
|
349
355
|
readonly includeComments?: boolean;
|
|
350
356
|
/** Built-in context shared by every rule, with the default applied. */
|
|
351
357
|
readonly reads: typeof Reads.Type;
|
|
358
|
+
/** Maximum characters of each symbol's complete resolved content. */
|
|
359
|
+
readonly maxSymbolLength: number;
|
|
360
|
+
/** Maximum characters of documentation and JSDoc tags before the complete symbol limit applies. */
|
|
361
|
+
readonly maxSymbolDocumentationLength: number;
|
|
352
362
|
/**
|
|
353
363
|
* The workspace's packages, each name with its directory from the working
|
|
354
364
|
* directory, read from its manifests on every run when the config's reads
|
|
@@ -411,6 +421,9 @@ export const resolveConfig = (
|
|
|
411
421
|
...(config.exclude === undefined ? {} : { exclude: config.exclude }),
|
|
412
422
|
...(config.includeComments === undefined ? {} : { includeComments: config.includeComments }),
|
|
413
423
|
reads: config.reads ?? DEFAULT_READS,
|
|
424
|
+
maxSymbolLength: config.maxSymbolLength ?? DEFAULT_MAX_SYMBOL_LENGTH,
|
|
425
|
+
maxSymbolDocumentationLength:
|
|
426
|
+
config.maxSymbolDocumentationLength ?? DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
414
427
|
};
|
|
415
428
|
};
|
|
416
429
|
|
package/src/services/Jev.http.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { Credentials } from "#services/Credentials.ts";
|
|
|
14
14
|
import {
|
|
15
15
|
type Body,
|
|
16
16
|
type ComparedRule,
|
|
17
|
+
describeState,
|
|
17
18
|
edgeKey,
|
|
18
19
|
conflictBody,
|
|
19
20
|
contradictBody,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
type Rules,
|
|
37
38
|
sufficiencyKey,
|
|
38
39
|
tokensOf,
|
|
40
|
+
logRequestPayload,
|
|
39
41
|
} from "#services/Jev.ts";
|
|
40
42
|
import {
|
|
41
43
|
Duration,
|
|
@@ -87,7 +89,7 @@ const hooked = (
|
|
|
87
89
|
return Effect.tryPromise({
|
|
88
90
|
try: async () => ({
|
|
89
91
|
...body,
|
|
90
|
-
state: { ...body.state, ...(await appendState(body.state, file, bun)) },
|
|
92
|
+
state: describeState({ ...body.state, ...(await appendState(body.state, file, bun)) }),
|
|
91
93
|
}),
|
|
92
94
|
catch: (cause) => refused(`the appendState of ${id} failed: ${messageOf(cause)}`),
|
|
93
95
|
});
|
|
@@ -190,6 +192,7 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
190
192
|
const limiter = yield* RateLimiter.RateLimiter;
|
|
191
193
|
|
|
192
194
|
const provider: Provider = config.provider ?? jev({ model: config.model, fetch });
|
|
195
|
+
const previewed = new Set<string>();
|
|
193
196
|
// The key it asks with, when it names one `adhere login` saves. Read when
|
|
194
197
|
// a request needs it, not in the layer: a run with nothing pending needs none.
|
|
195
198
|
const apiKey: Effect.Effect<string | undefined, JevUnavailable> =
|
|
@@ -226,8 +229,23 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
226
229
|
yield* Effect.logDebug(
|
|
227
230
|
`${kind}: asking ${provider.model} ${questions} ${questions === 1 ? "question" : "questions"}, about ${tokensOf(body)} tokens`,
|
|
228
231
|
);
|
|
232
|
+
if ((kind === "judge" || kind === "locate") && !previewed.has(kind)) {
|
|
233
|
+
previewed.add(kind);
|
|
234
|
+
yield* logRequestPayload(
|
|
235
|
+
`${kind}: first ${kind === "locate" ? "match-to-lines " : ""}request payload`,
|
|
236
|
+
() => body,
|
|
237
|
+
"Debug",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
229
240
|
const [elapsed, answered] = yield* Effect.timed(
|
|
230
|
-
askProvider(provider, body.state, questionsOf(body.questions), Answers, key, pace)
|
|
241
|
+
askProvider(provider, body.state, questionsOf(body.questions), Answers, key, pace).pipe(
|
|
242
|
+
Effect.tapErrorTag("JevOverflow", () =>
|
|
243
|
+
logRequestPayload(
|
|
244
|
+
`${kind}: too long request payload (provider overflow)`,
|
|
245
|
+
() => body,
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
231
249
|
);
|
|
232
250
|
yield* Effect.logDebug(
|
|
233
251
|
`${kind}: ${provider.model} answered in ${Math.round(Duration.toMillis(elapsed))} ms`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeConfig, resolveConfig, type Rule, type Rules } from "#config.ts";
|
|
1
|
+
import { decodeConfig, resolveConfig, type JevState, type Rule, type Rules } from "#config.ts";
|
|
2
2
|
import {
|
|
3
3
|
type AskOptions,
|
|
4
4
|
ContextOverflow,
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
13
13
|
import { Credentials } from "#services/Credentials.ts";
|
|
14
14
|
import { JevLive } from "#services/Jev.http.ts";
|
|
15
|
-
import { Jev } from "#services/Jev.ts";
|
|
16
|
-
import {
|
|
15
|
+
import { describeState, Jev, judgeBody, locateBody, requestsOf } from "#services/Jev.ts";
|
|
16
|
+
import { questionsOf, requestOf, type SystemOneRequest } from "#providers/systemOne.ts";
|
|
17
|
+
import { Effect, Layer, Logger, Redacted, References } from "effect";
|
|
17
18
|
import { FetchHttpClient } from "effect/http";
|
|
18
19
|
import { describe, expect, it } from "vite-plus/test";
|
|
19
20
|
|
|
@@ -25,6 +26,12 @@ const named: Rule = {
|
|
|
25
26
|
const lines = ["const port = 3000;"];
|
|
26
27
|
const file = { path: "/repo/src/server.ts", contents: lines.join("\n") };
|
|
27
28
|
|
|
29
|
+
// Restore JSON escapes only inside string tokens, leaving layout and source backslashes intact.
|
|
30
|
+
const jsonFromDiagnostic = (text: string): string =>
|
|
31
|
+
text.replace(/"(?:\\[\s\S]|[^"\\])*"/g, (token) =>
|
|
32
|
+
token.replace(/\n/g, "\\n").replace(/\t/g, "\\t"),
|
|
33
|
+
);
|
|
34
|
+
|
|
28
35
|
/**
|
|
29
36
|
* Jev under a config whose provider answers: any request Jev's own provider
|
|
30
37
|
* sends, or any read of its key, dies. The saved OpenAI key is "sk-saved".
|
|
@@ -38,10 +45,13 @@ const judged = (
|
|
|
38
45
|
ask,
|
|
39
46
|
...(savedKey === undefined ? {} : { savedKey }),
|
|
40
47
|
},
|
|
48
|
+
kind: "judge" | "locate" = "judge",
|
|
41
49
|
) =>
|
|
42
50
|
Effect.gen(function* () {
|
|
43
51
|
const jev = yield* Jev;
|
|
44
|
-
return
|
|
52
|
+
return kind === "judge"
|
|
53
|
+
? yield* jev.judge(lines, rules, [], file)
|
|
54
|
+
: yield* jev.locate(lines, rules, file);
|
|
45
55
|
}).pipe(
|
|
46
56
|
Effect.provide(
|
|
47
57
|
JevLive.pipe(
|
|
@@ -117,6 +127,272 @@ describe("a config's provider", () => {
|
|
|
117
127
|
expect(blocked).toMatchObject({ _tag: "JevBlocked", ray: "ref-7" });
|
|
118
128
|
});
|
|
119
129
|
|
|
130
|
+
it.each([
|
|
131
|
+
["All", "judge"],
|
|
132
|
+
["Trace", "judge"],
|
|
133
|
+
["Debug", "judge"],
|
|
134
|
+
["All", "locate"],
|
|
135
|
+
["Trace", "locate"],
|
|
136
|
+
["Debug", "locate"],
|
|
137
|
+
] as const)(
|
|
138
|
+
"logs split requests after appendState with line criteria only at All (minimum: %s, kind: %s)",
|
|
139
|
+
async (minimum, kind) => {
|
|
140
|
+
const context = `export function caller() {\n\tconst escaped = "\\n\\t";\n\treturn "${"hook-context".repeat(40_000)}";\n}`;
|
|
141
|
+
const added = { references: { "port:caller.ts": context } };
|
|
142
|
+
const hooked: Rule = {
|
|
143
|
+
...plain,
|
|
144
|
+
appliesTo: ["a server"],
|
|
145
|
+
appendState: async () => added,
|
|
146
|
+
};
|
|
147
|
+
const body = (
|
|
148
|
+
kind === "judge"
|
|
149
|
+
? judgeBody("clef-flash", lines, { plain: hooked })
|
|
150
|
+
: locateBody("clef-flash", lines, { plain: hooked })
|
|
151
|
+
) as SystemOneRequest & { readonly state: JevState };
|
|
152
|
+
const requests = requestsOf({ ...body, state: describeState({ ...body.state, ...added }) });
|
|
153
|
+
expect(requests.length).toBeGreaterThan(1);
|
|
154
|
+
const request = requests[0]!;
|
|
155
|
+
const asked: Array<{
|
|
156
|
+
state: State;
|
|
157
|
+
questions: ReadonlyArray<Question>;
|
|
158
|
+
apiKey: string | undefined;
|
|
159
|
+
}> = [];
|
|
160
|
+
let originalBody = "";
|
|
161
|
+
const originalAdded = JSON.stringify(added);
|
|
162
|
+
const logs: Array<Logger.Options<unknown>> = [];
|
|
163
|
+
const overflowed = await Effect.runPromise(
|
|
164
|
+
Effect.flip(
|
|
165
|
+
judged(
|
|
166
|
+
async (state, questions, { apiKey }) => {
|
|
167
|
+
asked.push({ state, questions, apiKey });
|
|
168
|
+
originalBody = JSON.stringify(requestOf("clef-flash", state, questions));
|
|
169
|
+
throw new ContextOverflow("too long");
|
|
170
|
+
},
|
|
171
|
+
"openai",
|
|
172
|
+
{ plain: hooked },
|
|
173
|
+
undefined,
|
|
174
|
+
kind,
|
|
175
|
+
),
|
|
176
|
+
).pipe(
|
|
177
|
+
Effect.provide(Logger.layer([Logger.make((entry) => logs.push(entry))])),
|
|
178
|
+
Effect.provideService(References.MinimumLogLevel, minimum),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
expect(overflowed._tag).toBe("JevOverflow");
|
|
182
|
+
expect(asked).toEqual([
|
|
183
|
+
{ state: request.state, questions: questionsOf(request.questions), apiKey: "sk-saved" },
|
|
184
|
+
]);
|
|
185
|
+
const messages = logs.flatMap(({ message }) =>
|
|
186
|
+
Array.isArray(message) ? message : [message],
|
|
187
|
+
);
|
|
188
|
+
const oversized = messages.filter((message) =>
|
|
189
|
+
String(message).includes("too long request payload"),
|
|
190
|
+
);
|
|
191
|
+
if (minimum !== "Debug") {
|
|
192
|
+
const label = `${kind}: too long request payload (provider overflow)\n`;
|
|
193
|
+
expect(oversized).toHaveLength(1);
|
|
194
|
+
expect(String(oversized[0]).startsWith(label)).toBe(true);
|
|
195
|
+
const json = jsonFromDiagnostic(String(oversized[0]).slice(label.length));
|
|
196
|
+
const logged = JSON.parse(json) as typeof request;
|
|
197
|
+
const id = kind === "judge" ? "plain" : "start:plain:1";
|
|
198
|
+
expect(Object.keys(request.questions)).toEqual([id]);
|
|
199
|
+
const question = request.questions[id]!;
|
|
200
|
+
expect(question.instructions).toMatchObject({ rule: plain.description, must: plain.must });
|
|
201
|
+
if (minimum === "Trace" && question.type === "choice") {
|
|
202
|
+
const { criteria, ...withoutCriteria } = question;
|
|
203
|
+
expect(criteria).toEqual({ "1": lines[0] });
|
|
204
|
+
expect(logged.questions[id]).not.toHaveProperty("criteria");
|
|
205
|
+
expect(logged).toEqual({ ...request, questions: { [id]: withoutCriteria } });
|
|
206
|
+
expect(json).toBe(JSON.stringify(logged, null, 2));
|
|
207
|
+
} else {
|
|
208
|
+
expect(json).toBe(JSON.stringify(request, null, 2));
|
|
209
|
+
expect(logged).toEqual(request);
|
|
210
|
+
expect(logged.questions[id]?.criteria).toBeDefined();
|
|
211
|
+
}
|
|
212
|
+
expect(logged.state).toMatchObject(added);
|
|
213
|
+
if (minimum === "Trace") expect(Object.keys(logged.state)[0]).toBe("code");
|
|
214
|
+
expect(logged.model).toBe("clef-flash");
|
|
215
|
+
expect(
|
|
216
|
+
logs.find(({ message }) => Array.isArray(message) && message.includes(oversized[0]))
|
|
217
|
+
?.logLevel,
|
|
218
|
+
).toBe("Trace");
|
|
219
|
+
} else {
|
|
220
|
+
expect(oversized).toEqual([]);
|
|
221
|
+
const label = `${kind}: first ${kind === "locate" ? "match-to-lines " : ""}request payload\n`;
|
|
222
|
+
const previews = messages.filter((message) => String(message).startsWith(label));
|
|
223
|
+
expect(previews).toHaveLength(1);
|
|
224
|
+
const logged = JSON.parse(
|
|
225
|
+
jsonFromDiagnostic(String(previews[0]).slice(label.length)),
|
|
226
|
+
) as SystemOneRequest;
|
|
227
|
+
expect(logged.state).toEqual(request.state);
|
|
228
|
+
expect(Object.keys(logged.state)[0]).toBe("code");
|
|
229
|
+
expect(logged.model).toBe(request.model);
|
|
230
|
+
const id = kind === "judge" ? "plain" : "start:plain:1";
|
|
231
|
+
if (kind === "locate") {
|
|
232
|
+
const { criteria, ...question } = request.questions[id]!;
|
|
233
|
+
expect(criteria).toBeDefined();
|
|
234
|
+
expect(logged.questions).toEqual({ [id]: question });
|
|
235
|
+
} else {
|
|
236
|
+
expect(logged.questions).toEqual(request.questions);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const message of messages.filter((message) =>
|
|
240
|
+
String(message).includes("request payload"),
|
|
241
|
+
)) {
|
|
242
|
+
expect(String(message)).toContain("\n\tconst escaped = ");
|
|
243
|
+
expect(String(message)).toContain(String.raw`\\n\\t`);
|
|
244
|
+
expect(String(message)).not.toContain(String.raw`\n\tconst escaped`);
|
|
245
|
+
}
|
|
246
|
+
expect(originalBody).toContain(String.raw`\n\tconst escaped`);
|
|
247
|
+
expect(JSON.stringify(added)).toBe(originalAdded);
|
|
248
|
+
const sent = asked[0]!;
|
|
249
|
+
expect(JSON.stringify(requestOf("clef-flash", sent.state, sent.questions))).toBe(
|
|
250
|
+
originalBody,
|
|
251
|
+
);
|
|
252
|
+
expect(JSON.parse(originalBody)).toEqual(request);
|
|
253
|
+
expect(JSON.stringify(messages)).not.toContain("sk-saved");
|
|
254
|
+
expect(request).not.toHaveProperty("apiKey");
|
|
255
|
+
expect(request).not.toHaveProperty("signal");
|
|
256
|
+
expect(messages).toContain("attempt failed: too long");
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
it.each(["All", "Trace", "Debug", "Info"] as const)(
|
|
261
|
+
"previews only the first judge and match-to-lines request per shared JevLive (minimum: %s)",
|
|
262
|
+
async (minimum) => {
|
|
263
|
+
const context = `export function caller() {\n\tconst escaped = "\\n\\t";\n\treturn "${"preview-context".repeat(40_000)}";\n}`;
|
|
264
|
+
const added = { references: { "port:caller.ts": context } };
|
|
265
|
+
const readState = { workspacePackages: { server: "src" } };
|
|
266
|
+
const hookedRules = {
|
|
267
|
+
plain: { ...plain, appliesTo: ["a server"], appendState: async () => added },
|
|
268
|
+
named: { ...named, appliesTo: ["a server"], appendState: async () => added },
|
|
269
|
+
};
|
|
270
|
+
const files = [file, { path: "/repo/src/other.ts", contents: " const port = 8080;" }, file];
|
|
271
|
+
const expected = files.flatMap((current) => {
|
|
272
|
+
const code = current.contents.split("\n");
|
|
273
|
+
return ["judge", "locate"].flatMap((kind) =>
|
|
274
|
+
Object.entries(hookedRules).flatMap(([id, rule]) => {
|
|
275
|
+
const body = (
|
|
276
|
+
kind === "judge"
|
|
277
|
+
? judgeBody("clef-flash", code, { [id]: rule }, [], readState)
|
|
278
|
+
: locateBody("clef-flash", code, { [id]: rule }, readState)
|
|
279
|
+
) as SystemOneRequest & { readonly state: JevState };
|
|
280
|
+
const split = requestsOf({
|
|
281
|
+
...body,
|
|
282
|
+
state: describeState({ ...body.state, ...added }),
|
|
283
|
+
});
|
|
284
|
+
expect(split.length).toBeGreaterThan(1);
|
|
285
|
+
return split;
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
const firstJudge = expected[0]!;
|
|
290
|
+
const firstLocate = expected.find((body) =>
|
|
291
|
+
Object.keys(body.questions)[0]?.startsWith("start:"),
|
|
292
|
+
)!;
|
|
293
|
+
const asked: Array<SystemOneRequest> = [];
|
|
294
|
+
const snapshots: string[] = [];
|
|
295
|
+
const keys: Array<string | undefined> = [];
|
|
296
|
+
const logs: Array<Logger.Options<unknown>> = [];
|
|
297
|
+
const provider: Provider = {
|
|
298
|
+
model: "clef-flash",
|
|
299
|
+
savedKey: "openai",
|
|
300
|
+
ask: async (state, questions, { apiKey }) => {
|
|
301
|
+
const body = requestOf("clef-flash", state, questions);
|
|
302
|
+
asked.push(body);
|
|
303
|
+
snapshots.push(JSON.stringify(body));
|
|
304
|
+
keys.push(apiKey);
|
|
305
|
+
if (asked.length === 1) throw new RequestFailed("retry first request", 503);
|
|
306
|
+
return { answers: {} };
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
await Effect.runPromise(
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
const jev = yield* Jev;
|
|
312
|
+
for (const current of files) {
|
|
313
|
+
const code = current.contents.split("\n");
|
|
314
|
+
const judged = yield* jev.judge(code, hookedRules, [], current, readState);
|
|
315
|
+
expect(judged.probabilities).toEqual({});
|
|
316
|
+
expect(yield* jev.locate(code, hookedRules, current, readState)).toEqual({});
|
|
317
|
+
}
|
|
318
|
+
}).pipe(
|
|
319
|
+
Effect.provide(
|
|
320
|
+
JevLive.pipe(
|
|
321
|
+
Layer.provide([
|
|
322
|
+
Layer.succeed(FetchHttpClient.Fetch, (() => {
|
|
323
|
+
throw new Error("preview tests send no HTTP requests");
|
|
324
|
+
}) as unknown as typeof globalThis.fetch),
|
|
325
|
+
Layer.succeed(AdhereConfig, resolveConfig({ presets: [], rules: {}, provider })),
|
|
326
|
+
Layer.succeed(Credentials, {
|
|
327
|
+
key: () => Effect.succeed(Redacted.make("sk-saved")),
|
|
328
|
+
file: Effect.die("unused"),
|
|
329
|
+
save: () => Effect.die("unused"),
|
|
330
|
+
remove: () => Effect.die("unused"),
|
|
331
|
+
}),
|
|
332
|
+
]),
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
Effect.provide(Logger.layer([Logger.make((entry) => logs.push(entry))])),
|
|
336
|
+
Effect.provideService(References.MinimumLogLevel, minimum),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
expect(asked).toEqual([firstJudge, ...expected]);
|
|
340
|
+
expect(asked.map((body) => JSON.stringify(body))).toEqual(snapshots);
|
|
341
|
+
expect(snapshots[0]).toContain(String.raw`\n\tconst escaped`);
|
|
342
|
+
expect(snapshots.map((json) => JSON.parse(json))).toEqual(asked);
|
|
343
|
+
expect(keys).toEqual(asked.map(() => "sk-saved"));
|
|
344
|
+
const messages = logs.flatMap(({ message }) =>
|
|
345
|
+
Array.isArray(message) ? message : [message],
|
|
346
|
+
);
|
|
347
|
+
const previews = messages.filter((message) => String(message).includes("request payload"));
|
|
348
|
+
if (minimum === "Info") {
|
|
349
|
+
expect(previews).toEqual([]);
|
|
350
|
+
expect(JSON.stringify(messages)).not.toContain("preview-context");
|
|
351
|
+
} else {
|
|
352
|
+
const labels = [
|
|
353
|
+
"judge: first request payload\n",
|
|
354
|
+
"locate: first match-to-lines request payload\n",
|
|
355
|
+
];
|
|
356
|
+
expect(previews).toHaveLength(2);
|
|
357
|
+
for (const [index, body] of [firstJudge, firstLocate].entries()) {
|
|
358
|
+
const label = labels[index]!;
|
|
359
|
+
expect(String(previews[index]).startsWith(label)).toBe(true);
|
|
360
|
+
const text = String(previews[index]).slice(label.length);
|
|
361
|
+
expect(text).toContain("\n\tconst escaped = ");
|
|
362
|
+
expect(text).toContain(String.raw`\\n\\t`);
|
|
363
|
+
expect(text).not.toContain(String.raw`\n\tconst escaped`);
|
|
364
|
+
const json = jsonFromDiagnostic(text);
|
|
365
|
+
const logged = JSON.parse(json) as SystemOneRequest;
|
|
366
|
+
expect(logged.model).toBe(body.model);
|
|
367
|
+
expect(logged.state).toEqual(body.state);
|
|
368
|
+
expect(logged.state).toMatchObject({ ...readState, ...added });
|
|
369
|
+
if (minimum === "All") {
|
|
370
|
+
expect(json).toBe(JSON.stringify(body, null, 2));
|
|
371
|
+
expect(logged).toEqual(body);
|
|
372
|
+
} else {
|
|
373
|
+
expect(Object.keys(logged.state)[0]).toBe("code");
|
|
374
|
+
if (index === 0) {
|
|
375
|
+
expect(logged.questions).toEqual(body.questions);
|
|
376
|
+
expect(logged.questions.plain?.criteria).toBeDefined();
|
|
377
|
+
} else {
|
|
378
|
+
const { criteria, ...question } = body.questions["start:plain:1"]!;
|
|
379
|
+
expect(criteria).toEqual({ "1": lines[0] });
|
|
380
|
+
expect(logged.questions).toEqual({ "start:plain:1": question });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
expect(
|
|
384
|
+
logs.find(({ message }) => Array.isArray(message) && message.includes(previews[index]))
|
|
385
|
+
?.logLevel,
|
|
386
|
+
).toBe("Debug");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
expect(JSON.stringify(messages)).not.toContain("sk-saved");
|
|
390
|
+
expect(messages.some((message) => String(message).includes("too long request payload"))).toBe(
|
|
391
|
+
false,
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
120
396
|
it("is asked as a method, so a class can implement it", async () => {
|
|
121
397
|
class Fixed implements Provider {
|
|
122
398
|
readonly model = "fixed";
|