@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
package/src/services/Jev.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Example,
|
|
3
3
|
examplesOf,
|
|
4
|
+
type JevState,
|
|
4
5
|
type JudgedFile,
|
|
6
|
+
type ReadState,
|
|
5
7
|
type Rule,
|
|
6
8
|
type RuleId,
|
|
7
9
|
type Rules,
|
|
8
10
|
} from "#config.ts";
|
|
9
11
|
import { type Range, sectionsOf } from "#excerpt.ts";
|
|
10
|
-
import
|
|
12
|
+
import type { SystemOneQuestion, SystemOneRequest } from "#providers/systemOne.ts";
|
|
13
|
+
import { Context, Effect, LogLevel, Record, References, Schema } from "effect";
|
|
11
14
|
|
|
12
15
|
export type { Rules };
|
|
13
16
|
export type Lines = ReadonlyArray<string>;
|
|
@@ -54,23 +57,26 @@ export class Jev extends Context.Service<
|
|
|
54
57
|
/**
|
|
55
58
|
* Each rule's probability that the file breaks it. A rule in `sampled`
|
|
56
59
|
* also carries the linter check's question, whose answer comes back
|
|
57
|
-
* under `linter`. `file` is what a rule's `appendState` gets
|
|
60
|
+
* under `linter`. `file` is what a rule's `appendState` gets;
|
|
61
|
+
* `readState` is the shared context already prepared for the file.
|
|
58
62
|
*/
|
|
59
63
|
readonly judge: (
|
|
60
64
|
lines: Lines,
|
|
61
65
|
rules: Rules,
|
|
62
66
|
sampled: ReadonlyArray<RuleId>,
|
|
63
67
|
file: JudgedFile,
|
|
68
|
+
readState?: ReadState,
|
|
64
69
|
) => Effect.Effect<Judged, JevUnavailable | JevOverflow | JevBlocked>;
|
|
65
70
|
/**
|
|
66
71
|
* Per rule, the section of the file that most clearly breaks it, and
|
|
67
|
-
* whether the file
|
|
68
|
-
* question for with nothing it offered is left out.
|
|
72
|
+
* whether the file and its prepared context show enough to decide.
|
|
73
|
+
* A rule Jev answers either question for with nothing it offered is left out.
|
|
69
74
|
*/
|
|
70
75
|
readonly locate: (
|
|
71
76
|
lines: Lines,
|
|
72
77
|
rules: Rules,
|
|
73
78
|
file: JudgedFile,
|
|
79
|
+
readState?: ReadState,
|
|
74
80
|
) => Effect.Effect<Record<RuleId, Located>, JevUnavailable | JevOverflow | JevBlocked>;
|
|
75
81
|
/**
|
|
76
82
|
* Per rule, the partner Jev names as impossible to follow in the same code,
|
|
@@ -95,7 +101,7 @@ export class Jev extends Context.Service<
|
|
|
95
101
|
/** Where a finding is, and how sure Jev is that the file shows enough to decide it. */
|
|
96
102
|
export interface Located {
|
|
97
103
|
readonly section: Range;
|
|
98
|
-
/** Jev's probability that everything judging the rule turns on
|
|
104
|
+
/** Jev's probability that the file and its shared context show everything judging the rule turns on. */
|
|
99
105
|
readonly sufficiency: number;
|
|
100
106
|
/** The lines in the section the code that breaks the rule starts and ends on, when Jev named them. */
|
|
101
107
|
readonly lines?: Range;
|
|
@@ -123,6 +129,7 @@ const REQUEST_CONTEXT = 64_000;
|
|
|
123
129
|
const WRAPPER = 1_000;
|
|
124
130
|
|
|
125
131
|
const encoder = new TextEncoder();
|
|
132
|
+
const bytesOf = (value: unknown): number => encoder.encode(JSON.stringify(value)).length;
|
|
126
133
|
|
|
127
134
|
/**
|
|
128
135
|
* An estimate, meant to run high, of the tokens Jev reads for a value: one
|
|
@@ -130,23 +137,54 @@ const encoder = new TextEncoder();
|
|
|
130
137
|
* bytes a token, and this ran 12 to 16 percent high on the eval's requests.
|
|
131
138
|
* Text that packs more tokens into its bytes, such as CJK, can outrun it.
|
|
132
139
|
*/
|
|
133
|
-
export const tokensOf = (value: unknown): number =>
|
|
134
|
-
|
|
140
|
+
export const tokensOf = (value: unknown): number => Math.ceil(bytesOf(value) / 3);
|
|
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
|
+
};
|
|
135
167
|
|
|
136
168
|
/**
|
|
137
169
|
* The file as Jev reads it: its sections, by `sectionsOf`, under their
|
|
138
|
-
* numbers from 1, so the locate question can name one.
|
|
139
|
-
*
|
|
140
|
-
* numbered, and located as well as a choice among the lines.
|
|
170
|
+
* numbers from 1, so the locate question can name one. Prepared context is
|
|
171
|
+
* added beside the code, never in its place. On the eval it judged as well as
|
|
172
|
+
* the file with every line numbered, and located as well as a choice among the lines.
|
|
141
173
|
*/
|
|
142
|
-
const stateOf = (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
const stateOf = (
|
|
175
|
+
lines: Lines,
|
|
176
|
+
readState: ReadState = {},
|
|
177
|
+
sections: ReadonlyArray<Range> = sectionsOf(lines),
|
|
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
|
+
});
|
|
150
188
|
|
|
151
189
|
/** The field an example goes under in a question: its word, with `should not` as `should_not`. */
|
|
152
190
|
const fieldOf = (example: Example): string => example.word.replace(" ", "_");
|
|
@@ -198,7 +236,7 @@ const comparedState = (rules: ReadonlyArray<ComparedRule>, mentioned: Iterable<n
|
|
|
198
236
|
* asking whether code "must be written the way `must` shows": the same words,
|
|
199
237
|
* but a pattern to compare rather than code to match.
|
|
200
238
|
*/
|
|
201
|
-
|
|
239
|
+
const judgeQuestionWith = (rule: Rule, fields: ReturnType<typeof ruleFields>) => {
|
|
202
240
|
const { good, bad } = examplesOf(rule);
|
|
203
241
|
const avoided = fieldOf(bad ?? { word: "never", code: "" });
|
|
204
242
|
const example = bad === undefined ? "" : `, for example by doing what \`${avoided}\` shows`;
|
|
@@ -216,7 +254,7 @@ export const judgeQuestion = (rule: Rule) => {
|
|
|
216
254
|
];
|
|
217
255
|
return {
|
|
218
256
|
type: "noul" as const,
|
|
219
|
-
instructions: { question, ...
|
|
257
|
+
instructions: { question, ...fields },
|
|
220
258
|
criteria: {
|
|
221
259
|
true: `${yes}, in code that \`rule\` is about`,
|
|
222
260
|
false: `${no}, or has no code that \`rule\` is about`,
|
|
@@ -224,6 +262,8 @@ export const judgeQuestion = (rule: Rule) => {
|
|
|
224
262
|
};
|
|
225
263
|
};
|
|
226
264
|
|
|
265
|
+
export const judgeQuestion = (rule: Rule) => judgeQuestionWith(rule, ruleFields(rule));
|
|
266
|
+
|
|
227
267
|
/** What the offending section does, for the locate question. */
|
|
228
268
|
const offense = (rule: Rule): string => {
|
|
229
269
|
const { good, bad } = examplesOf(rule);
|
|
@@ -269,8 +309,11 @@ type MatcherKind = keyof MatcherScores;
|
|
|
269
309
|
* `appliesTo`, whether any of that code is as described, and for `excludeIf`,
|
|
270
310
|
* whether all of it is. It carries the rule as the judge question does.
|
|
271
311
|
*/
|
|
272
|
-
|
|
273
|
-
|
|
312
|
+
const matcherQuestionWith = (
|
|
313
|
+
instructions: ReturnType<typeof judgeQuestion>["instructions"],
|
|
314
|
+
kind: MatcherKind,
|
|
315
|
+
matcher: string,
|
|
316
|
+
) => {
|
|
274
317
|
const [quantity, yes, no] =
|
|
275
318
|
kind === "appliesTo" ? ["any", "Some", "None"] : ["all", "All", "Not all"];
|
|
276
319
|
return {
|
|
@@ -287,6 +330,9 @@ export const matcherQuestion = (rule: Rule, kind: MatcherKind, matcher: string)
|
|
|
287
330
|
};
|
|
288
331
|
};
|
|
289
332
|
|
|
333
|
+
export const matcherQuestion = (rule: Rule, kind: MatcherKind, matcher: string) =>
|
|
334
|
+
matcherQuestionWith(judgeQuestion(rule).instructions, kind, matcher);
|
|
335
|
+
|
|
290
336
|
/** The key a rule's matcher question rides under, beside its judge question. */
|
|
291
337
|
export const matcherKey = (kind: MatcherKind, id: RuleId, index: number): string =>
|
|
292
338
|
`${kind}:${id}:${index}`;
|
|
@@ -302,9 +348,9 @@ export const matcherQuestions = (id: RuleId, rule: Rule) =>
|
|
|
302
348
|
),
|
|
303
349
|
);
|
|
304
350
|
|
|
305
|
-
/** The
|
|
306
|
-
export const questionRoom = (lines: Lines): number =>
|
|
307
|
-
QUESTION_CONTEXT - WRAPPER - tokensOf(stateOf(lines));
|
|
351
|
+
/** The room for one question beside the file and its shared prepared context. Hooks are not predicted. */
|
|
352
|
+
export const questionRoom = (lines: Lines, readState: ReadState = {}): number =>
|
|
353
|
+
QUESTION_CONTEXT - WRAPPER - tokensOf(stateOf(lines, readState));
|
|
308
354
|
|
|
309
355
|
/**
|
|
310
356
|
* One question per rule, one per matcher of each rule, and the linter
|
|
@@ -315,9 +361,10 @@ export const judgeBody = (
|
|
|
315
361
|
lines: Lines,
|
|
316
362
|
rules: Rules,
|
|
317
363
|
sampled: ReadonlyArray<RuleId> = [],
|
|
364
|
+
readState: ReadState = {},
|
|
318
365
|
) => ({
|
|
319
366
|
model,
|
|
320
|
-
state: stateOf(lines),
|
|
367
|
+
state: stateOf(lines, readState),
|
|
321
368
|
questions: {
|
|
322
369
|
...Record.map(rules, (rule): ReturnType<typeof judgeQuestion> => judgeQuestion(rule)),
|
|
323
370
|
...Object.fromEntries(
|
|
@@ -340,19 +387,34 @@ export const judgeBody = (
|
|
|
340
387
|
* in a field, ranked real findings worse, and read no easier. Of findings
|
|
341
388
|
* it scored below 0.6, 62% were false, where 22% of all were.
|
|
342
389
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
390
|
+
const sufficiencyQuestionWith = (fields: ReturnType<typeof ruleFields>, readState: ReadState) => {
|
|
391
|
+
const keys = Object.keys(readState).filter((key) => key !== "code" && key !== "__comment");
|
|
392
|
+
const context =
|
|
393
|
+
keys.length === 0 ? undefined : ["`code`", ...keys.map((key) => `\`${key}\``)].join(" and ");
|
|
394
|
+
return {
|
|
395
|
+
type: "noul" as const,
|
|
396
|
+
instructions: {
|
|
397
|
+
question:
|
|
398
|
+
context === undefined
|
|
399
|
+
? "Can you tell whether `code` breaks `rule` from `code` alone, without knowing what other files, libraries, services, or configuration do?"
|
|
400
|
+
: `Can you tell whether \`code\` breaks \`rule\` from ${context}, without knowing what other files, libraries, services, or configuration do beyond what they show?`,
|
|
401
|
+
...fields,
|
|
402
|
+
},
|
|
403
|
+
criteria: {
|
|
404
|
+
true:
|
|
405
|
+
context === undefined
|
|
406
|
+
? "`code` shows everything needed to tell whether it breaks `rule`"
|
|
407
|
+
: `${context} show everything needed to tell whether \`code\` breaks \`rule\``,
|
|
408
|
+
false:
|
|
409
|
+
context === undefined
|
|
410
|
+
? "Whether `code` breaks `rule` depends on something `code` does not show, such as another file, a library, a service, or configuration"
|
|
411
|
+
: `Whether \`code\` breaks \`rule\` depends on something ${context} do not show, such as another file, a library, a service, or configuration`,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
export const sufficiencyQuestion = (rule: Rule, readState: ReadState = {}) =>
|
|
417
|
+
sufficiencyQuestionWith(ruleFields(rule), readState);
|
|
356
418
|
|
|
357
419
|
/** The key a rule's sufficiency question rides under, beside its locate question. */
|
|
358
420
|
export const sufficiencyKey = (id: RuleId): string => `sufficient:${id}`;
|
|
@@ -367,20 +429,39 @@ export const edgeKey = (edge: "start" | "end", id: RuleId, section: number): str
|
|
|
367
429
|
* line's text, since the state carries no line numbers.
|
|
368
430
|
*/
|
|
369
431
|
const edgeQuestion = (
|
|
370
|
-
|
|
432
|
+
fields: ReturnType<typeof ruleFields>,
|
|
371
433
|
edge: "start" | "end",
|
|
372
434
|
key: string,
|
|
373
|
-
|
|
374
|
-
numbers: ReadonlyArray<number>,
|
|
435
|
+
criteria: Readonly<Record<string, string>>,
|
|
375
436
|
) => ({
|
|
376
437
|
type: "choice" as const,
|
|
377
438
|
instructions: {
|
|
378
439
|
question: `Which line of \`code["${key}"]\` does the code that breaks \`rule\` ${edge} on?`,
|
|
379
|
-
...
|
|
440
|
+
...fields,
|
|
380
441
|
},
|
|
381
|
-
criteria
|
|
382
|
-
|
|
383
|
-
|
|
442
|
+
criteria,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const lineCriteria = (lines: Lines, section: Range): Record<string, string> => {
|
|
446
|
+
const criteria: Record<string, string> = {};
|
|
447
|
+
for (let line = section.first; line <= section.last; line++) {
|
|
448
|
+
const text = (lines[line - 1] ?? "").trim();
|
|
449
|
+
if (text !== "") criteria[String(line)] = text.slice(0, 120);
|
|
450
|
+
}
|
|
451
|
+
return criteria;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const sectionQuestion = (
|
|
455
|
+
rule: Rule,
|
|
456
|
+
fields: ReturnType<typeof ruleFields>,
|
|
457
|
+
criteria: Readonly<Record<string, null>>,
|
|
458
|
+
) => ({
|
|
459
|
+
type: "choice" as const,
|
|
460
|
+
instructions: {
|
|
461
|
+
question: `Which entry of \`code\` most clearly ${offense(rule)}?`,
|
|
462
|
+
...fields,
|
|
463
|
+
},
|
|
464
|
+
criteria,
|
|
384
465
|
});
|
|
385
466
|
|
|
386
467
|
/**
|
|
@@ -394,80 +475,150 @@ const edgeQuestion = (
|
|
|
394
475
|
* held a violation for 94% of real findings, and 98% where the section was
|
|
395
476
|
* right, and were most often one line.
|
|
396
477
|
*/
|
|
397
|
-
export const locateBody = (
|
|
398
|
-
|
|
478
|
+
export const locateBody = (
|
|
479
|
+
model: string,
|
|
480
|
+
lines: Lines,
|
|
481
|
+
rules: Rules,
|
|
482
|
+
readState: ReadState = {},
|
|
483
|
+
) => {
|
|
484
|
+
const sections = sectionsOf(lines);
|
|
485
|
+
const state = stateOf(lines, readState, sections);
|
|
399
486
|
const criteria = Record.map(state.code, () => null);
|
|
400
|
-
const choosing =
|
|
401
|
-
const withCode =
|
|
402
|
-
Array.from({ length: last - first + 1 }, (_, index) => first + index).filter(
|
|
403
|
-
(line) => (lines[line - 1] ?? "").trim() !== "",
|
|
404
|
-
),
|
|
405
|
-
);
|
|
487
|
+
const choosing = sections.length > 1;
|
|
488
|
+
const withCode = sections.map((section) => lineCriteria(lines, section));
|
|
406
489
|
return {
|
|
407
490
|
model,
|
|
408
491
|
state,
|
|
409
492
|
questions: Object.fromEntries(
|
|
410
|
-
Object.entries(rules).flatMap(([id, rule]) =>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
? []
|
|
429
|
-
: (["start", "end"] as const).map(
|
|
430
|
-
(edge) =>
|
|
431
|
-
[
|
|
432
|
-
edgeKey(edge, id, index + 1),
|
|
433
|
-
edgeQuestion(rule, edge, String(index + 1), lines, numbers),
|
|
434
|
-
] as const,
|
|
435
|
-
),
|
|
436
|
-
),
|
|
437
|
-
[sufficiencyKey(id), sufficiencyQuestion(rule)] as const,
|
|
438
|
-
]),
|
|
493
|
+
Object.entries(rules).flatMap(([id, rule]) => {
|
|
494
|
+
const fields = ruleFields(rule);
|
|
495
|
+
return [
|
|
496
|
+
...(choosing ? [[id, sectionQuestion(rule, fields, criteria)] as const] : []),
|
|
497
|
+
...withCode.flatMap((options, index) =>
|
|
498
|
+
Record.isEmptyRecord(options)
|
|
499
|
+
? []
|
|
500
|
+
: (["start", "end"] as const).map(
|
|
501
|
+
(edge) =>
|
|
502
|
+
[
|
|
503
|
+
edgeKey(edge, id, index + 1),
|
|
504
|
+
edgeQuestion(fields, edge, String(index + 1), options),
|
|
505
|
+
] as const,
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
[sufficiencyKey(id), sufficiencyQuestionWith(fields, readState)] as const,
|
|
509
|
+
];
|
|
510
|
+
}),
|
|
439
511
|
),
|
|
440
512
|
};
|
|
441
513
|
};
|
|
442
514
|
|
|
443
515
|
/**
|
|
444
516
|
* Whether Jev can take every request a file may need: its sections within
|
|
445
|
-
* one choice, and
|
|
517
|
+
* one choice, and the shared state beside its longest question. Hooks are not predicted.
|
|
446
518
|
*/
|
|
447
|
-
export const fits = (lines: Lines, rules: Rules): boolean => {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
519
|
+
export const fits = (lines: Lines, rules: Rules, readState: ReadState = {}): boolean => {
|
|
520
|
+
const sections = sectionsOf(lines);
|
|
521
|
+
if (sections.length > CHOICE_LIMIT) return false;
|
|
522
|
+
const state = stateOf(lines, readState, sections);
|
|
523
|
+
const room = QUESTION_CONTEXT - WRAPPER - tokensOf(state);
|
|
524
|
+
if (room < 0) return false;
|
|
525
|
+
if (Record.isEmptyRecord(rules)) return true;
|
|
526
|
+
|
|
527
|
+
// These ids can overwrite generated questions; retain the bodies' last-write semantics.
|
|
528
|
+
if (Object.keys(rules).some((id) => /^(appliesTo|excludeIf|start|end|sufficient):/.test(id))) {
|
|
529
|
+
return [
|
|
530
|
+
judgeBody("", lines, rules, [], readState),
|
|
531
|
+
locateBody("", lines, rules, readState),
|
|
532
|
+
].every((body) =>
|
|
533
|
+
Object.values(body.questions).every((question) => tokensOf(question) <= room),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const emptyFields = { rule: "" };
|
|
538
|
+
const fieldBytes = bytesOf(emptyFields);
|
|
539
|
+
const limit = room * 3;
|
|
540
|
+
const criteriaBytes = bytesOf(Record.map(state.code, () => null)) - 2;
|
|
541
|
+
let longest = bytesOf(sufficiencyQuestionWith(emptyFields, readState));
|
|
542
|
+
for (const [index, section] of sections.entries()) {
|
|
543
|
+
const options = lineCriteria(lines, section);
|
|
544
|
+
if (Record.isEmptyRecord(options)) continue;
|
|
545
|
+
// `start` is two bytes longer than `end`; fields and criteria are identical.
|
|
546
|
+
longest = Math.max(
|
|
547
|
+
longest,
|
|
548
|
+
bytesOf(edgeQuestion(emptyFields, "start", String(index + 1), options)),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
for (const rule of Object.values(rules)) {
|
|
552
|
+
// Replace the template's fields in bytes, rounding only the complete question.
|
|
553
|
+
const extra = bytesOf(ruleFields(rule)) - fieldBytes;
|
|
554
|
+
if (longest + extra > limit) return false;
|
|
555
|
+
const judge = judgeQuestionWith(rule, emptyFields);
|
|
556
|
+
if (bytesOf(judge) + extra > limit) return false;
|
|
557
|
+
if (
|
|
558
|
+
sections.length > 1 &&
|
|
559
|
+
bytesOf(sectionQuestion(rule, emptyFields, {})) + criteriaBytes + extra > limit
|
|
560
|
+
) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
for (const kind of MATCHER_KINDS) {
|
|
564
|
+
for (const matcher of rule[kind] ?? []) {
|
|
565
|
+
if (bytesOf(matcherQuestionWith(judge.instructions, kind, matcher)) + extra > limit) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return true;
|
|
455
572
|
};
|
|
456
573
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
574
|
+
/** A request to Jev, its questions typed as `Q`: `Question`, as adhere asks them, or anything an eval arm asks. */
|
|
575
|
+
export type Body<Q = unknown> = SystemOneRequest<Q>;
|
|
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
|
+
});
|
|
462
613
|
|
|
463
614
|
/**
|
|
464
615
|
* A body as requests Jev can take: its questions in order, with a new
|
|
465
616
|
* request whenever the next question would not fit beside the state and
|
|
466
617
|
* the questions before it.
|
|
467
618
|
*/
|
|
468
|
-
export const requestsOf = (body: Body): ReadonlyArray<Body
|
|
619
|
+
export const requestsOf = <Q>(body: Body<Q>): ReadonlyArray<Body<Q>> => {
|
|
469
620
|
const room = REQUEST_CONTEXT - WRAPPER - tokensOf(body.state);
|
|
470
|
-
const requests: Array<{ readonly questions: Record<string,
|
|
621
|
+
const requests: Array<{ readonly questions: Record<string, Q>; tokens: number }> = [];
|
|
471
622
|
for (const [id, question] of Object.entries(body.questions)) {
|
|
472
623
|
const tokens = tokensOf(question);
|
|
473
624
|
const last = requests.at(-1);
|
|
@@ -481,26 +632,30 @@ export const requestsOf = (body: Body): ReadonlyArray<Body> => {
|
|
|
481
632
|
return requests.map(({ questions }) => ({ ...body, questions }));
|
|
482
633
|
};
|
|
483
634
|
|
|
484
|
-
/** Whether a rule reads state of its own: what its `appendState` adds, or what its `reads` names. */
|
|
485
|
-
const readsOwnState = (rule: Rule): boolean =>
|
|
486
|
-
rule.appendState !== undefined || (rule.reads ?? []).length > 0;
|
|
487
|
-
|
|
488
635
|
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* that what is added to the state for it only its own questions read.
|
|
636
|
+
* All rules share one body except those with `appendState`, each in a body
|
|
637
|
+
* of its own so only its questions read what its hook adds to the shared state.
|
|
492
638
|
*/
|
|
493
639
|
export const requestGroups = (rules: Rules): ReadonlyArray<Rules> => {
|
|
494
|
-
const shared = Record.filter(rules, (rule) =>
|
|
640
|
+
const shared = Record.filter(rules, (rule) => rule.appendState === undefined);
|
|
495
641
|
return [
|
|
496
642
|
...(Record.isEmptyRecord(shared) ? [] : [shared]),
|
|
497
|
-
...Object.entries(rules).flatMap(([id, rule]) =>
|
|
643
|
+
...Object.entries(rules).flatMap(([id, rule]) =>
|
|
644
|
+
rule.appendState === undefined ? [] : [{ [id]: rule }],
|
|
645
|
+
),
|
|
498
646
|
];
|
|
499
647
|
};
|
|
500
648
|
|
|
501
649
|
/** The judge requests for `rules` over a file: a body per group, split to fit, as `judge` sends them. */
|
|
502
|
-
const judgeRequestsOf = (
|
|
503
|
-
|
|
650
|
+
const judgeRequestsOf = (
|
|
651
|
+
lines: Lines,
|
|
652
|
+
rules: Rules,
|
|
653
|
+
sampled: ReadonlyArray<RuleId>,
|
|
654
|
+
readState: ReadState,
|
|
655
|
+
) =>
|
|
656
|
+
requestGroups(rules).flatMap((group) =>
|
|
657
|
+
requestsOf(judgeBody("", lines, group, sampled, readState)),
|
|
658
|
+
);
|
|
504
659
|
|
|
505
660
|
/**
|
|
506
661
|
* The requests judging `rules` over a file takes, and about how many input
|
|
@@ -512,8 +667,9 @@ export const judgeLoad = (
|
|
|
512
667
|
lines: Lines,
|
|
513
668
|
rules: Rules,
|
|
514
669
|
sampled: ReadonlyArray<RuleId> = [],
|
|
670
|
+
readState: ReadState = {},
|
|
515
671
|
): { readonly requests: number; readonly tokens: number } => {
|
|
516
|
-
const requests = judgeRequestsOf(lines, rules, sampled);
|
|
672
|
+
const requests = judgeRequestsOf(lines, rules, sampled, readState);
|
|
517
673
|
return {
|
|
518
674
|
requests: requests.length,
|
|
519
675
|
tokens: requests.reduce((sum, request) => sum + tokensOf(request), 0),
|
|
@@ -525,11 +681,13 @@ export const judgeRequests = (
|
|
|
525
681
|
lines: Lines,
|
|
526
682
|
rules: Rules,
|
|
527
683
|
sampled: ReadonlyArray<RuleId> = [],
|
|
528
|
-
|
|
684
|
+
readState: ReadState = {},
|
|
685
|
+
): number => judgeRequestsOf(lines, rules, sampled, readState).length;
|
|
529
686
|
|
|
530
687
|
/** The requests locating `rules` in a file takes, a body per group split to fit, as `locate` sends them. */
|
|
531
|
-
export const locateRequests = (lines: Lines, rules: Rules): number =>
|
|
532
|
-
requestGroups(rules).flatMap((group) => requestsOf(locateBody("", lines, group)))
|
|
688
|
+
export const locateRequests = (lines: Lines, rules: Rules, readState: ReadState = {}): number =>
|
|
689
|
+
requestGroups(rules).flatMap((group) => requestsOf(locateBody("", lines, group, readState)))
|
|
690
|
+
.length;
|
|
533
691
|
|
|
534
692
|
/** A choice keeps one criterion for "none", which leaves this many for partners. */
|
|
535
693
|
const PARTNERS_PER_QUESTION = CHOICE_LIMIT - 1;
|
|
@@ -596,17 +754,15 @@ export const contradictBody = (model: string, rules: ReadonlyArray<ComparedRule>
|
|
|
596
754
|
* it was one the question offered. "none", or a stray answer, names no pair.
|
|
597
755
|
*/
|
|
598
756
|
export const namedPairs = (
|
|
599
|
-
answers: Readonly<Record<string,
|
|
757
|
+
answers: Readonly<Record<string, string>>,
|
|
600
758
|
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
601
759
|
): ReadonlyArray<Pair> =>
|
|
602
|
-
Object.entries(answers).flatMap(([key,
|
|
760
|
+
Object.entries(answers).flatMap(([key, choice]): ReadonlyArray<Pair> => {
|
|
603
761
|
const rule = Number(key.split(":")[0]);
|
|
604
762
|
const partner = Number(choice);
|
|
605
763
|
return partners[rule]?.includes(partner) === true ? [[rule, partner]] : [];
|
|
606
764
|
});
|
|
607
765
|
|
|
608
766
|
/** The probability a contradiction answer gives its pair; 0 when it gives none. */
|
|
609
|
-
export const pairProbability = (
|
|
610
|
-
answers
|
|
611
|
-
pair: Pair,
|
|
612
|
-
): number => answers[pairKey(pair)]?.noul ?? 0;
|
|
767
|
+
export const pairProbability = (answers: Readonly<Record<string, number>>, pair: Pair): number =>
|
|
768
|
+
answers[pairKey(pair)] ?? 0;
|