@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.http.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import type { AppendState, JevState, JudgedFile, RuleId } from "#config.ts";
|
|
1
|
+
import type { AppendState, JevState, JudgedFile, ReadState, RuleId } from "#config.ts";
|
|
2
2
|
import { sectionsOf } from "#excerpt.ts";
|
|
3
|
+
import { jev } from "#providers/jev.ts";
|
|
4
|
+
import {
|
|
5
|
+
Answered,
|
|
6
|
+
type Provider,
|
|
7
|
+
type Question,
|
|
8
|
+
RequestFailed,
|
|
9
|
+
type State,
|
|
10
|
+
} from "#providers/provider.ts";
|
|
11
|
+
import { questionsOf, type SystemOneQuestion } from "#providers/systemOne.ts";
|
|
3
12
|
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
4
13
|
import { Credentials } from "#services/Credentials.ts";
|
|
5
14
|
import {
|
|
6
15
|
type Body,
|
|
7
16
|
type ComparedRule,
|
|
17
|
+
describeState,
|
|
8
18
|
edgeKey,
|
|
9
19
|
conflictBody,
|
|
10
20
|
contradictBody,
|
|
@@ -27,62 +37,50 @@ import {
|
|
|
27
37
|
type Rules,
|
|
28
38
|
sufficiencyKey,
|
|
29
39
|
tokensOf,
|
|
40
|
+
logRequestPayload,
|
|
30
41
|
} from "#services/Jev.ts";
|
|
31
42
|
import {
|
|
32
|
-
type Cause,
|
|
33
43
|
Duration,
|
|
34
44
|
Effect,
|
|
35
45
|
Layer,
|
|
36
|
-
|
|
46
|
+
Predicate,
|
|
37
47
|
Record,
|
|
48
|
+
Redacted,
|
|
38
49
|
Result,
|
|
39
50
|
Schedule,
|
|
40
51
|
Schema,
|
|
41
52
|
} from "effect";
|
|
42
|
-
import {
|
|
43
|
-
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
53
|
+
import { FetchHttpClient } from "effect/http";
|
|
44
54
|
import { RateLimiter } from "effect/persistence";
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
answers: Schema.Record(Schema.String, Schema.
|
|
56
|
+
/** A provider's answers to `noul`s, which the eval's harness reads too, to `choice`s, and to a locate request's both. */
|
|
57
|
+
export const NoulAnswers = Schema.Struct({
|
|
58
|
+
...Answered.fields,
|
|
59
|
+
answers: Schema.Record(Schema.String, Schema.Finite),
|
|
50
60
|
});
|
|
51
61
|
const ChoiceAnswers = Schema.Struct({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/** A locate request's answers: a choice per rule, and a noul per sufficiency question. */
|
|
55
|
-
const LocateAnswers = Schema.Struct({
|
|
56
|
-
answers: Schema.Record(
|
|
57
|
-
Schema.String,
|
|
58
|
-
Schema.Struct({
|
|
59
|
-
choice: Schema.optionalKey(Schema.String),
|
|
60
|
-
noul: Schema.optionalKey(Schema.Finite),
|
|
61
|
-
}),
|
|
62
|
-
),
|
|
62
|
+
...Answered.fields,
|
|
63
|
+
answers: Schema.Record(Schema.String, Schema.String),
|
|
63
64
|
});
|
|
65
|
+
const LocateAnswers = Answered;
|
|
64
66
|
|
|
65
67
|
const refused = (message: string) => JevUnavailable.make({ message });
|
|
66
68
|
|
|
69
|
+
const messageOf = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause));
|
|
70
|
+
|
|
67
71
|
/** Bun's API, for a rule's hook. Read off `globalThis`, as presets.ts reads it: Vitest runs this on Node. */
|
|
68
72
|
const bun = (globalThis as { readonly Bun?: unknown }).Bun as Parameters<AppendState>[2];
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
* A body as it goes out:
|
|
72
|
-
*
|
|
73
|
-
*
|
|
75
|
+
* A prepared body as it goes out: `appendState` sees the shared enriched
|
|
76
|
+
* state, with what it returns spread over this group's state alone.
|
|
77
|
+
* A hook that fails refuses the run.
|
|
74
78
|
*/
|
|
75
79
|
const hooked = (
|
|
76
|
-
|
|
80
|
+
body: Body<SystemOneQuestion> & { readonly state: JevState },
|
|
77
81
|
group: Rules,
|
|
78
82
|
file: JudgedFile,
|
|
79
|
-
|
|
80
|
-
): Effect.Effect<Body, JevUnavailable> => {
|
|
81
|
-
const body = Object.values(group).some(
|
|
82
|
-
(rule) => rule.reads?.includes("workspacePackages") === true,
|
|
83
|
-
)
|
|
84
|
-
? { ...sent, state: { ...sent.state, workspacePackages } }
|
|
85
|
-
: sent;
|
|
83
|
+
): Effect.Effect<Body<SystemOneQuestion>, JevUnavailable> => {
|
|
86
84
|
const [hook] = Object.entries(group).flatMap(([id, rule]) =>
|
|
87
85
|
rule.appendState === undefined ? [] : [[id, rule.appendState] as const],
|
|
88
86
|
);
|
|
@@ -91,71 +89,80 @@ const hooked = (
|
|
|
91
89
|
return Effect.tryPromise({
|
|
92
90
|
try: async () => ({
|
|
93
91
|
...body,
|
|
94
|
-
state: { ...body.state, ...(await appendState(body.state, file, bun)) },
|
|
92
|
+
state: describeState({ ...body.state, ...(await appendState(body.state, file, bun)) }),
|
|
95
93
|
}),
|
|
96
|
-
catch: (cause) =>
|
|
97
|
-
refused(
|
|
98
|
-
`the appendState of ${id} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
99
|
-
),
|
|
94
|
+
catch: (cause) => refused(`the appendState of ${id} failed: ${messageOf(cause)}`),
|
|
100
95
|
});
|
|
101
96
|
};
|
|
102
97
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
98
|
+
/**
|
|
99
|
+
* A provider's rejection, as the run takes it: a `ContextOverflow` skips the
|
|
100
|
+
* file, a `RequestBlocked` reports it blocked, and anything else refuses the
|
|
101
|
+
* run with what the provider said. Read by tag, not class, so a provider
|
|
102
|
+
* built against another copy of adhere is read the same.
|
|
103
|
+
*/
|
|
104
|
+
const rejected = (cause: unknown): JevOverflow | JevBlocked | JevUnavailable => {
|
|
105
|
+
if (Predicate.isTagged(cause, "ContextOverflow")) return JevOverflow.make();
|
|
106
|
+
if (Predicate.isTagged(cause, "RequestBlocked") && Predicate.hasProperty(cause, "reference")) {
|
|
107
|
+
return JevBlocked.make({ ray: String(cause.reference) });
|
|
108
|
+
}
|
|
109
|
+
return refused(messageOf(cause));
|
|
115
110
|
};
|
|
116
111
|
|
|
117
|
-
/**
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
/** Whether a failed attempt is worth another: a `RequestFailed`, read by tag, with no answer, a 408, a 429, or a 5xx. */
|
|
113
|
+
const isTransient = (cause: unknown): boolean => {
|
|
114
|
+
if (!Predicate.isTagged(cause, "RequestFailed") || !Predicate.hasProperty(cause, "status")) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const { status } = cause;
|
|
118
|
+
return (
|
|
119
|
+
status === 0 ||
|
|
120
|
+
status === 408 ||
|
|
121
|
+
status === 429 ||
|
|
122
|
+
(Predicate.isNumber(status) && status >= 500)
|
|
123
|
+
);
|
|
124
|
+
};
|
|
128
125
|
|
|
129
126
|
/**
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
132
|
-
*
|
|
133
|
-
*
|
|
127
|
+
* One request to `provider`, as adhere asks every provider: each attempt
|
|
128
|
+
* after `pace`, given 30 seconds before its signal aborts it, and logged when
|
|
129
|
+
* it fails. One that failed transiently is tried again up to three times,
|
|
130
|
+
* after half a second, one, and two. What it answers decodes as `Answers`.
|
|
131
|
+
* The eval's harness asks its providers this way too.
|
|
134
132
|
*/
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
export const askProvider = <A>(
|
|
134
|
+
provider: Provider,
|
|
135
|
+
state: State,
|
|
136
|
+
questions: ReadonlyArray<Question>,
|
|
137
|
+
Answers: Schema.Codec<A, unknown, never, never>,
|
|
138
|
+
apiKey?: string,
|
|
139
|
+
pace: Effect.Effect<unknown, unknown> = Effect.void,
|
|
140
|
+
): Effect.Effect<A, JevOverflow | JevBlocked | JevUnavailable> =>
|
|
141
|
+
pace.pipe(
|
|
142
|
+
Effect.andThen(
|
|
143
|
+
Effect.tryPromise({
|
|
144
|
+
try: (signal) =>
|
|
145
|
+
provider.ask(state, questions, apiKey === undefined ? { signal } : { apiKey, signal }),
|
|
146
|
+
catch: (cause) => cause,
|
|
147
|
+
}).pipe(
|
|
148
|
+
Effect.timeoutOrElse({
|
|
149
|
+
duration: "30 seconds",
|
|
150
|
+
orElse: () =>
|
|
151
|
+
Effect.fail(new RequestFailed(`${provider.model} did not answer in 30 seconds`, 0)),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
Effect.tapError((cause) => Effect.logDebug(`attempt failed: ${messageOf(cause)}`)),
|
|
156
|
+
Effect.retry({ while: isTransient, times: 3, schedule: Schedule.exponential("500 millis") }),
|
|
157
|
+
Effect.mapError(rejected),
|
|
158
|
+
Effect.flatMap((answered) =>
|
|
159
|
+
Schema.decodeUnknownEffect(Answers)(answered).pipe(
|
|
160
|
+
Effect.mapError((problem) =>
|
|
161
|
+
refused(`${provider.model}'s answers did not decode: ${problem.message}`),
|
|
147
162
|
),
|
|
148
163
|
),
|
|
149
|
-
Effect.tap((body) => Effect.logTrace(`Jev's answer: ${body.slice(0, 4000)}`)),
|
|
150
164
|
),
|
|
151
|
-
(body): Effect.Effect<never, JevOverflow | JevBlocked | JevUnavailable> =>
|
|
152
|
-
response.status === 400 && isOverflow(body)
|
|
153
|
-
? Effect.fail(JevOverflow.make())
|
|
154
|
-
: response.status === 403 && isBlockPage(response, body)
|
|
155
|
-
? Effect.fail(JevBlocked.make({ ray: response.headers["cf-ray"] ?? "none given" }))
|
|
156
|
-
: Effect.fail(refused(`Jev answered HTTP ${response.status}${detailOf(body)}`)),
|
|
157
165
|
);
|
|
158
|
-
};
|
|
159
166
|
|
|
160
167
|
/**
|
|
161
168
|
* Comparing rules has no file to skip, so a request over Jev's context, or one
|
|
@@ -181,70 +188,75 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
181
188
|
Effect.gen(function* () {
|
|
182
189
|
const config = yield* AdhereConfig;
|
|
183
190
|
const credentials = yield* Credentials;
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
algorithm: "token-bucket",
|
|
200
|
-
limit: 1,
|
|
201
|
-
window: Duration.millis(60_000 / config.rpm),
|
|
202
|
-
}),
|
|
191
|
+
const fetch = yield* FetchHttpClient.Fetch;
|
|
192
|
+
const limiter = yield* RateLimiter.RateLimiter;
|
|
193
|
+
|
|
194
|
+
const provider: Provider = config.provider ?? jev({ model: config.model, fetch });
|
|
195
|
+
const previewed = new Set<string>();
|
|
196
|
+
// The key it asks with, when it names one `adhere login` saves. Read when
|
|
197
|
+
// a request needs it, not in the layer: a run with nothing pending needs none.
|
|
198
|
+
const apiKey: Effect.Effect<string | undefined, JevUnavailable> =
|
|
199
|
+
provider.savedKey === undefined
|
|
200
|
+
? Effect.succeed(undefined)
|
|
201
|
+
: yield* Effect.cached(
|
|
202
|
+
credentials.key(provider.savedKey).pipe(
|
|
203
|
+
Effect.map(Redacted.value),
|
|
204
|
+
Effect.mapError((problem) => refused(problem.message)),
|
|
205
|
+
),
|
|
203
206
|
);
|
|
204
|
-
const client = paced.pipe(
|
|
205
|
-
HttpClient.transformResponse(Effect.timeout("30 seconds")),
|
|
206
|
-
// Every failed attempt, including those retried, so a log shows what retrying hid.
|
|
207
|
-
HttpClient.transformResponse(
|
|
208
|
-
Effect.tapError((problem) => Effect.logDebug(`attempt failed: ${attemptFailure(problem)}`)),
|
|
209
|
-
),
|
|
210
|
-
HttpClient.retryTransient({
|
|
211
|
-
schedule: Schedule.exponential("500 millis"),
|
|
212
|
-
times: 3,
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
207
|
|
|
216
|
-
|
|
217
|
-
|
|
208
|
+
// `--rpm`: one attempt per 60/rpm seconds, evenly spaced rather than in a
|
|
209
|
+
// burst each minute, so a retry waits its turn too.
|
|
210
|
+
const pace =
|
|
211
|
+
config.rpm === undefined
|
|
212
|
+
? Effect.void
|
|
213
|
+
: RateLimiter.sleep(limiter, {
|
|
214
|
+
key: "provider",
|
|
215
|
+
algorithm: "token-bucket",
|
|
216
|
+
limit: 1,
|
|
217
|
+
window: Duration.millis(60_000 / config.rpm),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/** One request. `kind` names the question for the log: judge, locate, conflicts, and so on. */
|
|
221
|
+
const ask = <A>(
|
|
222
|
+
kind: string,
|
|
223
|
+
body: Body<SystemOneQuestion>,
|
|
224
|
+
Answers: Schema.Codec<A, unknown, never, never>,
|
|
225
|
+
) =>
|
|
218
226
|
Effect.gen(function* () {
|
|
219
|
-
|
|
220
|
-
const apiKey = yield* credentials.apiKey.pipe(
|
|
221
|
-
Effect.mapError((problem) => refused(problem.message)),
|
|
222
|
-
);
|
|
223
|
-
const request = yield* HttpClientRequest.bodyJson(
|
|
224
|
-
HttpClientRequest.post(SYSTEM_ONE),
|
|
225
|
-
body,
|
|
226
|
-
).pipe(Effect.orDie);
|
|
227
|
+
const key = yield* apiKey;
|
|
227
228
|
const questions = Object.keys(body.questions).length;
|
|
228
229
|
yield* Effect.logDebug(
|
|
229
|
-
`${kind}:
|
|
230
|
+
`${kind}: asking ${provider.model} ${questions} ${questions === 1 ? "question" : "questions"}, about ${tokensOf(body)} tokens`,
|
|
230
231
|
);
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
if ((kind === "judge" || kind === "locate") && !previewed.has(kind)) {
|
|
233
|
+
previewed.add(kind);
|
|
234
|
+
yield* logRequestPayload(
|
|
235
|
+
`${kind}: first ${kind === "locate" ? "match-to-lines " : ""}request payload`,
|
|
236
|
+
() => body,
|
|
237
|
+
"Debug",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const [elapsed, answered] = yield* Effect.timed(
|
|
241
|
+
askProvider(provider, body.state, questionsOf(body.questions), Answers, key, pace).pipe(
|
|
242
|
+
Effect.tapErrorTag("JevOverflow", () =>
|
|
243
|
+
logRequestPayload(
|
|
244
|
+
`${kind}: too long request payload (provider overflow)`,
|
|
245
|
+
() => body,
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
233
249
|
);
|
|
234
250
|
yield* Effect.logDebug(
|
|
235
|
-
`${kind}:
|
|
236
|
-
);
|
|
237
|
-
return yield* HttpClientResponse.schemaBodyJson(Answers)(response).pipe(
|
|
238
|
-
Effect.mapError((problem) =>
|
|
239
|
-
refused(`System One response did not decode: ${problem.message}`),
|
|
240
|
-
),
|
|
251
|
+
`${kind}: ${provider.model} answered in ${Math.round(Duration.toMillis(elapsed))} ms`,
|
|
241
252
|
);
|
|
253
|
+
return answered;
|
|
242
254
|
});
|
|
243
255
|
|
|
244
256
|
/** Every answer to a body, over as many requests as Jev's context needs. */
|
|
245
257
|
const answersTo = <A>(
|
|
246
258
|
kind: string,
|
|
247
|
-
body: Body
|
|
259
|
+
body: Body<SystemOneQuestion>,
|
|
248
260
|
Answers: Schema.Codec<
|
|
249
261
|
{ readonly answers: Readonly<Record<string, A>> },
|
|
250
262
|
unknown,
|
|
@@ -269,7 +281,7 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
269
281
|
const answersAbout = <A>(
|
|
270
282
|
kind: string,
|
|
271
283
|
rules: Rules,
|
|
272
|
-
bodyOf: (group: Rules) => Body & { readonly state: JevState },
|
|
284
|
+
bodyOf: (group: Rules) => Body<SystemOneQuestion> & { readonly state: JevState },
|
|
273
285
|
file: JudgedFile,
|
|
274
286
|
Answers: Schema.Codec<
|
|
275
287
|
{ readonly answers: Readonly<Record<string, A>> },
|
|
@@ -280,9 +292,8 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
280
292
|
) =>
|
|
281
293
|
Effect.map(
|
|
282
294
|
Effect.forEach(requestGroups(rules), (group) =>
|
|
283
|
-
Effect.flatMap(
|
|
284
|
-
|
|
285
|
-
(body) => answersTo(kind, body, Answers),
|
|
295
|
+
Effect.flatMap(hooked(bodyOf(group), group, file), (body) =>
|
|
296
|
+
answersTo(kind, body, Answers),
|
|
286
297
|
),
|
|
287
298
|
),
|
|
288
299
|
(groups) =>
|
|
@@ -294,46 +305,47 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
294
305
|
rules: Rules,
|
|
295
306
|
sampled: ReadonlyArray<RuleId>,
|
|
296
307
|
file: JudgedFile,
|
|
308
|
+
readState: ReadState = {},
|
|
297
309
|
) {
|
|
298
310
|
const answers = yield* answersAbout(
|
|
299
311
|
"judge",
|
|
300
312
|
rules,
|
|
301
|
-
(group) => judgeBody(config.model, lines, group, sampled),
|
|
313
|
+
(group) => judgeBody(config.model, lines, group, sampled, readState),
|
|
302
314
|
file,
|
|
303
315
|
NoulAnswers,
|
|
304
316
|
);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
const all = (scores: ReadonlyArray<number | undefined>): scores is ReadonlyArray<number> =>
|
|
318
|
+
scores.every((score) => score !== undefined);
|
|
319
|
+
// A rule is answered when its question and each of its matchers' are: a
|
|
320
|
+
// matcher left unanswered, as a refusal leaves one, leaves its rule
|
|
321
|
+
// unanswered too, to ask again on the next run rather than cache as a no.
|
|
322
|
+
const judged = Object.entries(rules).flatMap(([id, rule]) => {
|
|
323
|
+
const scored = (kind: keyof MatcherScores) =>
|
|
324
|
+
(rule[kind] ?? []).map((_, index) => answers[matcherKey(kind, id, index)]);
|
|
325
|
+
const probability = answers[id];
|
|
326
|
+
const appliesTo = scored("appliesTo");
|
|
327
|
+
const excludeIf = scored("excludeIf");
|
|
328
|
+
return probability === undefined || !all(appliesTo) || !all(excludeIf)
|
|
329
|
+
? []
|
|
330
|
+
: [{ id, rule, probability, appliesTo, excludeIf }];
|
|
331
|
+
});
|
|
319
332
|
const matchers: Record<RuleId, MatcherScores> = Record.fromEntries(
|
|
320
|
-
|
|
333
|
+
judged.flatMap(({ id, rule, appliesTo, excludeIf }) =>
|
|
321
334
|
rule.appliesTo === undefined && rule.excludeIf === undefined
|
|
322
335
|
? []
|
|
323
|
-
: [
|
|
324
|
-
[
|
|
325
|
-
id,
|
|
326
|
-
{
|
|
327
|
-
appliesTo: scored("appliesTo", id, rule.appliesTo),
|
|
328
|
-
excludeIf: scored("excludeIf", id, rule.excludeIf),
|
|
329
|
-
},
|
|
330
|
-
] as const,
|
|
331
|
-
],
|
|
336
|
+
: [[id, { appliesTo, excludeIf }] as const],
|
|
332
337
|
),
|
|
333
338
|
);
|
|
334
339
|
return {
|
|
335
|
-
probabilities:
|
|
336
|
-
|
|
340
|
+
probabilities: Record.fromEntries(
|
|
341
|
+
judged.map(({ id, probability }) => [id, probability] as const),
|
|
342
|
+
),
|
|
343
|
+
linter: Record.fromEntries(
|
|
344
|
+
sampled.flatMap((id) => {
|
|
345
|
+
const answer = answers[linterKey(id)];
|
|
346
|
+
return answer === undefined ? [] : [[id, answer] as const];
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
337
349
|
...(Record.isEmptyRecord(matchers) ? {} : { matchers }),
|
|
338
350
|
} satisfies Judged;
|
|
339
351
|
});
|
|
@@ -342,25 +354,26 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
342
354
|
lines: Lines,
|
|
343
355
|
rules: Rules,
|
|
344
356
|
file: JudgedFile,
|
|
357
|
+
readState: ReadState = {},
|
|
345
358
|
) {
|
|
346
359
|
const sections = sectionsOf(lines);
|
|
347
360
|
const answers = yield* answersAbout(
|
|
348
361
|
"locate",
|
|
349
362
|
rules,
|
|
350
|
-
(group) => locateBody(config.model, lines, group),
|
|
363
|
+
(group) => locateBody(config.model, lines, group, readState),
|
|
351
364
|
file,
|
|
352
365
|
LocateAnswers,
|
|
353
366
|
);
|
|
354
367
|
// A file of one section has every finding in it. An answer that names no section,
|
|
355
|
-
// or none at all, which
|
|
356
|
-
// the section leave it without lines.
|
|
368
|
+
// or none at all, which a model should not give, leaves the rule unlocated; lines
|
|
369
|
+
// outside the section leave it without lines.
|
|
357
370
|
return Record.filterMap(rules, (_, id) => {
|
|
358
|
-
const chosen = sections.length === 1 ? 1 : Number(answers[id]
|
|
371
|
+
const chosen = sections.length === 1 ? 1 : Number(answers[id]);
|
|
359
372
|
const section = sections[chosen - 1];
|
|
360
|
-
const sufficiency = answers[sufficiencyKey(id)]
|
|
361
|
-
if (section === undefined || sufficiency
|
|
373
|
+
const sufficiency = answers[sufficiencyKey(id)];
|
|
374
|
+
if (section === undefined || typeof sufficiency !== "number") return Result.failVoid;
|
|
362
375
|
const [start, end] = (["start", "end"] as const).map((edge) =>
|
|
363
|
-
Number(answers[edgeKey(edge, id, chosen)]
|
|
376
|
+
Number(answers[edgeKey(edge, id, chosen)]),
|
|
364
377
|
);
|
|
365
378
|
const within = (line: number | undefined): line is number =>
|
|
366
379
|
line !== undefined && section.first <= line && line <= section.last;
|