@drkmttr/adhere 0.14.0 → 0.17.5
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 +294 -146
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/presets/effect/config/tests-provide-values-directly/RULE.md +12 -1
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +14 -2
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +17 -4
- package/presets/effect/services/dependencies-through-layers/RULE.md +16 -3
- package/presets/effect/services/no-mutable-state/RULE.md +7 -1
- 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 +49 -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 +165 -170
- package/src/services/Jev.provider.test.ts +207 -0
- package/src/services/Jev.reads.test.ts +439 -0
- package/src/services/Jev.ts +205 -113
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1040 -0
- package/src/services/SourceReads.ts +437 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +440 -265
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
- package/presets/effect/services/operations-have-no-requirements/RULE.md +0 -50
- package/presets/effect/services/test-layers-are-in-memory/RULE.md +0 -29
package/src/services/Jev.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
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";
|
|
12
|
+
import type { SystemOneRequest } from "#providers/systemOne.ts";
|
|
10
13
|
import { Context, type Effect, Record, Schema } from "effect";
|
|
11
14
|
|
|
12
15
|
export type { Rules };
|
|
@@ -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,18 +137,22 @@ 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
|
-
Math.ceil(encoder.encode(JSON.stringify(value)).length / 3);
|
|
140
|
+
export const tokensOf = (value: unknown): number => Math.ceil(bytesOf(value) / 3);
|
|
135
141
|
|
|
136
142
|
/**
|
|
137
143
|
* 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.
|
|
144
|
+
* numbers from 1, so the locate question can name one. Prepared context is
|
|
145
|
+
* added beside the code, never in its place. On the eval it judged as well as
|
|
146
|
+
* the file with every line numbered, and located as well as a choice among the lines.
|
|
141
147
|
*/
|
|
142
|
-
const stateOf = (
|
|
148
|
+
const stateOf = (
|
|
149
|
+
lines: Lines,
|
|
150
|
+
readState: ReadState = {},
|
|
151
|
+
sections: ReadonlyArray<Range> = sectionsOf(lines),
|
|
152
|
+
): JevState => ({
|
|
153
|
+
...readState,
|
|
143
154
|
code: Object.fromEntries(
|
|
144
|
-
|
|
155
|
+
sections.map((section, index) => [
|
|
145
156
|
String(index + 1),
|
|
146
157
|
lines.slice(section.first - 1, section.last).join("\n"),
|
|
147
158
|
]),
|
|
@@ -198,7 +209,7 @@ const comparedState = (rules: ReadonlyArray<ComparedRule>, mentioned: Iterable<n
|
|
|
198
209
|
* asking whether code "must be written the way `must` shows": the same words,
|
|
199
210
|
* but a pattern to compare rather than code to match.
|
|
200
211
|
*/
|
|
201
|
-
|
|
212
|
+
const judgeQuestionWith = (rule: Rule, fields: ReturnType<typeof ruleFields>) => {
|
|
202
213
|
const { good, bad } = examplesOf(rule);
|
|
203
214
|
const avoided = fieldOf(bad ?? { word: "never", code: "" });
|
|
204
215
|
const example = bad === undefined ? "" : `, for example by doing what \`${avoided}\` shows`;
|
|
@@ -216,7 +227,7 @@ export const judgeQuestion = (rule: Rule) => {
|
|
|
216
227
|
];
|
|
217
228
|
return {
|
|
218
229
|
type: "noul" as const,
|
|
219
|
-
instructions: { question, ...
|
|
230
|
+
instructions: { question, ...fields },
|
|
220
231
|
criteria: {
|
|
221
232
|
true: `${yes}, in code that \`rule\` is about`,
|
|
222
233
|
false: `${no}, or has no code that \`rule\` is about`,
|
|
@@ -224,6 +235,8 @@ export const judgeQuestion = (rule: Rule) => {
|
|
|
224
235
|
};
|
|
225
236
|
};
|
|
226
237
|
|
|
238
|
+
export const judgeQuestion = (rule: Rule) => judgeQuestionWith(rule, ruleFields(rule));
|
|
239
|
+
|
|
227
240
|
/** What the offending section does, for the locate question. */
|
|
228
241
|
const offense = (rule: Rule): string => {
|
|
229
242
|
const { good, bad } = examplesOf(rule);
|
|
@@ -269,8 +282,11 @@ type MatcherKind = keyof MatcherScores;
|
|
|
269
282
|
* `appliesTo`, whether any of that code is as described, and for `excludeIf`,
|
|
270
283
|
* whether all of it is. It carries the rule as the judge question does.
|
|
271
284
|
*/
|
|
272
|
-
|
|
273
|
-
|
|
285
|
+
const matcherQuestionWith = (
|
|
286
|
+
instructions: ReturnType<typeof judgeQuestion>["instructions"],
|
|
287
|
+
kind: MatcherKind,
|
|
288
|
+
matcher: string,
|
|
289
|
+
) => {
|
|
274
290
|
const [quantity, yes, no] =
|
|
275
291
|
kind === "appliesTo" ? ["any", "Some", "None"] : ["all", "All", "Not all"];
|
|
276
292
|
return {
|
|
@@ -287,6 +303,9 @@ export const matcherQuestion = (rule: Rule, kind: MatcherKind, matcher: string)
|
|
|
287
303
|
};
|
|
288
304
|
};
|
|
289
305
|
|
|
306
|
+
export const matcherQuestion = (rule: Rule, kind: MatcherKind, matcher: string) =>
|
|
307
|
+
matcherQuestionWith(judgeQuestion(rule).instructions, kind, matcher);
|
|
308
|
+
|
|
290
309
|
/** The key a rule's matcher question rides under, beside its judge question. */
|
|
291
310
|
export const matcherKey = (kind: MatcherKind, id: RuleId, index: number): string =>
|
|
292
311
|
`${kind}:${id}:${index}`;
|
|
@@ -302,9 +321,9 @@ export const matcherQuestions = (id: RuleId, rule: Rule) =>
|
|
|
302
321
|
),
|
|
303
322
|
);
|
|
304
323
|
|
|
305
|
-
/** The
|
|
306
|
-
export const questionRoom = (lines: Lines): number =>
|
|
307
|
-
QUESTION_CONTEXT - WRAPPER - tokensOf(stateOf(lines));
|
|
324
|
+
/** The room for one question beside the file and its shared prepared context. Hooks are not predicted. */
|
|
325
|
+
export const questionRoom = (lines: Lines, readState: ReadState = {}): number =>
|
|
326
|
+
QUESTION_CONTEXT - WRAPPER - tokensOf(stateOf(lines, readState));
|
|
308
327
|
|
|
309
328
|
/**
|
|
310
329
|
* One question per rule, one per matcher of each rule, and the linter
|
|
@@ -315,9 +334,10 @@ export const judgeBody = (
|
|
|
315
334
|
lines: Lines,
|
|
316
335
|
rules: Rules,
|
|
317
336
|
sampled: ReadonlyArray<RuleId> = [],
|
|
337
|
+
readState: ReadState = {},
|
|
318
338
|
) => ({
|
|
319
339
|
model,
|
|
320
|
-
state: stateOf(lines),
|
|
340
|
+
state: stateOf(lines, readState),
|
|
321
341
|
questions: {
|
|
322
342
|
...Record.map(rules, (rule): ReturnType<typeof judgeQuestion> => judgeQuestion(rule)),
|
|
323
343
|
...Object.fromEntries(
|
|
@@ -340,19 +360,34 @@ export const judgeBody = (
|
|
|
340
360
|
* in a field, ranked real findings worse, and read no easier. Of findings
|
|
341
361
|
* it scored below 0.6, 62% were false, where 22% of all were.
|
|
342
362
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
363
|
+
const sufficiencyQuestionWith = (fields: ReturnType<typeof ruleFields>, readState: ReadState) => {
|
|
364
|
+
const keys = Object.keys(readState).filter((key) => key !== "code");
|
|
365
|
+
const context =
|
|
366
|
+
keys.length === 0 ? undefined : ["`code`", ...keys.map((key) => `\`${key}\``)].join(" and ");
|
|
367
|
+
return {
|
|
368
|
+
type: "noul" as const,
|
|
369
|
+
instructions: {
|
|
370
|
+
question:
|
|
371
|
+
context === undefined
|
|
372
|
+
? "Can you tell whether `code` breaks `rule` from `code` alone, without knowing what other files, libraries, services, or configuration do?"
|
|
373
|
+
: `Can you tell whether \`code\` breaks \`rule\` from ${context}, without knowing what other files, libraries, services, or configuration do beyond what they show?`,
|
|
374
|
+
...fields,
|
|
375
|
+
},
|
|
376
|
+
criteria: {
|
|
377
|
+
true:
|
|
378
|
+
context === undefined
|
|
379
|
+
? "`code` shows everything needed to tell whether it breaks `rule`"
|
|
380
|
+
: `${context} show everything needed to tell whether \`code\` breaks \`rule\``,
|
|
381
|
+
false:
|
|
382
|
+
context === undefined
|
|
383
|
+
? "Whether `code` breaks `rule` depends on something `code` does not show, such as another file, a library, a service, or configuration"
|
|
384
|
+
: `Whether \`code\` breaks \`rule\` depends on something ${context} do not show, such as another file, a library, a service, or configuration`,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const sufficiencyQuestion = (rule: Rule, readState: ReadState = {}) =>
|
|
390
|
+
sufficiencyQuestionWith(ruleFields(rule), readState);
|
|
356
391
|
|
|
357
392
|
/** The key a rule's sufficiency question rides under, beside its locate question. */
|
|
358
393
|
export const sufficiencyKey = (id: RuleId): string => `sufficient:${id}`;
|
|
@@ -367,20 +402,39 @@ export const edgeKey = (edge: "start" | "end", id: RuleId, section: number): str
|
|
|
367
402
|
* line's text, since the state carries no line numbers.
|
|
368
403
|
*/
|
|
369
404
|
const edgeQuestion = (
|
|
370
|
-
|
|
405
|
+
fields: ReturnType<typeof ruleFields>,
|
|
371
406
|
edge: "start" | "end",
|
|
372
407
|
key: string,
|
|
373
|
-
|
|
374
|
-
numbers: ReadonlyArray<number>,
|
|
408
|
+
criteria: Readonly<Record<string, string>>,
|
|
375
409
|
) => ({
|
|
376
410
|
type: "choice" as const,
|
|
377
411
|
instructions: {
|
|
378
412
|
question: `Which line of \`code["${key}"]\` does the code that breaks \`rule\` ${edge} on?`,
|
|
379
|
-
...
|
|
413
|
+
...fields,
|
|
380
414
|
},
|
|
381
|
-
criteria
|
|
382
|
-
|
|
383
|
-
|
|
415
|
+
criteria,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const lineCriteria = (lines: Lines, section: Range): Record<string, string> => {
|
|
419
|
+
const criteria: Record<string, string> = {};
|
|
420
|
+
for (let line = section.first; line <= section.last; line++) {
|
|
421
|
+
const text = (lines[line - 1] ?? "").trim();
|
|
422
|
+
if (text !== "") criteria[String(line)] = text.slice(0, 120);
|
|
423
|
+
}
|
|
424
|
+
return criteria;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const sectionQuestion = (
|
|
428
|
+
rule: Rule,
|
|
429
|
+
fields: ReturnType<typeof ruleFields>,
|
|
430
|
+
criteria: Readonly<Record<string, null>>,
|
|
431
|
+
) => ({
|
|
432
|
+
type: "choice" as const,
|
|
433
|
+
instructions: {
|
|
434
|
+
question: `Which entry of \`code\` most clearly ${offense(rule)}?`,
|
|
435
|
+
...fields,
|
|
436
|
+
},
|
|
437
|
+
criteria,
|
|
384
438
|
});
|
|
385
439
|
|
|
386
440
|
/**
|
|
@@ -394,80 +448,113 @@ const edgeQuestion = (
|
|
|
394
448
|
* held a violation for 94% of real findings, and 98% where the section was
|
|
395
449
|
* right, and were most often one line.
|
|
396
450
|
*/
|
|
397
|
-
export const locateBody = (
|
|
398
|
-
|
|
451
|
+
export const locateBody = (
|
|
452
|
+
model: string,
|
|
453
|
+
lines: Lines,
|
|
454
|
+
rules: Rules,
|
|
455
|
+
readState: ReadState = {},
|
|
456
|
+
) => {
|
|
457
|
+
const sections = sectionsOf(lines);
|
|
458
|
+
const state = stateOf(lines, readState, sections);
|
|
399
459
|
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
|
-
);
|
|
460
|
+
const choosing = sections.length > 1;
|
|
461
|
+
const withCode = sections.map((section) => lineCriteria(lines, section));
|
|
406
462
|
return {
|
|
407
463
|
model,
|
|
408
464
|
state,
|
|
409
465
|
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
|
-
]),
|
|
466
|
+
Object.entries(rules).flatMap(([id, rule]) => {
|
|
467
|
+
const fields = ruleFields(rule);
|
|
468
|
+
return [
|
|
469
|
+
...(choosing ? [[id, sectionQuestion(rule, fields, criteria)] as const] : []),
|
|
470
|
+
...withCode.flatMap((options, index) =>
|
|
471
|
+
Record.isEmptyRecord(options)
|
|
472
|
+
? []
|
|
473
|
+
: (["start", "end"] as const).map(
|
|
474
|
+
(edge) =>
|
|
475
|
+
[
|
|
476
|
+
edgeKey(edge, id, index + 1),
|
|
477
|
+
edgeQuestion(fields, edge, String(index + 1), options),
|
|
478
|
+
] as const,
|
|
479
|
+
),
|
|
480
|
+
),
|
|
481
|
+
[sufficiencyKey(id), sufficiencyQuestionWith(fields, readState)] as const,
|
|
482
|
+
];
|
|
483
|
+
}),
|
|
439
484
|
),
|
|
440
485
|
};
|
|
441
486
|
};
|
|
442
487
|
|
|
443
488
|
/**
|
|
444
489
|
* Whether Jev can take every request a file may need: its sections within
|
|
445
|
-
* one choice, and
|
|
490
|
+
* one choice, and the shared state beside its longest question. Hooks are not predicted.
|
|
446
491
|
*/
|
|
447
|
-
export const fits = (lines: Lines, rules: Rules): boolean => {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
492
|
+
export const fits = (lines: Lines, rules: Rules, readState: ReadState = {}): boolean => {
|
|
493
|
+
const sections = sectionsOf(lines);
|
|
494
|
+
if (sections.length > CHOICE_LIMIT) return false;
|
|
495
|
+
const state = stateOf(lines, readState, sections);
|
|
496
|
+
const room = QUESTION_CONTEXT - WRAPPER - tokensOf(state);
|
|
497
|
+
if (room < 0) return false;
|
|
498
|
+
if (Record.isEmptyRecord(rules)) return true;
|
|
499
|
+
|
|
500
|
+
// These ids can overwrite generated questions; retain the bodies' last-write semantics.
|
|
501
|
+
if (Object.keys(rules).some((id) => /^(appliesTo|excludeIf|start|end|sufficient):/.test(id))) {
|
|
502
|
+
return [
|
|
503
|
+
judgeBody("", lines, rules, [], readState),
|
|
504
|
+
locateBody("", lines, rules, readState),
|
|
505
|
+
].every((body) =>
|
|
506
|
+
Object.values(body.questions).every((question) => tokensOf(question) <= room),
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const emptyFields = { rule: "" };
|
|
511
|
+
const fieldBytes = bytesOf(emptyFields);
|
|
512
|
+
const limit = room * 3;
|
|
513
|
+
const criteriaBytes = bytesOf(Record.map(state.code, () => null)) - 2;
|
|
514
|
+
let longest = bytesOf(sufficiencyQuestionWith(emptyFields, readState));
|
|
515
|
+
for (const [index, section] of sections.entries()) {
|
|
516
|
+
const options = lineCriteria(lines, section);
|
|
517
|
+
if (Record.isEmptyRecord(options)) continue;
|
|
518
|
+
// `start` is two bytes longer than `end`; fields and criteria are identical.
|
|
519
|
+
longest = Math.max(
|
|
520
|
+
longest,
|
|
521
|
+
bytesOf(edgeQuestion(emptyFields, "start", String(index + 1), options)),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
for (const rule of Object.values(rules)) {
|
|
525
|
+
// Replace the template's fields in bytes, rounding only the complete question.
|
|
526
|
+
const extra = bytesOf(ruleFields(rule)) - fieldBytes;
|
|
527
|
+
if (longest + extra > limit) return false;
|
|
528
|
+
const judge = judgeQuestionWith(rule, emptyFields);
|
|
529
|
+
if (bytesOf(judge) + extra > limit) return false;
|
|
530
|
+
if (
|
|
531
|
+
sections.length > 1 &&
|
|
532
|
+
bytesOf(sectionQuestion(rule, emptyFields, {})) + criteriaBytes + extra > limit
|
|
533
|
+
) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
for (const kind of MATCHER_KINDS) {
|
|
537
|
+
for (const matcher of rule[kind] ?? []) {
|
|
538
|
+
if (bytesOf(matcherQuestionWith(judge.instructions, kind, matcher)) + extra > limit) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
455
545
|
};
|
|
456
546
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
readonly state: unknown;
|
|
460
|
-
readonly questions: Readonly<Record<string, unknown>>;
|
|
461
|
-
}
|
|
547
|
+
/** A request to Jev, its questions typed as `Q`: `Question`, as adhere asks them, or anything an eval arm asks. */
|
|
548
|
+
export type Body<Q = unknown> = SystemOneRequest<Q>;
|
|
462
549
|
|
|
463
550
|
/**
|
|
464
551
|
* A body as requests Jev can take: its questions in order, with a new
|
|
465
552
|
* request whenever the next question would not fit beside the state and
|
|
466
553
|
* the questions before it.
|
|
467
554
|
*/
|
|
468
|
-
export const requestsOf = (body: Body): ReadonlyArray<Body
|
|
555
|
+
export const requestsOf = <Q>(body: Body<Q>): ReadonlyArray<Body<Q>> => {
|
|
469
556
|
const room = REQUEST_CONTEXT - WRAPPER - tokensOf(body.state);
|
|
470
|
-
const requests: Array<{ readonly questions: Record<string,
|
|
557
|
+
const requests: Array<{ readonly questions: Record<string, Q>; tokens: number }> = [];
|
|
471
558
|
for (const [id, question] of Object.entries(body.questions)) {
|
|
472
559
|
const tokens = tokensOf(question);
|
|
473
560
|
const last = requests.at(-1);
|
|
@@ -481,26 +568,30 @@ export const requestsOf = (body: Body): ReadonlyArray<Body> => {
|
|
|
481
568
|
return requests.map(({ questions }) => ({ ...body, questions }));
|
|
482
569
|
};
|
|
483
570
|
|
|
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
571
|
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* that what is added to the state for it only its own questions read.
|
|
572
|
+
* All rules share one body except those with `appendState`, each in a body
|
|
573
|
+
* of its own so only its questions read what its hook adds to the shared state.
|
|
492
574
|
*/
|
|
493
575
|
export const requestGroups = (rules: Rules): ReadonlyArray<Rules> => {
|
|
494
|
-
const shared = Record.filter(rules, (rule) =>
|
|
576
|
+
const shared = Record.filter(rules, (rule) => rule.appendState === undefined);
|
|
495
577
|
return [
|
|
496
578
|
...(Record.isEmptyRecord(shared) ? [] : [shared]),
|
|
497
|
-
...Object.entries(rules).flatMap(([id, rule]) =>
|
|
579
|
+
...Object.entries(rules).flatMap(([id, rule]) =>
|
|
580
|
+
rule.appendState === undefined ? [] : [{ [id]: rule }],
|
|
581
|
+
),
|
|
498
582
|
];
|
|
499
583
|
};
|
|
500
584
|
|
|
501
585
|
/** The judge requests for `rules` over a file: a body per group, split to fit, as `judge` sends them. */
|
|
502
|
-
const judgeRequestsOf = (
|
|
503
|
-
|
|
586
|
+
const judgeRequestsOf = (
|
|
587
|
+
lines: Lines,
|
|
588
|
+
rules: Rules,
|
|
589
|
+
sampled: ReadonlyArray<RuleId>,
|
|
590
|
+
readState: ReadState,
|
|
591
|
+
) =>
|
|
592
|
+
requestGroups(rules).flatMap((group) =>
|
|
593
|
+
requestsOf(judgeBody("", lines, group, sampled, readState)),
|
|
594
|
+
);
|
|
504
595
|
|
|
505
596
|
/**
|
|
506
597
|
* The requests judging `rules` over a file takes, and about how many input
|
|
@@ -512,8 +603,9 @@ export const judgeLoad = (
|
|
|
512
603
|
lines: Lines,
|
|
513
604
|
rules: Rules,
|
|
514
605
|
sampled: ReadonlyArray<RuleId> = [],
|
|
606
|
+
readState: ReadState = {},
|
|
515
607
|
): { readonly requests: number; readonly tokens: number } => {
|
|
516
|
-
const requests = judgeRequestsOf(lines, rules, sampled);
|
|
608
|
+
const requests = judgeRequestsOf(lines, rules, sampled, readState);
|
|
517
609
|
return {
|
|
518
610
|
requests: requests.length,
|
|
519
611
|
tokens: requests.reduce((sum, request) => sum + tokensOf(request), 0),
|
|
@@ -525,11 +617,13 @@ export const judgeRequests = (
|
|
|
525
617
|
lines: Lines,
|
|
526
618
|
rules: Rules,
|
|
527
619
|
sampled: ReadonlyArray<RuleId> = [],
|
|
528
|
-
|
|
620
|
+
readState: ReadState = {},
|
|
621
|
+
): number => judgeRequestsOf(lines, rules, sampled, readState).length;
|
|
529
622
|
|
|
530
623
|
/** 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)))
|
|
624
|
+
export const locateRequests = (lines: Lines, rules: Rules, readState: ReadState = {}): number =>
|
|
625
|
+
requestGroups(rules).flatMap((group) => requestsOf(locateBody("", lines, group, readState)))
|
|
626
|
+
.length;
|
|
533
627
|
|
|
534
628
|
/** A choice keeps one criterion for "none", which leaves this many for partners. */
|
|
535
629
|
const PARTNERS_PER_QUESTION = CHOICE_LIMIT - 1;
|
|
@@ -596,17 +690,15 @@ export const contradictBody = (model: string, rules: ReadonlyArray<ComparedRule>
|
|
|
596
690
|
* it was one the question offered. "none", or a stray answer, names no pair.
|
|
597
691
|
*/
|
|
598
692
|
export const namedPairs = (
|
|
599
|
-
answers: Readonly<Record<string,
|
|
693
|
+
answers: Readonly<Record<string, string>>,
|
|
600
694
|
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
601
695
|
): ReadonlyArray<Pair> =>
|
|
602
|
-
Object.entries(answers).flatMap(([key,
|
|
696
|
+
Object.entries(answers).flatMap(([key, choice]): ReadonlyArray<Pair> => {
|
|
603
697
|
const rule = Number(key.split(":")[0]);
|
|
604
698
|
const partner = Number(choice);
|
|
605
699
|
return partners[rule]?.includes(partner) === true ? [[rule, partner]] : [];
|
|
606
700
|
});
|
|
607
701
|
|
|
608
702
|
/** 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;
|
|
703
|
+
export const pairProbability = (answers: Readonly<Record<string, number>>, pair: Pair): number =>
|
|
704
|
+
answers[pairKey(pair)] ?? 0;
|