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