@drkmttr/adhere 0.4.1 → 0.6.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 +55 -21
- package/package.json +6 -6
- package/presets/effect/basics/fn-for-named-effects.md +1 -1
- package/presets/effect/basics/gen-for-sequencing.md +1 -1
- package/presets/effect/basics/instrument-with-pipe.md +1 -1
- package/presets/effect/config/business-logic-depends-on-config-service.md +1 -1
- package/presets/effect/config/secrets-are-redacted.md +1 -1
- package/presets/effect/config/validate-with-schema.md +1 -1
- package/presets/effect/data/brand-meaningful-primitives.md +1 -1
- package/presets/effect/data/decode-json-with-schema.md +1 -1
- package/presets/effect/data/records-are-schema-classes.md +1 -1
- package/presets/effect/data/variants-are-tagged-unions.md +1 -1
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +1 -1
- package/presets/effect/errors/defects-for-bugs.md +1 -1
- package/presets/effect/errors/domain-errors-are-tagged.md +1 -1
- package/presets/effect/errors/wrap-external-errors.md +1 -1
- package/presets/effect/platform/os-access-through-platform-services.md +1 -1
- package/presets/effect/services/fresh-layer-per-test.md +1 -1
- package/presets/effect/services/memoize-parameterized-layers.md +1 -1
- package/presets/effect/services/methods-have-no-requirements.md +1 -1
- package/presets/effect/services/provide-at-entry.md +1 -1
- package/presets/effect/services/test-layers-are-in-memory.md +1 -1
- package/presets/effect/testing/test-clock-for-time.md +1 -1
- package/skills/adhere/SKILL.md +1 -1
- package/src/cli.ts +86 -8
- package/src/contradictions.ts +51 -57
- package/src/services/Jev.http.ts +80 -11
- package/src/services/Jev.ts +244 -36
- package/src/workflows/audit.ts +195 -79
- package/src/workflows/format.ts +42 -1
package/src/services/Jev.http.ts
CHANGED
|
@@ -2,16 +2,24 @@ import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
|
2
2
|
import { Credentials } from "#services/Credentials.ts";
|
|
3
3
|
import {
|
|
4
4
|
blockBody,
|
|
5
|
+
type Body,
|
|
6
|
+
type ComparedRule,
|
|
7
|
+
conflictBody,
|
|
8
|
+
contradictBody,
|
|
5
9
|
Jev,
|
|
6
10
|
JevUnavailable,
|
|
7
11
|
judgeBody,
|
|
8
12
|
type Lines,
|
|
9
13
|
locateBody,
|
|
14
|
+
namedPairs,
|
|
10
15
|
needsBlocks,
|
|
16
|
+
type Pair,
|
|
17
|
+
pairProbability,
|
|
18
|
+
requestsOf,
|
|
11
19
|
type Rules,
|
|
12
20
|
} from "#services/Jev.ts";
|
|
13
|
-
import { Effect, Layer, Record, Schedule, Schema } from "effect";
|
|
14
|
-
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
|
|
21
|
+
import { type Cause, Effect, Layer, Record, Schedule, Schema } from "effect";
|
|
22
|
+
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http";
|
|
15
23
|
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
16
24
|
|
|
17
25
|
const SYSTEM_ONE = "https://api.typesafe.ai/v1/systemone";
|
|
@@ -25,6 +33,27 @@ const ChoiceAnswers = Schema.Struct({
|
|
|
25
33
|
|
|
26
34
|
const refused = (message: string) => JevUnavailable.make({ message });
|
|
27
35
|
|
|
36
|
+
/** Jev's error body, on one line and cut short, or nothing when it sent none. */
|
|
37
|
+
const detailOf = (body: string): string => {
|
|
38
|
+
const text = body.trim().replace(/\s+/g, " ");
|
|
39
|
+
return text.length === 0 ? "" : `: ${text.slice(0, 300)}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A failed request as a refusal: for an answer outside 2xx, its status and
|
|
44
|
+
* what Jev said, since the status alone does not say why; otherwise the cause.
|
|
45
|
+
*/
|
|
46
|
+
const refusalOf = (problem: HttpClientError.HttpClientError | Cause.TimeoutError) => {
|
|
47
|
+
if (!HttpClientError.isHttpClientError(problem) || problem.reason._tag !== "StatusCodeError") {
|
|
48
|
+
return Effect.succeed(refused(`System One request failed: ${problem.message}`));
|
|
49
|
+
}
|
|
50
|
+
const { response } = problem.reason;
|
|
51
|
+
return Effect.map(
|
|
52
|
+
Effect.orElseSucceed(response.text, () => ""),
|
|
53
|
+
(body) => refused(`Jev answered HTTP ${response.status}${detailOf(body)}`),
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
28
57
|
export const JevLive = Layer.effect(Jev)(
|
|
29
58
|
Effect.gen(function* () {
|
|
30
59
|
const config = yield* AdhereConfig;
|
|
@@ -38,7 +67,7 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
38
67
|
}),
|
|
39
68
|
);
|
|
40
69
|
|
|
41
|
-
const ask = <A>(body:
|
|
70
|
+
const ask = <A>(body: Body, Answers: Schema.Codec<A, unknown, never, never>) =>
|
|
42
71
|
Effect.gen(function* () {
|
|
43
72
|
// Read here, not in the layer: a run with nothing pending needs no key.
|
|
44
73
|
const apiKey = yield* credentials.apiKey.pipe(
|
|
@@ -50,9 +79,7 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
50
79
|
).pipe(Effect.orDie);
|
|
51
80
|
const response = yield* client
|
|
52
81
|
.execute(HttpClientRequest.bearerToken(request, apiKey))
|
|
53
|
-
.pipe(
|
|
54
|
-
Effect.mapError((problem) => refused(`System One request failed: ${problem.message}`)),
|
|
55
|
-
);
|
|
82
|
+
.pipe(Effect.catch((problem) => Effect.flatMap(refusalOf(problem), Effect.fail)));
|
|
56
83
|
return yield* HttpClientResponse.schemaBodyJson(Answers)(response).pipe(
|
|
57
84
|
Effect.mapError((problem) =>
|
|
58
85
|
refused(`System One response did not decode: ${problem.message}`),
|
|
@@ -60,8 +87,27 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
60
87
|
);
|
|
61
88
|
});
|
|
62
89
|
|
|
90
|
+
/** Every answer to a body, over as many requests as Jev's context needs. */
|
|
91
|
+
const answersTo = <A>(
|
|
92
|
+
body: Body,
|
|
93
|
+
Answers: Schema.Codec<
|
|
94
|
+
{ readonly answers: Readonly<Record<string, A>> },
|
|
95
|
+
unknown,
|
|
96
|
+
never,
|
|
97
|
+
never
|
|
98
|
+
>,
|
|
99
|
+
) =>
|
|
100
|
+
Effect.map(
|
|
101
|
+
Effect.forEach(requestsOf(body), (request) => ask(request, Answers)),
|
|
102
|
+
(responses) =>
|
|
103
|
+
responses.reduce<Record<string, A>>(
|
|
104
|
+
(answers, response) => ({ ...answers, ...response.answers }),
|
|
105
|
+
{},
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
63
109
|
const judge = Effect.fn("Jev.judge")(function* (lines: Lines, rules: Rules) {
|
|
64
|
-
const
|
|
110
|
+
const answers = yield* answersTo(judgeBody(config.model, lines, rules), NoulAnswers);
|
|
65
111
|
return Record.map(answers, (answer) => answer.noul);
|
|
66
112
|
});
|
|
67
113
|
|
|
@@ -70,12 +116,35 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
70
116
|
|
|
71
117
|
const locate = Effect.fn("Jev.locate")(function* (lines: Lines, rules: Rules) {
|
|
72
118
|
const blocks = needsBlocks(lines)
|
|
73
|
-
? chosen(
|
|
119
|
+
? chosen(yield* answersTo(blockBody(config.model, lines, rules), ChoiceAnswers))
|
|
74
120
|
: undefined;
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
return chosen(
|
|
122
|
+
yield* answersTo(locateBody(config.model, lines, rules, blocks), ChoiceAnswers),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const conflicts = Effect.fn("Jev.conflicts")(function* (
|
|
127
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
128
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
129
|
+
) {
|
|
130
|
+
const answers = yield* answersTo(conflictBody(config.model, rules, partners), ChoiceAnswers);
|
|
131
|
+
return namedPairs(answers, partners);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const contradicts = Effect.fn("Jev.contradicts")(function* (
|
|
135
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
136
|
+
pairs: ReadonlyArray<Pair>,
|
|
137
|
+
) {
|
|
138
|
+
return yield* Effect.forEach(
|
|
139
|
+
pairs,
|
|
140
|
+
(pair) =>
|
|
141
|
+
Effect.map(ask(contradictBody(config.model, rules, pair), NoulAnswers), ({ answers }) =>
|
|
142
|
+
pairProbability(answers, pair),
|
|
143
|
+
),
|
|
144
|
+
{ concurrency: 8 },
|
|
145
|
+
);
|
|
77
146
|
});
|
|
78
147
|
|
|
79
|
-
return Jev.of({ judge, locate });
|
|
148
|
+
return Jev.of({ judge, locate, conflicts, contradicts });
|
|
80
149
|
}),
|
|
81
150
|
);
|
package/src/services/Jev.ts
CHANGED
|
@@ -19,13 +19,58 @@ export class Jev extends Context.Service<
|
|
|
19
19
|
lines: Lines,
|
|
20
20
|
rules: Rules,
|
|
21
21
|
) => Effect.Effect<Record<RuleId, number>, JevUnavailable>;
|
|
22
|
+
/**
|
|
23
|
+
* Per rule, the partner Jev names as impossible to follow in the same code,
|
|
24
|
+
* as `[rule, partner]` indexes into `rules`. A rule Jev names none for is
|
|
25
|
+
* left out.
|
|
26
|
+
*/
|
|
27
|
+
readonly conflicts: (
|
|
28
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
29
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
30
|
+
) => Effect.Effect<ReadonlyArray<Pair>, JevUnavailable>;
|
|
31
|
+
/**
|
|
32
|
+
* Per pair, in order, Jev's probability that no code can follow both rules,
|
|
33
|
+
* each pair asked on its own.
|
|
34
|
+
*/
|
|
35
|
+
readonly contradicts: (
|
|
36
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
37
|
+
pairs: ReadonlyArray<Pair>,
|
|
38
|
+
) => Effect.Effect<ReadonlyArray<number>, JevUnavailable>;
|
|
22
39
|
}
|
|
23
40
|
>()("@drkmttr/adhere/services/Jev") {}
|
|
24
41
|
|
|
42
|
+
/** A rule compared with other rules rather than with a file. */
|
|
43
|
+
export interface ComparedRule {
|
|
44
|
+
readonly id: RuleId;
|
|
45
|
+
readonly rule: Rule;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Two rules, as indexes into the rules being compared. */
|
|
49
|
+
export type Pair = readonly [number, number];
|
|
50
|
+
|
|
25
51
|
/** A `choice` question accepts at most this many criteria. */
|
|
26
52
|
const CHOICE_LIMIT = 255;
|
|
27
53
|
const LINES_PER_BLOCK = 20;
|
|
28
|
-
|
|
54
|
+
const MAX_LINES = CHOICE_LIMIT * LINES_PER_BLOCK;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Jev 1.13's context, from its model card: the state with its longest
|
|
58
|
+
* question, and the state with every question in the request.
|
|
59
|
+
*/
|
|
60
|
+
const QUESTION_CONTEXT = 32_000;
|
|
61
|
+
const REQUEST_CONTEXT = 64_000;
|
|
62
|
+
/** Room for what Jev wraps around a request, which `tokensOf` does not see. */
|
|
63
|
+
const WRAPPER = 1_000;
|
|
64
|
+
|
|
65
|
+
const encoder = new TextEncoder();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* An estimate, meant to run high, of the tokens Jev reads for a value: one
|
|
69
|
+
* per three bytes of its JSON. Code runs a little over three bytes a token,
|
|
70
|
+
* prose about four.
|
|
71
|
+
*/
|
|
72
|
+
export const tokensOf = (value: unknown): number =>
|
|
73
|
+
Math.ceil(encoder.encode(JSON.stringify(value)).length / 3);
|
|
29
74
|
|
|
30
75
|
export const snippetOf = (line: string): string => line.trim().slice(0, 120);
|
|
31
76
|
|
|
@@ -34,37 +79,61 @@ const isBlank = (line: string): boolean => line.trim().length === 0;
|
|
|
34
79
|
export const needsBlocks = (lines: Lines): boolean =>
|
|
35
80
|
lines.filter((line) => !isBlank(line)).length > CHOICE_LIMIT;
|
|
36
81
|
|
|
37
|
-
|
|
82
|
+
/** The file, numbered so a line question can name a line. The only thing the questions share. */
|
|
83
|
+
const stateOf = (lines: Lines) => ({
|
|
38
84
|
code: lines.map((line, index) => `${index + 1} | ${line}`).join("\n"),
|
|
39
|
-
rules: Record.map(rules, ({ description, reference, avoid }) => ({
|
|
40
|
-
description,
|
|
41
|
-
...(reference === undefined ? {} : { reference }),
|
|
42
|
-
...(avoid === undefined ? {} : { avoid }),
|
|
43
|
-
})),
|
|
44
85
|
});
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
|
|
87
|
+
/** One side of a noul: what it means, with the rule's code for that side as an example. */
|
|
88
|
+
const sideOf = (what: string, example: string | undefined) =>
|
|
89
|
+
example === undefined ? { what } : { what, examples: [example] };
|
|
90
|
+
|
|
91
|
+
/** A rule's text as a comparison of rules reads it. Code a rule lacks is left out, not sent empty. */
|
|
92
|
+
const ruleState = ({ description, reference, avoid }: Rule) => ({
|
|
93
|
+
description,
|
|
94
|
+
...(reference === undefined ? {} : { reference }),
|
|
95
|
+
...(avoid === undefined ? {} : { avoid }),
|
|
96
|
+
});
|
|
48
97
|
|
|
49
98
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* whether the file contains the code to avoid.
|
|
99
|
+
* The compared rules a question mentions, keyed by index: ids alone would
|
|
100
|
+
* collide, since a nested rule can share its id with the rule it shadows.
|
|
53
101
|
*/
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
const comparedState = (rules: ReadonlyArray<ComparedRule>, mentioned: Iterable<number>) => {
|
|
103
|
+
const state: Record<string, { readonly id: RuleId } & ReturnType<typeof ruleState>> = {};
|
|
104
|
+
for (const index of [...new Set(mentioned)].sort((a, b) => a - b)) {
|
|
105
|
+
const compared = rules[index];
|
|
106
|
+
if (compared !== undefined) {
|
|
107
|
+
state[String(index)] = { id: compared.id, ...ruleState(compared.rule) };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { rules: state };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether the file breaks the rule. The rule rides in the question, not the
|
|
115
|
+
* state, so each question reads its own rule and no other. The criteria draw
|
|
116
|
+
* the line: code to avoid is a yes, and the reference is a no, as is a file
|
|
117
|
+
* with nothing the rule covers.
|
|
118
|
+
*/
|
|
119
|
+
export const judgeQuestion = (rule: Rule) => ({
|
|
120
|
+
type: "noul" as const,
|
|
121
|
+
instructions: { question: "Does `code` break `rule`?", rule: rule.description },
|
|
122
|
+
criteria: {
|
|
123
|
+
true: sideOf("Part of `code` breaks `rule`", rule.avoid),
|
|
124
|
+
false: sideOf(
|
|
125
|
+
"Every part of `code` that `rule` covers follows it, or `rule` covers no part of `code`",
|
|
126
|
+
rule.reference,
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** The rule as a line question carries it: the description, and its code on either side. */
|
|
132
|
+
const ruleFields = (rule: Rule) => ({
|
|
133
|
+
rule: rule.description,
|
|
134
|
+
...(rule.reference === undefined ? {} : { follows_rule: rule.reference }),
|
|
135
|
+
...(rule.avoid === undefined ? {} : { breaks_rule: rule.avoid }),
|
|
136
|
+
});
|
|
68
137
|
|
|
69
138
|
const lineCriteria = (lines: Lines, from: number, to: number): Record<string, string> => {
|
|
70
139
|
const criteria: Record<string, string> = {};
|
|
@@ -77,11 +146,8 @@ const lineCriteria = (lines: Lines, from: number, to: number): Record<string, st
|
|
|
77
146
|
|
|
78
147
|
export const judgeBody = (model: string, lines: Lines, rules: Rules) => ({
|
|
79
148
|
model,
|
|
80
|
-
state: stateOf(lines
|
|
81
|
-
questions: Record.map(rules, (rule
|
|
82
|
-
type: "noul" as const,
|
|
83
|
-
instructions: judgeInstructions(id, rule),
|
|
84
|
-
})),
|
|
149
|
+
state: stateOf(lines),
|
|
150
|
+
questions: Record.map(rules, (rule) => judgeQuestion(rule)),
|
|
85
151
|
});
|
|
86
152
|
|
|
87
153
|
export const locateBody = (
|
|
@@ -91,12 +157,15 @@ export const locateBody = (
|
|
|
91
157
|
blocks?: Readonly<Record<RuleId, number>>,
|
|
92
158
|
) => ({
|
|
93
159
|
model,
|
|
94
|
-
state: stateOf(lines
|
|
160
|
+
state: stateOf(lines),
|
|
95
161
|
questions: Record.map(rules, (rule, id) => {
|
|
96
162
|
const block = blocks?.[id];
|
|
97
163
|
return {
|
|
98
164
|
type: "choice" as const,
|
|
99
|
-
instructions:
|
|
165
|
+
instructions: {
|
|
166
|
+
question: "Which line of `code` most clearly breaks `rule`?",
|
|
167
|
+
...ruleFields(rule),
|
|
168
|
+
},
|
|
100
169
|
criteria:
|
|
101
170
|
block === undefined
|
|
102
171
|
? lineCriteria(lines, 1, lines.length)
|
|
@@ -115,11 +184,150 @@ export const blockBody = (model: string, lines: Lines, rules: Rules) => {
|
|
|
115
184
|
}
|
|
116
185
|
return {
|
|
117
186
|
model,
|
|
118
|
-
state: stateOf(lines
|
|
119
|
-
questions: Record.map(rules, (rule
|
|
187
|
+
state: stateOf(lines),
|
|
188
|
+
questions: Record.map(rules, (rule) => ({
|
|
120
189
|
type: "choice" as const,
|
|
121
|
-
instructions:
|
|
190
|
+
instructions: {
|
|
191
|
+
question: "Which block of `code` contains the line that most clearly breaks `rule`?",
|
|
192
|
+
...ruleFields(rule),
|
|
193
|
+
},
|
|
122
194
|
criteria,
|
|
123
195
|
})),
|
|
124
196
|
};
|
|
125
197
|
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether Jev can take every request a file may need: its lines within two
|
|
201
|
+
* line choices, and its code beside the longest question any rule asks.
|
|
202
|
+
*/
|
|
203
|
+
export const fits = (lines: Lines, rules: Rules): boolean => {
|
|
204
|
+
if (lines.length > MAX_LINES) return false;
|
|
205
|
+
const located = needsBlocks(lines) ? blockBody("", lines, rules) : locateBody("", lines, rules);
|
|
206
|
+
const questions = [
|
|
207
|
+
...Object.values(judgeBody("", lines, rules).questions),
|
|
208
|
+
...Object.values(located.questions),
|
|
209
|
+
];
|
|
210
|
+
const longest = Math.max(0, ...questions.map(tokensOf));
|
|
211
|
+
return tokensOf(stateOf(lines)) + longest <= QUESTION_CONTEXT - WRAPPER;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export interface Body {
|
|
215
|
+
readonly model: string;
|
|
216
|
+
readonly state: unknown;
|
|
217
|
+
readonly questions: Readonly<Record<string, unknown>>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A body as requests Jev can take: its questions in order, with a new
|
|
222
|
+
* request whenever the next question would not fit beside the state and
|
|
223
|
+
* the questions before it.
|
|
224
|
+
*/
|
|
225
|
+
export const requestsOf = (body: Body): ReadonlyArray<Body> => {
|
|
226
|
+
const room = REQUEST_CONTEXT - WRAPPER - tokensOf(body.state);
|
|
227
|
+
const requests: Array<{ readonly questions: Record<string, unknown>; tokens: number }> = [];
|
|
228
|
+
for (const [id, question] of Object.entries(body.questions)) {
|
|
229
|
+
const tokens = tokensOf(question);
|
|
230
|
+
const last = requests.at(-1);
|
|
231
|
+
if (last !== undefined && last.tokens + tokens <= room) {
|
|
232
|
+
last.questions[id] = question;
|
|
233
|
+
last.tokens += tokens;
|
|
234
|
+
} else {
|
|
235
|
+
requests.push({ questions: { [id]: question }, tokens });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return requests.map(({ questions }) => ({ ...body, questions }));
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** The requests judging `rules` over a file takes: the judge body, split to fit, as `judge` sends it. */
|
|
242
|
+
export const judgeRequests = (lines: Lines, rules: Rules): number =>
|
|
243
|
+
requestsOf(judgeBody("", lines, rules)).length;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The requests locating `rules` in a file takes, split to fit as `locate`
|
|
247
|
+
* sends them: a block choice first for a long file, then a line choice. For a
|
|
248
|
+
* long file the line choice is counted before a block is chosen, so it can
|
|
249
|
+
* run high.
|
|
250
|
+
*/
|
|
251
|
+
export const locateRequests = (lines: Lines, rules: Rules): number =>
|
|
252
|
+
(needsBlocks(lines) ? requestsOf(blockBody("", lines, rules)).length : 0) +
|
|
253
|
+
requestsOf(locateBody("", lines, rules)).length;
|
|
254
|
+
|
|
255
|
+
/** A choice keeps one criterion for "none", which leaves this many for partners. */
|
|
256
|
+
const PARTNERS_PER_QUESTION = CHOICE_LIMIT - 1;
|
|
257
|
+
|
|
258
|
+
interface ChoiceQuestion {
|
|
259
|
+
readonly type: "choice";
|
|
260
|
+
readonly instructions: string;
|
|
261
|
+
readonly criteria: Record<string, string>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Per rule, one `choice` among the rules that apply to the same files: which
|
|
266
|
+
* of them cannot be followed in the same code, or none. The questions grow
|
|
267
|
+
* with the rules, not with every pair of them. A rule with more partners than
|
|
268
|
+
* a choice holds gets one question per chunk, keyed `rule:chunk`.
|
|
269
|
+
*/
|
|
270
|
+
export const conflictBody = (
|
|
271
|
+
model: string,
|
|
272
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
273
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
274
|
+
) => {
|
|
275
|
+
const questions: Record<string, ChoiceQuestion> = {};
|
|
276
|
+
const mentioned: Array<number> = [];
|
|
277
|
+
partners.forEach((some, index) => {
|
|
278
|
+
for (let chunk = 0; chunk * PARTNERS_PER_QUESTION < some.length; chunk++) {
|
|
279
|
+
const from = chunk * PARTNERS_PER_QUESTION;
|
|
280
|
+
const offered = some.slice(from, from + PARTNERS_PER_QUESTION);
|
|
281
|
+
const criteria: Record<string, string> = { none: "None of them" };
|
|
282
|
+
for (const partner of offered) {
|
|
283
|
+
criteria[String(partner)] = rules[partner]?.rule.description ?? "";
|
|
284
|
+
}
|
|
285
|
+
mentioned.push(index, ...offered);
|
|
286
|
+
questions[`${index}:${chunk}`] = {
|
|
287
|
+
type: "choice",
|
|
288
|
+
instructions: `Each choice other than none is the rule in state.rules under its key. Which of them demands the opposite of what state.rules["${index}"] demands of the same code, so that following one breaks the other? Choose none if code can follow each of them alongside it.`,
|
|
289
|
+
criteria,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return { model, state: comparedState(rules, mentioned), questions };
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const pairKey = ([first, second]: Pair): string => `${first}-${second}`;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A `noul` for one pair Jev named: do the two rules demand opposite things of
|
|
300
|
+
* the same code? One pair per request, with only its two rules in the state.
|
|
301
|
+
* Other rules beside them dilute the judgment: over the effect preset, a real
|
|
302
|
+
* contradiction scored 0.89 alone and 0.59 among the other named pairs.
|
|
303
|
+
*/
|
|
304
|
+
export const contradictBody = (model: string, rules: ReadonlyArray<ComparedRule>, pair: Pair) => ({
|
|
305
|
+
model,
|
|
306
|
+
state: comparedState(rules, pair),
|
|
307
|
+
questions: {
|
|
308
|
+
[pairKey(pair)]: {
|
|
309
|
+
type: "noul" as const,
|
|
310
|
+
instructions: `Do state.rules["${pair[0]}"] and state.rules["${pair[1]}"] demand opposite things of the same code, so that following one breaks the other? Answer no if code can follow both, or if they are about different things.`,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The pairs a conflict answer names: per question, the partner Jev chose, when
|
|
317
|
+
* it was one the question offered. "none", or a stray answer, names no pair.
|
|
318
|
+
*/
|
|
319
|
+
export const namedPairs = (
|
|
320
|
+
answers: Readonly<Record<string, { readonly choice: string }>>,
|
|
321
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
322
|
+
): ReadonlyArray<Pair> =>
|
|
323
|
+
Object.entries(answers).flatMap(([key, { choice }]): ReadonlyArray<Pair> => {
|
|
324
|
+
const rule = Number(key.split(":")[0]);
|
|
325
|
+
const partner = Number(choice);
|
|
326
|
+
return partners[rule]?.includes(partner) === true ? [[rule, partner]] : [];
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/** The probability a contradiction answer gives its pair; 0 when it gives none. */
|
|
330
|
+
export const pairProbability = (
|
|
331
|
+
answers: Readonly<Record<string, { readonly noul: number }>>,
|
|
332
|
+
pair: Pair,
|
|
333
|
+
): number => answers[pairKey(pair)]?.noul ?? 0;
|