@drkmttr/adhere 0.17.5 → 0.18.1
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 +125 -64
- package/package.json +6 -6
- package/skills/adhere/SKILL.md +50 -33
- package/skills/adhere-setup/SKILL.md +71 -48
- package/src/cli.ts +13 -1
- package/src/config.ts +27 -3
- package/src/init.ts +22 -3
- package/src/install.ts +29 -23
- package/src/services/Jev.http.ts +20 -2
- package/src/services/Jev.provider.test.ts +280 -4
- package/src/services/Jev.reads.test.ts +99 -20
- 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 +82 -11
|
@@ -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";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AppendState,
|
|
3
|
+
DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
4
|
+
DEFAULT_MAX_SYMBOL_LENGTH,
|
|
3
5
|
type JevState,
|
|
4
6
|
type ReadState,
|
|
5
7
|
type ResolvedConfig,
|
|
@@ -10,6 +12,7 @@ import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
|
10
12
|
import { Credentials } from "#services/Credentials.ts";
|
|
11
13
|
import { JevLive } from "#services/Jev.http.ts";
|
|
12
14
|
import {
|
|
15
|
+
describeState,
|
|
13
16
|
fits,
|
|
14
17
|
Jev,
|
|
15
18
|
judgeBody,
|
|
@@ -28,11 +31,11 @@ import { FetchHttpClient } from "effect/http";
|
|
|
28
31
|
import { describe, expect, it } from "vite-plus/test";
|
|
29
32
|
|
|
30
33
|
const plain: Rule = { description: "Ports must be named.", must: "const port = PORT;" };
|
|
31
|
-
const lines = ["const port = 3000;"];
|
|
34
|
+
const lines = ["const port = 3000;"] as const;
|
|
32
35
|
const codeState = { code: { "1": lines[0] } };
|
|
33
36
|
const file = { path: "/repo/src/server.ts", contents: lines.join("\n") };
|
|
34
37
|
const symbols = { port: "```typescript\nconst port: number\n```" };
|
|
35
|
-
const references = { PORT:
|
|
38
|
+
const references = { "PORT:src/config.ts": "export const PORT = 3000;\r\n" };
|
|
36
39
|
const codeSufficiency = {
|
|
37
40
|
type: "noul",
|
|
38
41
|
instructions: {
|
|
@@ -73,9 +76,12 @@ const recordedJev = (
|
|
|
73
76
|
Layer.provide([
|
|
74
77
|
Layer.succeed(FetchHttpClient.Fetch, fetch),
|
|
75
78
|
Layer.succeed(AdhereConfig, {
|
|
79
|
+
typeAware: false,
|
|
76
80
|
model: "jev-latest",
|
|
77
81
|
threshold: 0.8,
|
|
78
82
|
sufficiencyThreshold: 0.6,
|
|
83
|
+
maxSymbolDocumentationLength: DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
84
|
+
maxSymbolLength: DEFAULT_MAX_SYMBOL_LENGTH,
|
|
79
85
|
reads,
|
|
80
86
|
rules: {},
|
|
81
87
|
workspacePackages,
|
|
@@ -153,7 +159,19 @@ describe("prepared shared Jev reads", () => {
|
|
|
153
159
|
project: { language: "typescript" },
|
|
154
160
|
code: { "1": "replaced" },
|
|
155
161
|
};
|
|
156
|
-
const enriched = { ...readState, ...codeState };
|
|
162
|
+
const enriched = describeState({ ...readState, ...codeState });
|
|
163
|
+
expect(enriched.__comment).toMatch(/only.*code|code.*only/i);
|
|
164
|
+
expect(enriched.__comment).toMatch(/context/i);
|
|
165
|
+
expect(enriched.symbols).toEqual({ ...symbols, __comment: expect.stringMatching(/symbol/i) });
|
|
166
|
+
expect(enriched.references).toEqual({
|
|
167
|
+
...references,
|
|
168
|
+
__comment: expect.stringMatching(/symbol.*path|symbol.*file/i),
|
|
169
|
+
});
|
|
170
|
+
expect(enriched.workspacePackages).toEqual({
|
|
171
|
+
orders: "packages/orders",
|
|
172
|
+
__comment: expect.stringMatching(/package/i),
|
|
173
|
+
});
|
|
174
|
+
expect(Object.keys(enriched.code)).toEqual(["1"]);
|
|
157
175
|
expect(judgeBody("", lines, rules, [], readState).state).toEqual(enriched);
|
|
158
176
|
expect(locateBody("", lines, rules, readState).state).toEqual(enriched);
|
|
159
177
|
const groups = requestGroups(rules);
|
|
@@ -166,8 +184,8 @@ describe("prepared shared Jev reads", () => {
|
|
|
166
184
|
expect(judgeBody("", lines, group, [], readState).state).toEqual(enriched);
|
|
167
185
|
const body = locateBody("", lines, group, readState);
|
|
168
186
|
expect(body.state).toEqual(enriched);
|
|
169
|
-
expect(body.state.symbols).
|
|
170
|
-
expect(body.state.references).
|
|
187
|
+
expect(body.state.symbols).toEqual({ ...symbols, __comment: expect.any(String) });
|
|
188
|
+
expect(body.state.references).toEqual({ ...references, __comment: expect.any(String) });
|
|
171
189
|
for (const id of Object.keys(group)) {
|
|
172
190
|
expect(body.questions[`sufficient:${id}`]).toEqual(sufficiencyQuestion(plain, readState));
|
|
173
191
|
}
|
|
@@ -176,9 +194,56 @@ describe("prepared shared Jev reads", () => {
|
|
|
176
194
|
expect(locateRequests(lines, rules, readState)).toBe(3);
|
|
177
195
|
});
|
|
178
196
|
|
|
197
|
+
it("preserves caller comments and raw context through judge, locate, and appended maps", async () => {
|
|
198
|
+
const readState = {
|
|
199
|
+
__comment: "User context: only target code is audited.",
|
|
200
|
+
symbols: { ...symbols, __comment: "User symbol description." },
|
|
201
|
+
references: { ...references, __comment: "User reference description." },
|
|
202
|
+
workspacePackages: { orders: "packages/orders", __comment: "User package description." },
|
|
203
|
+
};
|
|
204
|
+
const original = JSON.stringify(readState);
|
|
205
|
+
const hooked = {
|
|
206
|
+
...plain,
|
|
207
|
+
appendState: () => ({ references: { "port:src/caller.ts": "\tuse(port);\r\n" } }),
|
|
208
|
+
};
|
|
209
|
+
const { sent, layer } = recordedJev();
|
|
210
|
+
await Effect.runPromise(
|
|
211
|
+
Effect.gen(function* () {
|
|
212
|
+
const jev = yield* Jev;
|
|
213
|
+
yield* jev.judge(lines, { plain, hooked }, [], file, readState);
|
|
214
|
+
yield* jev.locate(lines, { plain, hooked }, file, readState);
|
|
215
|
+
}).pipe(Effect.provide(layer)),
|
|
216
|
+
);
|
|
217
|
+
expect(sent).toHaveLength(4);
|
|
218
|
+
for (const body of sent) {
|
|
219
|
+
expect(body.state.code).toEqual(codeState.code);
|
|
220
|
+
expect(body.state.__comment).toBe(readState.__comment);
|
|
221
|
+
expect(body.state.symbols).toEqual(readState.symbols);
|
|
222
|
+
expect(body.state.workspacePackages).toEqual(readState.workspacePackages);
|
|
223
|
+
}
|
|
224
|
+
expect(sent[0]?.state.references).toEqual(readState.references);
|
|
225
|
+
expect(sent[2]?.state.references).toEqual(readState.references);
|
|
226
|
+
for (const index of [1, 3]) {
|
|
227
|
+
expect(sent[index]?.state.references).toEqual({
|
|
228
|
+
"port:src/caller.ts": "\tuse(port);\r\n",
|
|
229
|
+
__comment: expect.stringMatching(/symbol.*path|symbol.*file/i),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
expect(JSON.stringify(readState)).toBe(original);
|
|
233
|
+
expect(
|
|
234
|
+
sent
|
|
235
|
+
.slice(2)
|
|
236
|
+
.flatMap(({ questions }) => Object.values(questions))
|
|
237
|
+
.filter(({ type }) => type === "noul")
|
|
238
|
+
.every(({ instructions }) => !instructions.question.includes("__comment")),
|
|
239
|
+
).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
179
242
|
it("names actual shared keys in sufficiency, including packages but excluding duplicate code", () => {
|
|
180
243
|
expect(sufficiencyQuestion(plain)).toEqual(codeSufficiency);
|
|
181
|
-
expect(sufficiencyQuestion(plain, { code: {} })).toEqual(
|
|
244
|
+
expect(sufficiencyQuestion(plain, { code: {}, __comment: "context description" })).toEqual(
|
|
245
|
+
codeSufficiency,
|
|
246
|
+
);
|
|
182
247
|
expect(sufficiencyQuestion({ ...plain, appendState: () => ({ private: true }) })).toEqual(
|
|
183
248
|
codeSufficiency,
|
|
184
249
|
);
|
|
@@ -186,22 +251,32 @@ describe("prepared shared Jev reads", () => {
|
|
|
186
251
|
{ workspacePackages: {} },
|
|
187
252
|
{ symbols },
|
|
188
253
|
{ references },
|
|
189
|
-
{
|
|
254
|
+
{
|
|
255
|
+
workspacePackages: {},
|
|
256
|
+
symbols,
|
|
257
|
+
references,
|
|
258
|
+
project: true,
|
|
259
|
+
code: {},
|
|
260
|
+
__comment: "context description",
|
|
261
|
+
},
|
|
190
262
|
]) {
|
|
191
263
|
const question = sufficiencyQuestion(plain, readState);
|
|
192
264
|
expect(question.instructions.question).not.toContain("`code` alone");
|
|
193
|
-
for (const key of Object.keys(readState).filter(
|
|
265
|
+
for (const key of Object.keys(readState).filter(
|
|
266
|
+
(key) => key !== "code" && key !== "__comment",
|
|
267
|
+
)) {
|
|
194
268
|
expect(question.instructions.question).toContain(`\`${key}\``);
|
|
195
269
|
expect(question.criteria.true).toContain(`\`${key}\``);
|
|
196
270
|
expect(question.criteria.false).toContain(`\`${key}\``);
|
|
197
271
|
}
|
|
198
272
|
expect(question.instructions.question.match(/`code`/g)).toHaveLength(2);
|
|
273
|
+
expect(JSON.stringify(question)).not.toContain("__comment");
|
|
199
274
|
}
|
|
200
275
|
});
|
|
201
276
|
|
|
202
277
|
it("budgets all shared context beside every rule's questions", () => {
|
|
203
278
|
const source = "export const value = 1;\n".repeat(2200);
|
|
204
|
-
const readState = { references: { value:
|
|
279
|
+
const readState = { references: { "value:src/related.ts": source } };
|
|
205
280
|
const large = { ...plain, must: "x".repeat(60_000) };
|
|
206
281
|
const hooked = { ...plain, appendState: () => ({ private: "x".repeat(120_000) }) };
|
|
207
282
|
expect(fits(lines, { large, hooked })).toBe(true);
|
|
@@ -295,7 +370,7 @@ describe("prepared shared Jev reads", () => {
|
|
|
295
370
|
};
|
|
296
371
|
const rules = { plain, native };
|
|
297
372
|
const readState = {
|
|
298
|
-
references: {
|
|
373
|
+
references: { "value:src/related.ts": "export const value = 1;\n".repeat(1500) },
|
|
299
374
|
};
|
|
300
375
|
const sampled = ["native"];
|
|
301
376
|
expect(fits(long, rules, readState)).toBe(true);
|
|
@@ -338,7 +413,7 @@ describe("prepared shared Jev reads", () => {
|
|
|
338
413
|
|
|
339
414
|
it("sends shared context once for non-hook rules and inherits it in private hook groups", async () => {
|
|
340
415
|
const seen: Array<JevState> = [];
|
|
341
|
-
const privateSymbols =
|
|
416
|
+
const privateSymbols = { private: "const private: number" };
|
|
342
417
|
const appendState: AppendState = (state, receivedFile) => {
|
|
343
418
|
expect(receivedFile).toBe(file);
|
|
344
419
|
seen.push(state);
|
|
@@ -373,16 +448,19 @@ describe("prepared shared Jev reads", () => {
|
|
|
373
448
|
});
|
|
374
449
|
expect(result.judged.linter).toEqual({ plain: 0.9, hooked: 0.9 });
|
|
375
450
|
expect(Object.keys(result.located)).toEqual(Object.keys(rules));
|
|
376
|
-
const enriched = { ...readState, ...codeState };
|
|
451
|
+
const enriched = describeState({ ...readState, ...codeState });
|
|
377
452
|
expect(seen).toEqual([enriched, enriched, enriched, enriched]);
|
|
378
453
|
for (const state of seen) {
|
|
379
|
-
expect(state.symbols).
|
|
380
|
-
expect(state.references).
|
|
381
|
-
expect(state.workspacePackages).
|
|
454
|
+
expect(state.symbols).toEqual({ ...symbols, __comment: expect.any(String) });
|
|
455
|
+
expect(state.references).toEqual({ ...references, __comment: expect.any(String) });
|
|
456
|
+
expect(state.workspacePackages).toEqual({
|
|
457
|
+
...workspacePackages,
|
|
458
|
+
__comment: expect.any(String),
|
|
459
|
+
});
|
|
382
460
|
}
|
|
383
461
|
const expectedStates = [
|
|
384
462
|
enriched,
|
|
385
|
-
{ ...enriched, private: "first", symbols: privateSymbols },
|
|
463
|
+
describeState({ ...enriched, private: "first", symbols: privateSymbols }),
|
|
386
464
|
{ ...enriched, private: "second" },
|
|
387
465
|
];
|
|
388
466
|
expect(sent.map((body) => body.state)).toEqual([...expectedStates, ...expectedStates]);
|
|
@@ -416,9 +494,10 @@ describe("prepared shared Jev reads", () => {
|
|
|
416
494
|
expect(readState.code).toEqual({});
|
|
417
495
|
});
|
|
418
496
|
|
|
419
|
-
it("
|
|
497
|
+
it("uses only caller-prepared context in HTTP with workspace-only defaults", async () => {
|
|
420
498
|
const config = resolveConfig({ presets: [], rules: { plain } });
|
|
421
|
-
expect(config.reads).toEqual(["workspacePackages"
|
|
499
|
+
expect(config.reads).toEqual(["workspacePackages"]);
|
|
500
|
+
expect(config.typeAware).toBe(false);
|
|
422
501
|
const { sent, layer } = recordedJev({ unused: "packages/unused" }, config.reads);
|
|
423
502
|
await Effect.runPromise(
|
|
424
503
|
Effect.gen(function* () {
|
|
@@ -432,8 +511,8 @@ describe("prepared shared Jev reads", () => {
|
|
|
432
511
|
expect(sent.map((body) => body.state)).toEqual([
|
|
433
512
|
codeState,
|
|
434
513
|
codeState,
|
|
435
|
-
{ symbols, ...codeState },
|
|
436
|
-
{ symbols, ...codeState },
|
|
514
|
+
describeState({ symbols, ...codeState }),
|
|
515
|
+
describeState({ symbols, ...codeState }),
|
|
437
516
|
]);
|
|
438
517
|
});
|
|
439
518
|
});
|
package/src/services/Jev.ts
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
type Rules,
|
|
10
10
|
} from "#config.ts";
|
|
11
11
|
import { type Range, sectionsOf } from "#excerpt.ts";
|
|
12
|
-
import type { SystemOneRequest } from "#providers/systemOne.ts";
|
|
13
|
-
import { Context,
|
|
12
|
+
import type { SystemOneQuestion, SystemOneRequest } from "#providers/systemOne.ts";
|
|
13
|
+
import { Context, Effect, LogLevel, Record, References, Schema } from "effect";
|
|
14
14
|
|
|
15
15
|
export type { Rules };
|
|
16
16
|
export type Lines = ReadonlyArray<string>;
|
|
@@ -139,6 +139,32 @@ const bytesOf = (value: unknown): number => encoder.encode(JSON.stringify(value)
|
|
|
139
139
|
*/
|
|
140
140
|
export const tokensOf = (value: unknown): number => Math.ceil(bytesOf(value) / 3);
|
|
141
141
|
|
|
142
|
+
const READ_COMMENTS: Readonly<Record<string, string>> = {
|
|
143
|
+
symbols:
|
|
144
|
+
"Compiler-rendered types and documentation for symbols used in `code`; keys are symbol names. A declared type, assertion, or type predicate is only a compile-time claim, not proof of runtime validation. When `references` show an implementation, inspect its actual checks. A predicate that returns true, tests only truthiness, or checks only whether a value is an object does not validate the fields it claims.",
|
|
145
|
+
references:
|
|
146
|
+
"Declarations of symbols used in `code`. Each key is `symbol:pathname`: the symbol name as used in `code` followed by its repository-relative declaration path. Each value is a complete declaration excerpt.",
|
|
147
|
+
workspacePackages:
|
|
148
|
+
"Workspace packages imported by `code`; keys are package names and values are repository-relative package directories. Only packages imported by this file are listed.",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Explain the known context maps while preserving caller-provided descriptions and target code. */
|
|
152
|
+
export const describeState = (state: JevState): JevState => {
|
|
153
|
+
const described = { ...state };
|
|
154
|
+
for (const [name, comment] of Object.entries(READ_COMMENTS)) {
|
|
155
|
+
const value = state[name];
|
|
156
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value))
|
|
157
|
+
described[name] = { __comment: comment, ...value };
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
described.__comment === undefined &&
|
|
161
|
+
Object.keys(state).some((name) => name !== "code" && name !== "__comment")
|
|
162
|
+
)
|
|
163
|
+
described.__comment =
|
|
164
|
+
"Only `code` is the audit target; it is split into numbered sections. All other state entries provide context, not additional code to audit.";
|
|
165
|
+
return described;
|
|
166
|
+
};
|
|
167
|
+
|
|
142
168
|
/**
|
|
143
169
|
* The file as Jev reads it: its sections, by `sectionsOf`, under their
|
|
144
170
|
* numbers from 1, so the locate question can name one. Prepared context is
|
|
@@ -149,15 +175,16 @@ const stateOf = (
|
|
|
149
175
|
lines: Lines,
|
|
150
176
|
readState: ReadState = {},
|
|
151
177
|
sections: ReadonlyArray<Range> = sectionsOf(lines),
|
|
152
|
-
): JevState =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
178
|
+
): JevState =>
|
|
179
|
+
describeState({
|
|
180
|
+
...readState,
|
|
181
|
+
code: Object.fromEntries(
|
|
182
|
+
sections.map((section, index) => [
|
|
183
|
+
String(index + 1),
|
|
184
|
+
lines.slice(section.first - 1, section.last).join("\n"),
|
|
185
|
+
]),
|
|
186
|
+
),
|
|
187
|
+
});
|
|
161
188
|
|
|
162
189
|
/** The field an example goes under in a question: its word, with `should not` as `should_not`. */
|
|
163
190
|
const fieldOf = (example: Example): string => example.word.replace(" ", "_");
|
|
@@ -361,7 +388,7 @@ export const judgeBody = (
|
|
|
361
388
|
* it scored below 0.6, 62% were false, where 22% of all were.
|
|
362
389
|
*/
|
|
363
390
|
const sufficiencyQuestionWith = (fields: ReturnType<typeof ruleFields>, readState: ReadState) => {
|
|
364
|
-
const keys = Object.keys(readState).filter((key) => key !== "code");
|
|
391
|
+
const keys = Object.keys(readState).filter((key) => key !== "code" && key !== "__comment");
|
|
365
392
|
const context =
|
|
366
393
|
keys.length === 0 ? undefined : ["`code`", ...keys.map((key) => `\`${key}\``)].join(" and ");
|
|
367
394
|
return {
|
|
@@ -547,6 +574,43 @@ export const fits = (lines: Lines, rules: Rules, readState: ReadState = {}): boo
|
|
|
547
574
|
/** A request to Jev, its questions typed as `Q`: `Question`, as adhere asks them, or anything an eval arm asks. */
|
|
548
575
|
export type Body<Q = unknown> = SystemOneRequest<Q>;
|
|
549
576
|
|
|
577
|
+
type RequestPayload =
|
|
578
|
+
| Body<SystemOneQuestion>
|
|
579
|
+
| { readonly judge: Body<SystemOneQuestion>; readonly locate: Body<SystemOneQuestion> };
|
|
580
|
+
|
|
581
|
+
const traceBody = (body: Body<SystemOneQuestion>) => ({
|
|
582
|
+
...body,
|
|
583
|
+
state: { code: body.state.code, ...body.state },
|
|
584
|
+
questions: Record.map(body.questions, (question, id) =>
|
|
585
|
+
question.type === "choice" && /^(start|end):/.test(id)
|
|
586
|
+
? { ...question, criteria: undefined }
|
|
587
|
+
: question,
|
|
588
|
+
),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
/** Debug/Trace put target code first and omit line-option copies; All retains the complete request. */
|
|
592
|
+
export const logRequestPayload = (
|
|
593
|
+
message: string,
|
|
594
|
+
payload: () => RequestPayload,
|
|
595
|
+
level: "Debug" | "Trace" = "Trace",
|
|
596
|
+
): Effect.Effect<void> =>
|
|
597
|
+
Effect.gen(function* () {
|
|
598
|
+
const minimum = yield* References.MinimumLogLevel;
|
|
599
|
+
if (LogLevel.isGreaterThan(minimum, level)) return;
|
|
600
|
+
const full = payload();
|
|
601
|
+
const visible =
|
|
602
|
+
minimum === "All"
|
|
603
|
+
? full
|
|
604
|
+
: "questions" in full
|
|
605
|
+
? traceBody(full)
|
|
606
|
+
: { judge: traceBody(full.judge), locate: traceBody(full.locate) };
|
|
607
|
+
// Consume escaped backslashes first so source literals such as "\\n" remain literals.
|
|
608
|
+
const text = JSON.stringify(visible, null, 2).replace(/\\\\|\\[nt]/g, (escaped) =>
|
|
609
|
+
escaped === "\\n" ? "\n" : escaped === "\\t" ? "\t" : escaped,
|
|
610
|
+
);
|
|
611
|
+
yield* (level === "Debug" ? Effect.logDebug : Effect.logTrace)(`${message}\n${text}`);
|
|
612
|
+
});
|
|
613
|
+
|
|
550
614
|
/**
|
|
551
615
|
* A body as requests Jev can take: its questions in order, with a new
|
|
552
616
|
* request whenever the next question would not fit beside the state and
|