@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
|
@@ -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: {
|
|
@@ -76,6 +79,8 @@ const recordedJev = (
|
|
|
76
79
|
model: "jev-latest",
|
|
77
80
|
threshold: 0.8,
|
|
78
81
|
sufficiencyThreshold: 0.6,
|
|
82
|
+
maxSymbolDocumentationLength: DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
83
|
+
maxSymbolLength: DEFAULT_MAX_SYMBOL_LENGTH,
|
|
79
84
|
reads,
|
|
80
85
|
rules: {},
|
|
81
86
|
workspacePackages,
|
|
@@ -153,7 +158,19 @@ describe("prepared shared Jev reads", () => {
|
|
|
153
158
|
project: { language: "typescript" },
|
|
154
159
|
code: { "1": "replaced" },
|
|
155
160
|
};
|
|
156
|
-
const enriched = { ...readState, ...codeState };
|
|
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"]);
|
|
157
174
|
expect(judgeBody("", lines, rules, [], readState).state).toEqual(enriched);
|
|
158
175
|
expect(locateBody("", lines, rules, readState).state).toEqual(enriched);
|
|
159
176
|
const groups = requestGroups(rules);
|
|
@@ -166,8 +183,8 @@ describe("prepared shared Jev reads", () => {
|
|
|
166
183
|
expect(judgeBody("", lines, group, [], readState).state).toEqual(enriched);
|
|
167
184
|
const body = locateBody("", lines, group, readState);
|
|
168
185
|
expect(body.state).toEqual(enriched);
|
|
169
|
-
expect(body.state.symbols).
|
|
170
|
-
expect(body.state.references).
|
|
186
|
+
expect(body.state.symbols).toEqual({ ...symbols, __comment: expect.any(String) });
|
|
187
|
+
expect(body.state.references).toEqual({ ...references, __comment: expect.any(String) });
|
|
171
188
|
for (const id of Object.keys(group)) {
|
|
172
189
|
expect(body.questions[`sufficient:${id}`]).toEqual(sufficiencyQuestion(plain, readState));
|
|
173
190
|
}
|
|
@@ -176,9 +193,56 @@ describe("prepared shared Jev reads", () => {
|
|
|
176
193
|
expect(locateRequests(lines, rules, readState)).toBe(3);
|
|
177
194
|
});
|
|
178
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
|
+
|
|
179
241
|
it("names actual shared keys in sufficiency, including packages but excluding duplicate code", () => {
|
|
180
242
|
expect(sufficiencyQuestion(plain)).toEqual(codeSufficiency);
|
|
181
|
-
expect(sufficiencyQuestion(plain, { code: {} })).toEqual(
|
|
243
|
+
expect(sufficiencyQuestion(plain, { code: {}, __comment: "context description" })).toEqual(
|
|
244
|
+
codeSufficiency,
|
|
245
|
+
);
|
|
182
246
|
expect(sufficiencyQuestion({ ...plain, appendState: () => ({ private: true }) })).toEqual(
|
|
183
247
|
codeSufficiency,
|
|
184
248
|
);
|
|
@@ -186,22 +250,32 @@ describe("prepared shared Jev reads", () => {
|
|
|
186
250
|
{ workspacePackages: {} },
|
|
187
251
|
{ symbols },
|
|
188
252
|
{ references },
|
|
189
|
-
{
|
|
253
|
+
{
|
|
254
|
+
workspacePackages: {},
|
|
255
|
+
symbols,
|
|
256
|
+
references,
|
|
257
|
+
project: true,
|
|
258
|
+
code: {},
|
|
259
|
+
__comment: "context description",
|
|
260
|
+
},
|
|
190
261
|
]) {
|
|
191
262
|
const question = sufficiencyQuestion(plain, readState);
|
|
192
263
|
expect(question.instructions.question).not.toContain("`code` alone");
|
|
193
|
-
for (const key of Object.keys(readState).filter(
|
|
264
|
+
for (const key of Object.keys(readState).filter(
|
|
265
|
+
(key) => key !== "code" && key !== "__comment",
|
|
266
|
+
)) {
|
|
194
267
|
expect(question.instructions.question).toContain(`\`${key}\``);
|
|
195
268
|
expect(question.criteria.true).toContain(`\`${key}\``);
|
|
196
269
|
expect(question.criteria.false).toContain(`\`${key}\``);
|
|
197
270
|
}
|
|
198
271
|
expect(question.instructions.question.match(/`code`/g)).toHaveLength(2);
|
|
272
|
+
expect(JSON.stringify(question)).not.toContain("__comment");
|
|
199
273
|
}
|
|
200
274
|
});
|
|
201
275
|
|
|
202
276
|
it("budgets all shared context beside every rule's questions", () => {
|
|
203
277
|
const source = "export const value = 1;\n".repeat(2200);
|
|
204
|
-
const readState = { references: { value:
|
|
278
|
+
const readState = { references: { "value:src/related.ts": source } };
|
|
205
279
|
const large = { ...plain, must: "x".repeat(60_000) };
|
|
206
280
|
const hooked = { ...plain, appendState: () => ({ private: "x".repeat(120_000) }) };
|
|
207
281
|
expect(fits(lines, { large, hooked })).toBe(true);
|
|
@@ -295,7 +369,7 @@ describe("prepared shared Jev reads", () => {
|
|
|
295
369
|
};
|
|
296
370
|
const rules = { plain, native };
|
|
297
371
|
const readState = {
|
|
298
|
-
references: {
|
|
372
|
+
references: { "value:src/related.ts": "export const value = 1;\n".repeat(1500) },
|
|
299
373
|
};
|
|
300
374
|
const sampled = ["native"];
|
|
301
375
|
expect(fits(long, rules, readState)).toBe(true);
|
|
@@ -338,7 +412,7 @@ describe("prepared shared Jev reads", () => {
|
|
|
338
412
|
|
|
339
413
|
it("sends shared context once for non-hook rules and inherits it in private hook groups", async () => {
|
|
340
414
|
const seen: Array<JevState> = [];
|
|
341
|
-
const privateSymbols =
|
|
415
|
+
const privateSymbols = { private: "const private: number" };
|
|
342
416
|
const appendState: AppendState = (state, receivedFile) => {
|
|
343
417
|
expect(receivedFile).toBe(file);
|
|
344
418
|
seen.push(state);
|
|
@@ -373,16 +447,19 @@ describe("prepared shared Jev reads", () => {
|
|
|
373
447
|
});
|
|
374
448
|
expect(result.judged.linter).toEqual({ plain: 0.9, hooked: 0.9 });
|
|
375
449
|
expect(Object.keys(result.located)).toEqual(Object.keys(rules));
|
|
376
|
-
const enriched = { ...readState, ...codeState };
|
|
450
|
+
const enriched = describeState({ ...readState, ...codeState });
|
|
377
451
|
expect(seen).toEqual([enriched, enriched, enriched, enriched]);
|
|
378
452
|
for (const state of seen) {
|
|
379
|
-
expect(state.symbols).
|
|
380
|
-
expect(state.references).
|
|
381
|
-
expect(state.workspacePackages).
|
|
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
|
+
});
|
|
382
459
|
}
|
|
383
460
|
const expectedStates = [
|
|
384
461
|
enriched,
|
|
385
|
-
{ ...enriched, private: "first", symbols: privateSymbols },
|
|
462
|
+
describeState({ ...enriched, private: "first", symbols: privateSymbols }),
|
|
386
463
|
{ ...enriched, private: "second" },
|
|
387
464
|
];
|
|
388
465
|
expect(sent.map((body) => body.state)).toEqual([...expectedStates, ...expectedStates]);
|
|
@@ -432,8 +509,8 @@ describe("prepared shared Jev reads", () => {
|
|
|
432
509
|
expect(sent.map((body) => body.state)).toEqual([
|
|
433
510
|
codeState,
|
|
434
511
|
codeState,
|
|
435
|
-
{ symbols, ...codeState },
|
|
436
|
-
{ symbols, ...codeState },
|
|
512
|
+
describeState({ symbols, ...codeState }),
|
|
513
|
+
describeState({ symbols, ...codeState }),
|
|
437
514
|
]);
|
|
438
515
|
});
|
|
439
516
|
});
|
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-inferred types, documentation, and JSDoc for symbols in `code`; keys are symbol names.",
|
|
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
|