@drkmttr/adhere 0.4.1 → 0.5.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 +14 -10
- package/package.json +6 -6
- package/skills/adhere/SKILL.md +1 -1
- package/src/cli.ts +13 -5
- package/src/contradictions.ts +51 -57
- package/src/services/Jev.http.ts +29 -1
- package/src/services/Jev.ts +129 -5
package/README.md
CHANGED
|
@@ -30,9 +30,9 @@ with a TypeSafe AI API key: the one `adhere login` saved, or
|
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
adhere init # .adhere/config.ts and two example rules
|
|
33
|
-
adhere validate # check the config and rules, without Jev
|
|
34
33
|
echo ".adhere/cache/" >> .gitignore
|
|
35
34
|
adhere login # save your TypeSafe AI API key, once
|
|
35
|
+
adhere validate # check the rules, and ask Jev whether any contradict
|
|
36
36
|
adhere lint # audit the working directory
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -68,7 +68,7 @@ refusal prints its reason.
|
|
|
68
68
|
adhere lint # audit the working directory
|
|
69
69
|
adhere lint --preset effect # add a built-in rule set; the config becomes optional
|
|
70
70
|
adhere lint --threshold 0.8 # replace the config's threshold; per-rule thresholds still apply
|
|
71
|
-
adhere validate # load the config and rules
|
|
71
|
+
adhere validate # load the config and rules, ask Jev whether any contradict
|
|
72
72
|
adhere init [--force] # scaffold .adhere/config.ts and two example rules
|
|
73
73
|
adhere login # save a TypeSafe AI API key for later runs
|
|
74
74
|
adhere logout # delete the saved key
|
|
@@ -99,20 +99,24 @@ kept, and no second one is added, even with `--force`.
|
|
|
99
99
|
### Validate
|
|
100
100
|
|
|
101
101
|
`adhere validate` loads the config, rule files, and presets the way `lint`
|
|
102
|
-
does,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
does, so a file that fails to decode refuses here too, and it prints how many
|
|
103
|
+
rules it loaded. `--preset` adds a built-in rule set, as for `lint`. It then
|
|
104
|
+
asks Jev whether any two rules that apply to the same files contradict, so that
|
|
105
|
+
no code can follow both: one request names, per rule, the rule it conflicts
|
|
106
|
+
with, if any, and each named pair then gets a probability from a request of
|
|
107
|
+
its own, holding only those two rules, since other rules beside them dilute
|
|
108
|
+
the judgment. A pair above
|
|
109
|
+
the threshold is printed with both rule files, and the exit code is 1. Two
|
|
110
|
+
rules with the same id are not compared: a nested rule that shares an id
|
|
111
|
+
shadows the other on purpose. When no two rules share files nothing is sent;
|
|
112
|
+
otherwise `validate` needs the API key, as `lint` does.
|
|
109
113
|
|
|
110
114
|
### Login
|
|
111
115
|
|
|
112
116
|
`adhere login` prompts for a TypeSafe AI API key, masking what you type, and
|
|
113
117
|
saves it to `~/.config/adhere/credentials.json`, or under `$XDG_CONFIG_HOME`
|
|
114
118
|
when that is set, readable only by you. Piped input is read instead of a
|
|
115
|
-
prompt: `adhere login < key.txt`. `lint`
|
|
119
|
+
prompt: `adhere login < key.txt`. `lint` and `validate` use the saved key, but
|
|
116
120
|
`TYPESAFE_API_KEY`, when set, takes precedence, so CI can pass a key without a
|
|
117
121
|
login. `adhere logout` deletes the saved key.
|
|
118
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against reference code in .adhere/, judged by Jev.",
|
|
6
6
|
"repository": {
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
71
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
72
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
73
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
74
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
70
|
+
"@drkmttr/adhere-darwin-arm64": "0.5.0",
|
|
71
|
+
"@drkmttr/adhere-darwin-x64": "0.5.0",
|
|
72
|
+
"@drkmttr/adhere-linux-arm64": "0.5.0",
|
|
73
|
+
"@drkmttr/adhere-linux-x64": "0.5.0",
|
|
74
|
+
"@drkmttr/adhere-win32-x64": "0.5.0"
|
|
75
75
|
}
|
|
76
76
|
}
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -103,7 +103,7 @@ precedence; in this org it comes from SOPS via `just shell`.
|
|
|
103
103
|
## 5. Run and calibrate
|
|
104
104
|
|
|
105
105
|
```sh
|
|
106
|
-
adhere validate # decodes the config and rules,
|
|
106
|
+
adhere validate # decodes the config and rules, asks Jev about contradictions
|
|
107
107
|
adhere lint # or: adhere lint --preset effect
|
|
108
108
|
adhere lint --threshold 0.9 # re-reads the cache, sends nothing
|
|
109
109
|
```
|
package/src/cli.ts
CHANGED
|
@@ -94,16 +94,24 @@ export const lintCommand = Command.make("lint", { preset, threshold }, () =>
|
|
|
94
94
|
),
|
|
95
95
|
);
|
|
96
96
|
|
|
97
|
+
/** Jev, with the key `lint` uses, and the config it reads the model and threshold from. */
|
|
98
|
+
export const validateLayer = (overrides: Overrides) =>
|
|
99
|
+
JevLive.pipe(
|
|
100
|
+
Layer.provide([FetchHttpClient.layer, CredentialsLive]),
|
|
101
|
+
Layer.provideMerge(AdhereConfigLive(overrides)),
|
|
102
|
+
);
|
|
103
|
+
|
|
97
104
|
/**
|
|
98
|
-
* `adhere validate`: everything `lint` loads,
|
|
99
|
-
*
|
|
105
|
+
* `adhere validate`: everything `lint` loads, then Jev on whether any two rules
|
|
106
|
+
* that apply to the same files contradict. A config, rule file, or preset that
|
|
107
|
+
* does not decode refuses here the way it would refuse `lint`.
|
|
100
108
|
*/
|
|
101
109
|
export const validateCommand = Command.make("validate", { preset }, () =>
|
|
102
110
|
Effect.gen(function* () {
|
|
103
111
|
const config = yield* AdhereConfig;
|
|
104
112
|
const path = yield* Path.Path;
|
|
105
113
|
const entries = config.scopedRules ?? globalRuleSet(config.rules, path.resolve());
|
|
106
|
-
const contradictions = findContradictions(entries);
|
|
114
|
+
const contradictions = yield* findContradictions(entries, config.threshold);
|
|
107
115
|
const loaded = `${entries.length} ${entries.length === 1 ? "rule" : "rules"} loaded.`;
|
|
108
116
|
yield* Console.log([loaded, formatContradictions(contradictions)].join("\n"));
|
|
109
117
|
if (contradictions.length > 0) {
|
|
@@ -112,9 +120,9 @@ export const validateCommand = Command.make("validate", { preset }, () =>
|
|
|
112
120
|
}),
|
|
113
121
|
).pipe(
|
|
114
122
|
Command.withDescription(
|
|
115
|
-
"Load the config, rule files, and presets the way lint does,
|
|
123
|
+
"Load the config, rule files, and presets the way lint does, then ask Jev whether any two rules that apply to the same files contradict each other.",
|
|
116
124
|
),
|
|
117
|
-
Command.provide((input) =>
|
|
125
|
+
Command.provide((input) => validateLayer({ presets: chosenPresets(input.preset) })),
|
|
118
126
|
);
|
|
119
127
|
|
|
120
128
|
/** `adhere init`: the scaffold. */
|
package/src/contradictions.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import type { RuleEntry } from "#rules.ts";
|
|
2
|
+
import { Jev, type Pair } from "#services/Jev.ts";
|
|
3
|
+
import { Effect } from "effect";
|
|
2
4
|
|
|
3
5
|
export interface Contradiction {
|
|
4
6
|
readonly first: RuleEntry;
|
|
5
7
|
readonly second: RuleEntry;
|
|
6
|
-
|
|
8
|
+
/** Jev's probability that no code can follow both rules. */
|
|
9
|
+
readonly probability: number;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
type Polarity = "positive" | "negative";
|
|
10
|
-
|
|
11
|
-
const NEGATIVE = /\b(?:avoid|do not|don't|must not|never|no|should not)\b/gi;
|
|
12
|
-
const POSITIVE = /\b(?:always|must|prefer|require|requires|required|should|use)\b/gi;
|
|
13
|
-
const NEGATIVE_WORD = /\b(?:avoid|do not|don't|must not|never|no|should not)\b/i;
|
|
14
|
-
const POSITIVE_WORD = /\b(?:always|must|prefer|require|requires|required|should|use)\b/i;
|
|
15
|
-
const FILLER = /\b(?:a|an|the|for|to|with|by|in|of)\b/gi;
|
|
16
|
-
|
|
17
12
|
const normalized = (path: string): string => path.replaceAll("\\", "/");
|
|
18
13
|
|
|
19
14
|
const scopesOverlap = (a: string, b: string): boolean => {
|
|
@@ -22,58 +17,57 @@ const scopesOverlap = (a: string, b: string): boolean => {
|
|
|
22
17
|
return first === second || first.startsWith(`${second}/`) || second.startsWith(`${first}/`);
|
|
23
18
|
};
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.trim()
|
|
39
|
-
.replace(/\s+/g, " ");
|
|
20
|
+
/**
|
|
21
|
+
* Per rule, the rules that apply to some of the same files. A rule with the
|
|
22
|
+
* same id is left out: a nested rule that shares an id shadows the other on
|
|
23
|
+
* purpose.
|
|
24
|
+
*/
|
|
25
|
+
const partnersOf = (entries: ReadonlyArray<RuleEntry>): ReadonlyArray<ReadonlyArray<number>> =>
|
|
26
|
+
entries.map((first, index) =>
|
|
27
|
+
entries.flatMap((second, other) =>
|
|
28
|
+
other !== index && first.id !== second.id && scopesOverlap(first.scope, second.scope)
|
|
29
|
+
? [other]
|
|
30
|
+
: [],
|
|
31
|
+
),
|
|
32
|
+
);
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
firstPolarity === undefined ||
|
|
48
|
-
secondPolarity === undefined ||
|
|
49
|
-
firstPolarity === secondPolarity
|
|
50
|
-
) {
|
|
51
|
-
return false;
|
|
34
|
+
/** Each pair Jev named once, lower index first. */
|
|
35
|
+
const distinctPairs = (named: ReadonlyArray<Pair>): ReadonlyArray<Pair> => {
|
|
36
|
+
const pairs = new Map<string, Pair>();
|
|
37
|
+
for (const [rule, partner] of named) {
|
|
38
|
+
const pair: Pair = rule < partner ? [rule, partner] : [partner, rule];
|
|
39
|
+
pairs.set(`${pair[0]}-${pair[1]}`, pair);
|
|
52
40
|
}
|
|
53
|
-
|
|
54
|
-
const secondTopic = topicOf(second.rule.description);
|
|
55
|
-
return firstTopic.length > 0 && firstTopic === secondTopic;
|
|
41
|
+
return [...pairs.values()].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
56
42
|
};
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Pairs of rules that apply to the same files and that Jev judges no code can
|
|
46
|
+
* follow both of, above `threshold`. Jev first names, per rule, which rule
|
|
47
|
+
* sharing its files it conflicts with, then gives each named pair a
|
|
48
|
+
* probability. Nothing is sent when no two rules share files, so such a run
|
|
49
|
+
* needs no API key.
|
|
50
|
+
*/
|
|
51
|
+
export const findContradictions = Effect.fn("findContradictions")(function* (
|
|
59
52
|
entries: ReadonlyArray<RuleEntry>,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
53
|
+
threshold: number,
|
|
54
|
+
) {
|
|
55
|
+
const none: ReadonlyArray<Contradiction> = [];
|
|
56
|
+
const partners = partnersOf(entries);
|
|
57
|
+
if (partners.every((some) => some.length === 0)) return none;
|
|
58
|
+
const jev = yield* Jev;
|
|
59
|
+
const pairs = distinctPairs(yield* jev.conflicts(entries, partners));
|
|
60
|
+
if (pairs.length === 0) return none;
|
|
61
|
+
const probabilities = yield* jev.contradicts(entries, pairs);
|
|
62
|
+
return pairs.flatMap(([first, second], index): ReadonlyArray<Contradiction> => {
|
|
63
|
+
const one = entries[first];
|
|
64
|
+
const other = entries[second];
|
|
65
|
+
const probability = probabilities[index] ?? 0;
|
|
66
|
+
return one !== undefined && other !== undefined && probability > threshold
|
|
67
|
+
? [{ first: one, second: other, probability }]
|
|
68
|
+
: [];
|
|
69
|
+
});
|
|
70
|
+
});
|
|
77
71
|
|
|
78
72
|
const locationOf = (entry: RuleEntry): string => `${entry.id} (${entry.file ?? entry.scope})`;
|
|
79
73
|
|
|
@@ -87,7 +81,7 @@ export const formatContradictions = (contradictions: ReadonlyArray<Contradiction
|
|
|
87
81
|
} among configured rules:`,
|
|
88
82
|
"",
|
|
89
83
|
...contradictions.flatMap((contradiction, index) => [
|
|
90
|
-
`${index + 1}. ${contradiction.
|
|
84
|
+
`${index + 1}. No code can follow both (${contradiction.probability.toFixed(2)}):`,
|
|
91
85
|
` - ${locationOf(contradiction.first)}`,
|
|
92
86
|
` ${contradiction.first.rule.description}`,
|
|
93
87
|
` - ${locationOf(contradiction.second)}`,
|
package/src/services/Jev.http.ts
CHANGED
|
@@ -2,12 +2,18 @@ import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
|
2
2
|
import { Credentials } from "#services/Credentials.ts";
|
|
3
3
|
import {
|
|
4
4
|
blockBody,
|
|
5
|
+
type ComparedRule,
|
|
6
|
+
conflictBody,
|
|
7
|
+
contradictBody,
|
|
5
8
|
Jev,
|
|
6
9
|
JevUnavailable,
|
|
7
10
|
judgeBody,
|
|
8
11
|
type Lines,
|
|
9
12
|
locateBody,
|
|
13
|
+
namedPairs,
|
|
10
14
|
needsBlocks,
|
|
15
|
+
type Pair,
|
|
16
|
+
pairProbability,
|
|
11
17
|
type Rules,
|
|
12
18
|
} from "#services/Jev.ts";
|
|
13
19
|
import { Effect, Layer, Record, Schedule, Schema } from "effect";
|
|
@@ -76,6 +82,28 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
76
82
|
return chosen(answers);
|
|
77
83
|
});
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
const conflicts = Effect.fn("Jev.conflicts")(function* (
|
|
86
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
87
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
88
|
+
) {
|
|
89
|
+
const { answers } = yield* ask(conflictBody(config.model, rules, partners), ChoiceAnswers);
|
|
90
|
+
return namedPairs(answers, partners);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const contradicts = Effect.fn("Jev.contradicts")(function* (
|
|
94
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
95
|
+
pairs: ReadonlyArray<Pair>,
|
|
96
|
+
) {
|
|
97
|
+
return yield* Effect.forEach(
|
|
98
|
+
pairs,
|
|
99
|
+
(pair) =>
|
|
100
|
+
Effect.map(ask(contradictBody(config.model, rules, pair), NoulAnswers), ({ answers }) =>
|
|
101
|
+
pairProbability(answers, pair),
|
|
102
|
+
),
|
|
103
|
+
{ concurrency: 8 },
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return Jev.of({ judge, locate, conflicts, contradicts });
|
|
80
108
|
}),
|
|
81
109
|
);
|
package/src/services/Jev.ts
CHANGED
|
@@ -19,9 +19,35 @@ 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;
|
|
@@ -34,15 +60,33 @@ const isBlank = (line: string): boolean => line.trim().length === 0;
|
|
|
34
60
|
export const needsBlocks = (lines: Lines): boolean =>
|
|
35
61
|
lines.filter((line) => !isBlank(line)).length > CHOICE_LIMIT;
|
|
36
62
|
|
|
63
|
+
/** A rule's text as Jev reads it. Code a rule lacks is left out, not sent empty. */
|
|
64
|
+
const ruleState = ({ description, reference, avoid }: Rule) => ({
|
|
65
|
+
description,
|
|
66
|
+
...(reference === undefined ? {} : { reference }),
|
|
67
|
+
...(avoid === undefined ? {} : { avoid }),
|
|
68
|
+
});
|
|
69
|
+
|
|
37
70
|
const stateOf = (lines: Lines, rules: Rules) => ({
|
|
38
71
|
code: lines.map((line, index) => `${index + 1} | ${line}`).join("\n"),
|
|
39
|
-
rules: Record.map(rules, (
|
|
40
|
-
description,
|
|
41
|
-
...(reference === undefined ? {} : { reference }),
|
|
42
|
-
...(avoid === undefined ? {} : { avoid }),
|
|
43
|
-
})),
|
|
72
|
+
rules: Record.map(rules, (rule) => ruleState(rule)),
|
|
44
73
|
});
|
|
45
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The compared rules a question mentions, keyed by index: ids alone would
|
|
77
|
+
* collide, since a nested rule can share its id with the rule it shadows.
|
|
78
|
+
*/
|
|
79
|
+
const comparedState = (rules: ReadonlyArray<ComparedRule>, mentioned: Iterable<number>) => {
|
|
80
|
+
const state: Record<string, { readonly id: RuleId } & ReturnType<typeof ruleState>> = {};
|
|
81
|
+
for (const index of [...new Set(mentioned)].sort((a, b) => a - b)) {
|
|
82
|
+
const compared = rules[index];
|
|
83
|
+
if (compared !== undefined) {
|
|
84
|
+
state[String(index)] = { id: compared.id, ...ruleState(compared.rule) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { rules: state };
|
|
88
|
+
};
|
|
89
|
+
|
|
46
90
|
const field = (id: RuleId, key: "avoid" | "description" | "reference") =>
|
|
47
91
|
`state.rules["${id}"].${key}`;
|
|
48
92
|
|
|
@@ -123,3 +167,83 @@ export const blockBody = (model: string, lines: Lines, rules: Rules) => {
|
|
|
123
167
|
})),
|
|
124
168
|
};
|
|
125
169
|
};
|
|
170
|
+
|
|
171
|
+
/** A choice keeps one criterion for "none", which leaves this many for partners. */
|
|
172
|
+
const PARTNERS_PER_QUESTION = CHOICE_LIMIT - 1;
|
|
173
|
+
|
|
174
|
+
interface ChoiceQuestion {
|
|
175
|
+
readonly type: "choice";
|
|
176
|
+
readonly instructions: string;
|
|
177
|
+
readonly criteria: Record<string, string>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Per rule, one `choice` among the rules that apply to the same files: which
|
|
182
|
+
* of them cannot be followed in the same code, or none. The questions grow
|
|
183
|
+
* with the rules, not with every pair of them. A rule with more partners than
|
|
184
|
+
* a choice holds gets one question per chunk, keyed `rule:chunk`.
|
|
185
|
+
*/
|
|
186
|
+
export const conflictBody = (
|
|
187
|
+
model: string,
|
|
188
|
+
rules: ReadonlyArray<ComparedRule>,
|
|
189
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
190
|
+
) => {
|
|
191
|
+
const questions: Record<string, ChoiceQuestion> = {};
|
|
192
|
+
const mentioned: Array<number> = [];
|
|
193
|
+
partners.forEach((some, index) => {
|
|
194
|
+
for (let chunk = 0; chunk * PARTNERS_PER_QUESTION < some.length; chunk++) {
|
|
195
|
+
const from = chunk * PARTNERS_PER_QUESTION;
|
|
196
|
+
const offered = some.slice(from, from + PARTNERS_PER_QUESTION);
|
|
197
|
+
const criteria: Record<string, string> = { none: "None of them" };
|
|
198
|
+
for (const partner of offered) {
|
|
199
|
+
criteria[String(partner)] = rules[partner]?.rule.description ?? "";
|
|
200
|
+
}
|
|
201
|
+
mentioned.push(index, ...offered);
|
|
202
|
+
questions[`${index}:${chunk}`] = {
|
|
203
|
+
type: "choice",
|
|
204
|
+
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.`,
|
|
205
|
+
criteria,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return { model, state: comparedState(rules, mentioned), questions };
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const pairKey = ([first, second]: Pair): string => `${first}-${second}`;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A `noul` for one pair Jev named: do the two rules demand opposite things of
|
|
216
|
+
* the same code? One pair per request, with only its two rules in the state.
|
|
217
|
+
* Other rules beside them dilute the judgment: over the effect preset, a real
|
|
218
|
+
* contradiction scored 0.89 alone and 0.59 among the other named pairs.
|
|
219
|
+
*/
|
|
220
|
+
export const contradictBody = (model: string, rules: ReadonlyArray<ComparedRule>, pair: Pair) => ({
|
|
221
|
+
model,
|
|
222
|
+
state: comparedState(rules, pair),
|
|
223
|
+
questions: {
|
|
224
|
+
[pairKey(pair)]: {
|
|
225
|
+
type: "noul" as const,
|
|
226
|
+
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.`,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The pairs a conflict answer names: per question, the partner Jev chose, when
|
|
233
|
+
* it was one the question offered. "none", or a stray answer, names no pair.
|
|
234
|
+
*/
|
|
235
|
+
export const namedPairs = (
|
|
236
|
+
answers: Readonly<Record<string, { readonly choice: string }>>,
|
|
237
|
+
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
238
|
+
): ReadonlyArray<Pair> =>
|
|
239
|
+
Object.entries(answers).flatMap(([key, { choice }]): ReadonlyArray<Pair> => {
|
|
240
|
+
const rule = Number(key.split(":")[0]);
|
|
241
|
+
const partner = Number(choice);
|
|
242
|
+
return partners[rule]?.includes(partner) === true ? [[rule, partner]] : [];
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/** The probability a contradiction answer gives its pair; 0 when it gives none. */
|
|
246
|
+
export const pairProbability = (
|
|
247
|
+
answers: Readonly<Record<string, { readonly noul: number }>>,
|
|
248
|
+
pair: Pair,
|
|
249
|
+
): number => answers[pairKey(pair)]?.noul ?? 0;
|