@drkmttr/adhere 0.15.0 → 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 +313 -131
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +62 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +184 -171
- package/src/services/Jev.provider.test.ts +483 -0
- package/src/services/Jev.reads.test.ts +516 -0
- package/src/services/Jev.ts +276 -120
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1289 -0
- package/src/services/SourceReads.ts +460 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +513 -267
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AppendState,
|
|
3
|
+
DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
4
|
+
DEFAULT_MAX_SYMBOL_LENGTH,
|
|
5
|
+
type JevState,
|
|
6
|
+
type ReadState,
|
|
7
|
+
type ResolvedConfig,
|
|
8
|
+
type Rule,
|
|
9
|
+
resolveConfig,
|
|
10
|
+
} from "#config.ts";
|
|
11
|
+
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
12
|
+
import { Credentials } from "#services/Credentials.ts";
|
|
13
|
+
import { JevLive } from "#services/Jev.http.ts";
|
|
14
|
+
import {
|
|
15
|
+
describeState,
|
|
16
|
+
fits,
|
|
17
|
+
Jev,
|
|
18
|
+
judgeBody,
|
|
19
|
+
judgeLoad,
|
|
20
|
+
judgeRequests,
|
|
21
|
+
locateBody,
|
|
22
|
+
locateRequests,
|
|
23
|
+
questionRoom,
|
|
24
|
+
requestGroups,
|
|
25
|
+
requestsOf,
|
|
26
|
+
sufficiencyQuestion,
|
|
27
|
+
tokensOf,
|
|
28
|
+
} from "#services/Jev.ts";
|
|
29
|
+
import { Effect, Layer, Record, Redacted } from "effect";
|
|
30
|
+
import { FetchHttpClient } from "effect/http";
|
|
31
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
32
|
+
|
|
33
|
+
const plain: Rule = { description: "Ports must be named.", must: "const port = PORT;" };
|
|
34
|
+
const lines = ["const port = 3000;"] as const;
|
|
35
|
+
const codeState = { code: { "1": lines[0] } };
|
|
36
|
+
const file = { path: "/repo/src/server.ts", contents: lines.join("\n") };
|
|
37
|
+
const symbols = { port: "```typescript\nconst port: number\n```" };
|
|
38
|
+
const references = { "PORT:src/config.ts": "export const PORT = 3000;\r\n" };
|
|
39
|
+
const codeSufficiency = {
|
|
40
|
+
type: "noul",
|
|
41
|
+
instructions: {
|
|
42
|
+
question:
|
|
43
|
+
"Can you tell whether `code` breaks `rule` from `code` alone, without knowing what other files, libraries, services, or configuration do?",
|
|
44
|
+
rule: plain.description,
|
|
45
|
+
must: plain.must,
|
|
46
|
+
},
|
|
47
|
+
criteria: {
|
|
48
|
+
true: "`code` shows everything needed to tell whether it breaks `rule`",
|
|
49
|
+
false:
|
|
50
|
+
"Whether `code` breaks `rule` depends on something `code` does not show, such as another file, a library, a service, or configuration",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
interface Sent {
|
|
55
|
+
readonly state: JevState;
|
|
56
|
+
readonly questions: Readonly<
|
|
57
|
+
Record<string, { readonly type: string; readonly instructions: { readonly question: string } }>
|
|
58
|
+
>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const recordedJev = (
|
|
62
|
+
workspacePackages: Readonly<Record<string, string>> = {},
|
|
63
|
+
reads: ResolvedConfig["reads"] = [],
|
|
64
|
+
) => {
|
|
65
|
+
const sent: Array<Sent> = [];
|
|
66
|
+
// Jev's provider sends its requests with this fetch: each body as Jev would get it.
|
|
67
|
+
const fetch = (async (_url: unknown, init?: RequestInit) => {
|
|
68
|
+
const body: Sent = JSON.parse(String(init?.body));
|
|
69
|
+
sent.push(body);
|
|
70
|
+
const answers = Record.map(body.questions, ({ type }) =>
|
|
71
|
+
type === "noul" ? { noul: 0.9 } : { choice: "1" },
|
|
72
|
+
);
|
|
73
|
+
return Response.json({ answers });
|
|
74
|
+
}) as typeof globalThis.fetch;
|
|
75
|
+
const layer = JevLive.pipe(
|
|
76
|
+
Layer.provide([
|
|
77
|
+
Layer.succeed(FetchHttpClient.Fetch, fetch),
|
|
78
|
+
Layer.succeed(AdhereConfig, {
|
|
79
|
+
model: "jev-latest",
|
|
80
|
+
threshold: 0.8,
|
|
81
|
+
sufficiencyThreshold: 0.6,
|
|
82
|
+
maxSymbolDocumentationLength: DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
83
|
+
maxSymbolLength: DEFAULT_MAX_SYMBOL_LENGTH,
|
|
84
|
+
reads,
|
|
85
|
+
rules: {},
|
|
86
|
+
workspacePackages,
|
|
87
|
+
}),
|
|
88
|
+
Layer.succeed(Credentials, {
|
|
89
|
+
key: () => Effect.succeed(Redacted.make("tsk_fake")),
|
|
90
|
+
file: Effect.succeed("/config/adhere/credentials.json"),
|
|
91
|
+
save: () => Effect.void,
|
|
92
|
+
remove: () => Effect.succeed(false),
|
|
93
|
+
}),
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
return { sent, layer };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
describe("prepared shared Jev reads", () => {
|
|
100
|
+
it("keeps code-only judge and locate bodies byte-identical and protects target code", () => {
|
|
101
|
+
const judged = {
|
|
102
|
+
model: "jev-latest",
|
|
103
|
+
state: codeState,
|
|
104
|
+
questions: {
|
|
105
|
+
plain: {
|
|
106
|
+
type: "noul",
|
|
107
|
+
instructions: {
|
|
108
|
+
question:
|
|
109
|
+
"Does `code` diverge from the pattern shown in `must`, as described by `rule`? Answer no if the pattern does not apply to this file.",
|
|
110
|
+
rule: plain.description,
|
|
111
|
+
must: plain.must,
|
|
112
|
+
},
|
|
113
|
+
criteria: {
|
|
114
|
+
true: "`code` diverges from the pattern shown in `must`, in code that `rule` is about",
|
|
115
|
+
false: "`code` follows the pattern, or has no code that `rule` is about",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const located = {
|
|
121
|
+
model: "jev-latest",
|
|
122
|
+
state: codeState,
|
|
123
|
+
questions: {
|
|
124
|
+
...Object.fromEntries(
|
|
125
|
+
["start", "end"].map((edge) => [
|
|
126
|
+
`${edge}:plain:1`,
|
|
127
|
+
{
|
|
128
|
+
type: "choice",
|
|
129
|
+
instructions: {
|
|
130
|
+
question: `Which line of \`code["1"]\` does the code that breaks \`rule\` ${edge} on?`,
|
|
131
|
+
rule: plain.description,
|
|
132
|
+
must: plain.must,
|
|
133
|
+
},
|
|
134
|
+
criteria: { "1": lines[0] },
|
|
135
|
+
},
|
|
136
|
+
]),
|
|
137
|
+
),
|
|
138
|
+
"sufficient:plain": codeSufficiency,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
for (const readState of [undefined, {}, { code: { "1": "replaced" } }]) {
|
|
142
|
+
expect(JSON.stringify(judgeBody("jev-latest", lines, { plain }, [], readState))).toBe(
|
|
143
|
+
JSON.stringify(judged),
|
|
144
|
+
);
|
|
145
|
+
expect(JSON.stringify(locateBody("jev-latest", lines, { plain }, readState))).toBe(
|
|
146
|
+
JSON.stringify(located),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("shares all prepared fields across rules and isolates only appendState groups", () => {
|
|
152
|
+
const hooked = { ...plain, appendState: () => ({}) };
|
|
153
|
+
const rules = { plain, hooked, alsoPlain: plain, alsoHooked: hooked };
|
|
154
|
+
const readState: ReadState = {
|
|
155
|
+
symbols,
|
|
156
|
+
references,
|
|
157
|
+
workspacePackages: { orders: "packages/orders" },
|
|
158
|
+
project: { language: "typescript" },
|
|
159
|
+
code: { "1": "replaced" },
|
|
160
|
+
};
|
|
161
|
+
const enriched = describeState({ ...readState, ...codeState });
|
|
162
|
+
expect(enriched.__comment).toMatch(/only.*code|code.*only/i);
|
|
163
|
+
expect(enriched.__comment).toMatch(/context/i);
|
|
164
|
+
expect(enriched.symbols).toEqual({ ...symbols, __comment: expect.stringMatching(/symbol/i) });
|
|
165
|
+
expect(enriched.references).toEqual({
|
|
166
|
+
...references,
|
|
167
|
+
__comment: expect.stringMatching(/symbol.*path|symbol.*file/i),
|
|
168
|
+
});
|
|
169
|
+
expect(enriched.workspacePackages).toEqual({
|
|
170
|
+
orders: "packages/orders",
|
|
171
|
+
__comment: expect.stringMatching(/package/i),
|
|
172
|
+
});
|
|
173
|
+
expect(Object.keys(enriched.code)).toEqual(["1"]);
|
|
174
|
+
expect(judgeBody("", lines, rules, [], readState).state).toEqual(enriched);
|
|
175
|
+
expect(locateBody("", lines, rules, readState).state).toEqual(enriched);
|
|
176
|
+
const groups = requestGroups(rules);
|
|
177
|
+
expect(groups.map((group) => Object.keys(group))).toEqual([
|
|
178
|
+
["plain", "alsoPlain"],
|
|
179
|
+
["hooked"],
|
|
180
|
+
["alsoHooked"],
|
|
181
|
+
]);
|
|
182
|
+
for (const group of groups) {
|
|
183
|
+
expect(judgeBody("", lines, group, [], readState).state).toEqual(enriched);
|
|
184
|
+
const body = locateBody("", lines, group, readState);
|
|
185
|
+
expect(body.state).toEqual(enriched);
|
|
186
|
+
expect(body.state.symbols).toEqual({ ...symbols, __comment: expect.any(String) });
|
|
187
|
+
expect(body.state.references).toEqual({ ...references, __comment: expect.any(String) });
|
|
188
|
+
for (const id of Object.keys(group)) {
|
|
189
|
+
expect(body.questions[`sufficient:${id}`]).toEqual(sufficiencyQuestion(plain, readState));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
expect(judgeRequests(lines, rules, [], readState)).toBe(3);
|
|
193
|
+
expect(locateRequests(lines, rules, readState)).toBe(3);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("preserves caller comments and raw context through judge, locate, and appended maps", async () => {
|
|
197
|
+
const readState = {
|
|
198
|
+
__comment: "User context: only target code is audited.",
|
|
199
|
+
symbols: { ...symbols, __comment: "User symbol description." },
|
|
200
|
+
references: { ...references, __comment: "User reference description." },
|
|
201
|
+
workspacePackages: { orders: "packages/orders", __comment: "User package description." },
|
|
202
|
+
};
|
|
203
|
+
const original = JSON.stringify(readState);
|
|
204
|
+
const hooked = {
|
|
205
|
+
...plain,
|
|
206
|
+
appendState: () => ({ references: { "port:src/caller.ts": "\tuse(port);\r\n" } }),
|
|
207
|
+
};
|
|
208
|
+
const { sent, layer } = recordedJev();
|
|
209
|
+
await Effect.runPromise(
|
|
210
|
+
Effect.gen(function* () {
|
|
211
|
+
const jev = yield* Jev;
|
|
212
|
+
yield* jev.judge(lines, { plain, hooked }, [], file, readState);
|
|
213
|
+
yield* jev.locate(lines, { plain, hooked }, file, readState);
|
|
214
|
+
}).pipe(Effect.provide(layer)),
|
|
215
|
+
);
|
|
216
|
+
expect(sent).toHaveLength(4);
|
|
217
|
+
for (const body of sent) {
|
|
218
|
+
expect(body.state.code).toEqual(codeState.code);
|
|
219
|
+
expect(body.state.__comment).toBe(readState.__comment);
|
|
220
|
+
expect(body.state.symbols).toEqual(readState.symbols);
|
|
221
|
+
expect(body.state.workspacePackages).toEqual(readState.workspacePackages);
|
|
222
|
+
}
|
|
223
|
+
expect(sent[0]?.state.references).toEqual(readState.references);
|
|
224
|
+
expect(sent[2]?.state.references).toEqual(readState.references);
|
|
225
|
+
for (const index of [1, 3]) {
|
|
226
|
+
expect(sent[index]?.state.references).toEqual({
|
|
227
|
+
"port:src/caller.ts": "\tuse(port);\r\n",
|
|
228
|
+
__comment: expect.stringMatching(/symbol.*path|symbol.*file/i),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
expect(JSON.stringify(readState)).toBe(original);
|
|
232
|
+
expect(
|
|
233
|
+
sent
|
|
234
|
+
.slice(2)
|
|
235
|
+
.flatMap(({ questions }) => Object.values(questions))
|
|
236
|
+
.filter(({ type }) => type === "noul")
|
|
237
|
+
.every(({ instructions }) => !instructions.question.includes("__comment")),
|
|
238
|
+
).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("names actual shared keys in sufficiency, including packages but excluding duplicate code", () => {
|
|
242
|
+
expect(sufficiencyQuestion(plain)).toEqual(codeSufficiency);
|
|
243
|
+
expect(sufficiencyQuestion(plain, { code: {}, __comment: "context description" })).toEqual(
|
|
244
|
+
codeSufficiency,
|
|
245
|
+
);
|
|
246
|
+
expect(sufficiencyQuestion({ ...plain, appendState: () => ({ private: true }) })).toEqual(
|
|
247
|
+
codeSufficiency,
|
|
248
|
+
);
|
|
249
|
+
for (const readState of [
|
|
250
|
+
{ workspacePackages: {} },
|
|
251
|
+
{ symbols },
|
|
252
|
+
{ references },
|
|
253
|
+
{
|
|
254
|
+
workspacePackages: {},
|
|
255
|
+
symbols,
|
|
256
|
+
references,
|
|
257
|
+
project: true,
|
|
258
|
+
code: {},
|
|
259
|
+
__comment: "context description",
|
|
260
|
+
},
|
|
261
|
+
]) {
|
|
262
|
+
const question = sufficiencyQuestion(plain, readState);
|
|
263
|
+
expect(question.instructions.question).not.toContain("`code` alone");
|
|
264
|
+
for (const key of Object.keys(readState).filter(
|
|
265
|
+
(key) => key !== "code" && key !== "__comment",
|
|
266
|
+
)) {
|
|
267
|
+
expect(question.instructions.question).toContain(`\`${key}\``);
|
|
268
|
+
expect(question.criteria.true).toContain(`\`${key}\``);
|
|
269
|
+
expect(question.criteria.false).toContain(`\`${key}\``);
|
|
270
|
+
}
|
|
271
|
+
expect(question.instructions.question.match(/`code`/g)).toHaveLength(2);
|
|
272
|
+
expect(JSON.stringify(question)).not.toContain("__comment");
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("budgets all shared context beside every rule's questions", () => {
|
|
277
|
+
const source = "export const value = 1;\n".repeat(2200);
|
|
278
|
+
const readState = { references: { "value:src/related.ts": source } };
|
|
279
|
+
const large = { ...plain, must: "x".repeat(60_000) };
|
|
280
|
+
const hooked = { ...plain, appendState: () => ({ private: "x".repeat(120_000) }) };
|
|
281
|
+
expect(fits(lines, { large, hooked })).toBe(true);
|
|
282
|
+
expect(fits(lines, { hooked }, readState)).toBe(true);
|
|
283
|
+
expect(fits(lines, { large, hooked }, readState)).toBe(false);
|
|
284
|
+
const room = questionRoom(lines, readState);
|
|
285
|
+
expect(room).toBe(31_000 - tokensOf(locateBody("", lines, { plain }, readState).state));
|
|
286
|
+
expect(room).toBeLessThan(questionRoom(lines));
|
|
287
|
+
expect(questionRoom(lines, { ...readState, code: "x".repeat(120_000) })).toBe(room);
|
|
288
|
+
const tooLarge = { ...readState, symbols: ["s".repeat(54_000)] };
|
|
289
|
+
expect(tokensOf(judgeBody("", lines, { plain }, [], tooLarge).state)).toBeGreaterThan(31_000);
|
|
290
|
+
expect(fits(lines, { plain, hooked }, tooLarge)).toBe(false);
|
|
291
|
+
expect(fits(lines, {}, tooLarge)).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("keeps the exact fit boundary across sections, escaped Unicode, matchers, and shared keys", () => {
|
|
295
|
+
const escaped = '"\\\u0000界😊';
|
|
296
|
+
const cases: ReadonlyArray<{
|
|
297
|
+
readonly code: ReadonlyArray<string>;
|
|
298
|
+
readonly rule: Rule;
|
|
299
|
+
readonly readState: ReadState;
|
|
300
|
+
}> = [
|
|
301
|
+
{ code: [], rule: plain, readState: {} },
|
|
302
|
+
{ code: ["", " \t"], rule: { description: escaped, never: escaped }, readState: {} },
|
|
303
|
+
{
|
|
304
|
+
code: [`const value = "${escaped}";`],
|
|
305
|
+
rule: { description: escaped, should: escaped, shouldNot: escaped, details: escaped },
|
|
306
|
+
readState: { [`context${escaped}`]: escaped, code: "ignored" },
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
code: Array.from({ length: 301 }, (_, index) =>
|
|
310
|
+
index === 0 ? "a".repeat(119) + "😊;" : "a;",
|
|
311
|
+
),
|
|
312
|
+
rule: { ...plain, never: escaped },
|
|
313
|
+
readState: { workspacePackages: {}, symbols, references },
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
code: lines,
|
|
317
|
+
rule: { ...plain, appliesTo: [escaped.repeat(100)], excludeIf: [escaped] },
|
|
318
|
+
readState: { references },
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
const encoder = new TextEncoder();
|
|
322
|
+
for (const { code, rule, readState } of cases) {
|
|
323
|
+
const questions = [
|
|
324
|
+
...Object.values(judgeBody("", code, { rule }, [], readState).questions),
|
|
325
|
+
...Object.values(locateBody("", code, { rule }, readState).questions),
|
|
326
|
+
];
|
|
327
|
+
const longest = Math.max(
|
|
328
|
+
...questions.map((question) => encoder.encode(JSON.stringify(question)).length),
|
|
329
|
+
);
|
|
330
|
+
const padding = questionRoom(code, readState) * 3 - longest;
|
|
331
|
+
for (const extra of [-2, -1, 0, 1, 2]) {
|
|
332
|
+
const padded = { ...rule, description: rule.description + "x".repeat(padding + extra) };
|
|
333
|
+
expect(fits(code, { rule: padded }, readState)).toBe(extra <= 0);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("fits the surviving questions when a rule id is overwritten by a generated matcher key", () => {
|
|
339
|
+
const collision = { ...plain, never: "const port = 3000;" };
|
|
340
|
+
const rules = {
|
|
341
|
+
"appliesTo:plain:0": collision,
|
|
342
|
+
plain: { ...plain, appliesTo: ["Server ports."] },
|
|
343
|
+
};
|
|
344
|
+
const body = locateBody("", lines, rules);
|
|
345
|
+
const questions = [
|
|
346
|
+
...Object.values(judgeBody("", lines, rules).questions),
|
|
347
|
+
...Object.values(body.questions),
|
|
348
|
+
];
|
|
349
|
+
const encoder = new TextEncoder();
|
|
350
|
+
const longest = Math.max(
|
|
351
|
+
...questions.map((question) => encoder.encode(JSON.stringify(question)).length),
|
|
352
|
+
);
|
|
353
|
+
const padding = questionRoom(lines) * 3 - longest;
|
|
354
|
+
for (const extra of [0, 1]) {
|
|
355
|
+
const padded = {
|
|
356
|
+
...collision,
|
|
357
|
+
description: collision.description + "x".repeat(padding + extra),
|
|
358
|
+
};
|
|
359
|
+
expect(fits(lines, { ...rules, "appliesTo:plain:0": padded })).toBe(extra === 0);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("counts shared context on each split judge and locate request, matching HTTP payloads", async () => {
|
|
364
|
+
const long = Array.from({ length: 31 }, (_, index) => `const value${index} = ${index};`);
|
|
365
|
+
const native = {
|
|
366
|
+
...plain,
|
|
367
|
+
must: "x".repeat(30_000),
|
|
368
|
+
appliesTo: ["first", "second", "third", "fourth"],
|
|
369
|
+
};
|
|
370
|
+
const rules = { plain, native };
|
|
371
|
+
const readState = {
|
|
372
|
+
references: { "value:src/related.ts": "export const value = 1;\n".repeat(1500) },
|
|
373
|
+
};
|
|
374
|
+
const sampled = ["native"];
|
|
375
|
+
expect(fits(long, rules, readState)).toBe(true);
|
|
376
|
+
expect(judgeRequests(long, rules, sampled)).toBe(1);
|
|
377
|
+
expect(judgeRequests(long, rules, sampled, readState)).toBe(2);
|
|
378
|
+
expect(locateRequests(long, rules)).toBe(1);
|
|
379
|
+
expect(locateRequests(long, rules, readState)).toBe(2);
|
|
380
|
+
const judged = judgeBody("", long, rules, sampled, readState);
|
|
381
|
+
const located = locateBody("", long, rules, readState);
|
|
382
|
+
const requests = requestsOf(judged);
|
|
383
|
+
const locations = requestsOf(located);
|
|
384
|
+
const load = judgeLoad(long, rules, sampled, readState);
|
|
385
|
+
expect(load).toEqual({
|
|
386
|
+
requests: 2,
|
|
387
|
+
tokens: requests.reduce((sum, request) => sum + tokensOf(request), 0),
|
|
388
|
+
});
|
|
389
|
+
expect(load.tokens).toBeGreaterThan(
|
|
390
|
+
judgeLoad(long, rules, sampled).tokens + tokensOf(readState),
|
|
391
|
+
);
|
|
392
|
+
expect(requests.flatMap((request) => Object.keys(request.questions))).toEqual(
|
|
393
|
+
Object.keys(judged.questions),
|
|
394
|
+
);
|
|
395
|
+
expect(locations.flatMap((request) => Object.keys(request.questions))).toEqual(
|
|
396
|
+
Object.keys(located.questions),
|
|
397
|
+
);
|
|
398
|
+
const { sent, layer } = recordedJev();
|
|
399
|
+
await Effect.runPromise(
|
|
400
|
+
Effect.gen(function* () {
|
|
401
|
+
const jev = yield* Jev;
|
|
402
|
+
const longFile = { ...file, contents: long.join("\n") };
|
|
403
|
+
yield* jev.judge(long, rules, sampled, longFile, readState);
|
|
404
|
+
yield* jev.locate(long, rules, longFile, readState);
|
|
405
|
+
}).pipe(Effect.provide(layer)),
|
|
406
|
+
);
|
|
407
|
+
expect(sent.map((body) => Object.keys(body.questions))).toEqual(
|
|
408
|
+
[...requests, ...locations].map((request) => Object.keys(request.questions)),
|
|
409
|
+
);
|
|
410
|
+
expect(sent.map((body) => body.state)).toEqual(Array.from({ length: 4 }, () => judged.state));
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("sends shared context once for non-hook rules and inherits it in private hook groups", async () => {
|
|
414
|
+
const seen: Array<JevState> = [];
|
|
415
|
+
const privateSymbols = { private: "const private: number" };
|
|
416
|
+
const appendState: AppendState = (state, receivedFile) => {
|
|
417
|
+
expect(receivedFile).toBe(file);
|
|
418
|
+
seen.push(state);
|
|
419
|
+
return { private: "first", symbols: privateSymbols };
|
|
420
|
+
};
|
|
421
|
+
const alsoAppend: AppendState = async (state) => {
|
|
422
|
+
seen.push(state);
|
|
423
|
+
return { private: "second" };
|
|
424
|
+
};
|
|
425
|
+
const rules = {
|
|
426
|
+
plain,
|
|
427
|
+
hooked: { ...plain, appendState },
|
|
428
|
+
alsoPlain: plain,
|
|
429
|
+
alsoHooked: { ...plain, appendState: alsoAppend },
|
|
430
|
+
};
|
|
431
|
+
const workspacePackages = { prepared: "packages/prepared" };
|
|
432
|
+
const readState = { symbols, references, workspacePackages, code: {} };
|
|
433
|
+
const { sent, layer } = recordedJev({ fallback: "packages/fallback" });
|
|
434
|
+
const result = await Effect.runPromise(
|
|
435
|
+
Effect.gen(function* () {
|
|
436
|
+
const jev = yield* Jev;
|
|
437
|
+
const judged = yield* jev.judge(lines, rules, ["plain", "hooked"], file, readState);
|
|
438
|
+
const located = yield* jev.locate(lines, rules, file, readState);
|
|
439
|
+
return { judged, located };
|
|
440
|
+
}).pipe(Effect.provide(layer)),
|
|
441
|
+
);
|
|
442
|
+
expect(result.judged.probabilities).toEqual({
|
|
443
|
+
plain: 0.9,
|
|
444
|
+
hooked: 0.9,
|
|
445
|
+
alsoPlain: 0.9,
|
|
446
|
+
alsoHooked: 0.9,
|
|
447
|
+
});
|
|
448
|
+
expect(result.judged.linter).toEqual({ plain: 0.9, hooked: 0.9 });
|
|
449
|
+
expect(Object.keys(result.located)).toEqual(Object.keys(rules));
|
|
450
|
+
const enriched = describeState({ ...readState, ...codeState });
|
|
451
|
+
expect(seen).toEqual([enriched, enriched, enriched, enriched]);
|
|
452
|
+
for (const state of seen) {
|
|
453
|
+
expect(state.symbols).toEqual({ ...symbols, __comment: expect.any(String) });
|
|
454
|
+
expect(state.references).toEqual({ ...references, __comment: expect.any(String) });
|
|
455
|
+
expect(state.workspacePackages).toEqual({
|
|
456
|
+
...workspacePackages,
|
|
457
|
+
__comment: expect.any(String),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const expectedStates = [
|
|
461
|
+
enriched,
|
|
462
|
+
describeState({ ...enriched, private: "first", symbols: privateSymbols }),
|
|
463
|
+
{ ...enriched, private: "second" },
|
|
464
|
+
];
|
|
465
|
+
expect(sent.map((body) => body.state)).toEqual([...expectedStates, ...expectedStates]);
|
|
466
|
+
expect(sent.map((body) => Object.keys(body.questions))).toEqual([
|
|
467
|
+
["plain", "alsoPlain", "linter:plain"],
|
|
468
|
+
["hooked", "linter:hooked"],
|
|
469
|
+
["alsoHooked"],
|
|
470
|
+
[
|
|
471
|
+
"start:plain:1",
|
|
472
|
+
"end:plain:1",
|
|
473
|
+
"sufficient:plain",
|
|
474
|
+
"start:alsoPlain:1",
|
|
475
|
+
"end:alsoPlain:1",
|
|
476
|
+
"sufficient:alsoPlain",
|
|
477
|
+
],
|
|
478
|
+
["start:hooked:1", "end:hooked:1", "sufficient:hooked"],
|
|
479
|
+
["start:alsoHooked:1", "end:alsoHooked:1", "sufficient:alsoHooked"],
|
|
480
|
+
]);
|
|
481
|
+
for (const body of sent.slice(3)) {
|
|
482
|
+
for (const [id, question] of Object.entries(body.questions)) {
|
|
483
|
+
if (id.startsWith("sufficient:")) {
|
|
484
|
+
expect(question.instructions.question).toBe(
|
|
485
|
+
sufficiencyQuestion(plain, readState).instructions.question,
|
|
486
|
+
);
|
|
487
|
+
expect(question.instructions.question).not.toContain("`private`");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
expect(readState).not.toHaveProperty("private");
|
|
492
|
+
expect(readState.symbols).toBe(symbols);
|
|
493
|
+
expect(readState.code).toEqual({});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("does not prepare context in HTTP even with resolved default reads and omitted or partial shared state", async () => {
|
|
497
|
+
const config = resolveConfig({ presets: [], rules: { plain } });
|
|
498
|
+
expect(config.reads).toEqual(["workspacePackages", "symbols", "references"]);
|
|
499
|
+
const { sent, layer } = recordedJev({ unused: "packages/unused" }, config.reads);
|
|
500
|
+
await Effect.runPromise(
|
|
501
|
+
Effect.gen(function* () {
|
|
502
|
+
const jev = yield* Jev;
|
|
503
|
+
yield* jev.judge(lines, { plain }, [], file);
|
|
504
|
+
yield* jev.locate(lines, { plain }, file);
|
|
505
|
+
yield* jev.judge(lines, { plain }, [], file, { symbols });
|
|
506
|
+
yield* jev.locate(lines, { plain }, file, { symbols });
|
|
507
|
+
}).pipe(Effect.provide(layer)),
|
|
508
|
+
);
|
|
509
|
+
expect(sent.map((body) => body.state)).toEqual([
|
|
510
|
+
codeState,
|
|
511
|
+
codeState,
|
|
512
|
+
describeState({ symbols, ...codeState }),
|
|
513
|
+
describeState({ symbols, ...codeState }),
|
|
514
|
+
]);
|
|
515
|
+
});
|
|
516
|
+
});
|