@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.
@@ -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: { "src/config.ts": "export const PORT = 3000;\r\n" } };
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).toBe(symbols);
170
- expect(body.state.references).toBe(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(codeSufficiency);
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
- { workspacePackages: {}, symbols, references, project: true, code: {} },
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((key) => key !== "code")) {
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: { "src/related.ts": source } } };
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: { files: { "src/related.ts": "export const value = 1;\n".repeat(1500) } },
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 = [{ name: "private", section: 1, usages: 0 }];
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).toBe(symbols);
380
- expect(state.references).toBe(references);
381
- expect(state.workspacePackages).toBe(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
  });
@@ -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, type Effect, Record, Schema } from "effect";
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
- ...readState,
154
- code: Object.fromEntries(
155
- sections.map((section, index) => [
156
- String(index + 1),
157
- lines.slice(section.first - 1, section.last).join("\n"),
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