@drkmttr/adhere 0.6.0 → 0.7.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 +75 -39
- package/package.json +7 -7
- package/presets/effect/basics/external-calls-are-resilient.md +6 -2
- package/presets/effect/basics/fn-for-named-effects.md +10 -2
- package/presets/effect/basics/gen-for-sequencing.md +10 -2
- package/presets/effect/basics/instrument-with-pipe.md +9 -2
- package/presets/effect/cli/handlers-delegate.md +13 -2
- package/presets/effect/config/business-logic-depends-on-config-service.md +9 -2
- package/presets/effect/config/secrets-are-redacted.md +6 -2
- package/presets/effect/config/tests-provide-values-directly.md +8 -2
- package/presets/effect/config/validate-with-schema.md +9 -2
- package/presets/effect/data/brand-meaningful-primitives.md +11 -2
- package/presets/effect/data/decode-json-with-schema.md +6 -2
- package/presets/effect/data/records-are-schema-classes.md +9 -2
- package/presets/effect/data/variants-are-tagged-unions.md +9 -2
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
- package/presets/effect/errors/defects-for-bugs.md +13 -2
- package/presets/effect/errors/domain-errors-are-tagged.md +6 -2
- package/presets/effect/errors/wrap-external-errors.md +7 -2
- package/presets/effect/platform/os-access-through-platform-services.md +8 -2
- package/presets/effect/services/fresh-layer-per-test.md +9 -2
- package/presets/effect/services/memoize-parameterized-layers.md +9 -2
- package/presets/effect/services/methods-have-no-requirements.md +9 -2
- package/presets/effect/services/no-mutable-state.md +9 -2
- package/presets/effect/services/provide-at-entry.md +7 -2
- package/presets/effect/services/test-layers-are-in-memory.md +12 -2
- package/presets/effect/testing/test-clock-for-time.md +11 -2
- package/presets/effect/testing/test-random.md +9 -2
- package/skills/adhere/SKILL.md +24 -17
- package/src/cli.ts +2 -2
- package/src/config.ts +67 -13
- package/src/init.ts +10 -8
- package/src/markdown.ts +59 -25
- package/src/services/Jev.http.ts +36 -11
- package/src/services/Jev.ts +75 -36
- package/src/workflows/audit.ts +34 -21
- package/src/workflows/format.ts +7 -3
package/src/services/Jev.http.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
conflictBody,
|
|
8
8
|
contradictBody,
|
|
9
9
|
Jev,
|
|
10
|
+
JevOverflow,
|
|
10
11
|
JevUnavailable,
|
|
11
12
|
judgeBody,
|
|
12
13
|
type Lines,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
requestsOf,
|
|
19
20
|
type Rules,
|
|
20
21
|
} from "#services/Jev.ts";
|
|
21
|
-
import { type Cause, Effect, Layer, Record, Schedule, Schema } from "effect";
|
|
22
|
+
import { type Cause, Effect, Layer, Option, Record, Schedule, Schema } from "effect";
|
|
22
23
|
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http";
|
|
23
24
|
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
24
25
|
|
|
@@ -39,21 +40,41 @@ const detailOf = (body: string): string => {
|
|
|
39
40
|
return text.length === 0 ? "" : `: ${text.slice(0, 300)}`;
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
/** What Jev answers, with a 400, to a request over its context. */
|
|
44
|
+
const Overflowed = Schema.fromJsonString(
|
|
45
|
+
Schema.Struct({ detail: Schema.Struct({ error_type: Schema.Literal("max_tokens_exceeded") }) }),
|
|
46
|
+
);
|
|
47
|
+
const isOverflow = (body: string): boolean =>
|
|
48
|
+
Option.isSome(Schema.decodeUnknownOption(Overflowed)(body));
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
|
-
* A failed
|
|
44
|
-
*
|
|
51
|
+
* A request that failed. A 400 saying the request is over Jev's context is a
|
|
52
|
+
* `JevOverflow`, which skips the file. Any other answer outside 2xx refuses
|
|
53
|
+
* the run with its status and what Jev said, since the status alone does not
|
|
54
|
+
* say why, and a request that got no answer refuses with the cause.
|
|
45
55
|
*/
|
|
46
|
-
const
|
|
56
|
+
const failed = (problem: HttpClientError.HttpClientError | Cause.TimeoutError) => {
|
|
47
57
|
if (!HttpClientError.isHttpClientError(problem) || problem.reason._tag !== "StatusCodeError") {
|
|
48
|
-
return Effect.
|
|
58
|
+
return Effect.fail(refused(`System One request failed: ${problem.message}`));
|
|
49
59
|
}
|
|
50
60
|
const { response } = problem.reason;
|
|
51
|
-
return Effect.
|
|
61
|
+
return Effect.flatMap(
|
|
52
62
|
Effect.orElseSucceed(response.text, () => ""),
|
|
53
|
-
(body)
|
|
63
|
+
(body): Effect.Effect<never, JevOverflow | JevUnavailable> =>
|
|
64
|
+
response.status === 400 && isOverflow(body)
|
|
65
|
+
? Effect.fail(JevOverflow.make())
|
|
66
|
+
: Effect.fail(refused(`Jev answered HTTP ${response.status}${detailOf(body)}`)),
|
|
54
67
|
);
|
|
55
68
|
};
|
|
56
69
|
|
|
70
|
+
/** Comparing rules has no file to skip, so a request over Jev's context refuses instead. */
|
|
71
|
+
const refuseOverflow = <A>(
|
|
72
|
+
self: Effect.Effect<A, JevOverflow | JevUnavailable>,
|
|
73
|
+
): Effect.Effect<A, JevUnavailable> =>
|
|
74
|
+
Effect.catchTag(self, "JevOverflow", () =>
|
|
75
|
+
Effect.fail(refused("Comparing the rules took a request over Jev's context")),
|
|
76
|
+
);
|
|
77
|
+
|
|
57
78
|
export const JevLive = Layer.effect(Jev)(
|
|
58
79
|
Effect.gen(function* () {
|
|
59
80
|
const config = yield* AdhereConfig;
|
|
@@ -79,7 +100,7 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
79
100
|
).pipe(Effect.orDie);
|
|
80
101
|
const response = yield* client
|
|
81
102
|
.execute(HttpClientRequest.bearerToken(request, apiKey))
|
|
82
|
-
.pipe(Effect.catch(
|
|
103
|
+
.pipe(Effect.catch(failed));
|
|
83
104
|
return yield* HttpClientResponse.schemaBodyJson(Answers)(response).pipe(
|
|
84
105
|
Effect.mapError((problem) =>
|
|
85
106
|
refused(`System One response did not decode: ${problem.message}`),
|
|
@@ -127,7 +148,10 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
127
148
|
rules: ReadonlyArray<ComparedRule>,
|
|
128
149
|
partners: ReadonlyArray<ReadonlyArray<number>>,
|
|
129
150
|
) {
|
|
130
|
-
const answers = yield* answersTo(
|
|
151
|
+
const answers = yield* answersTo(
|
|
152
|
+
conflictBody(config.model, rules, partners),
|
|
153
|
+
ChoiceAnswers,
|
|
154
|
+
).pipe(refuseOverflow);
|
|
131
155
|
return namedPairs(answers, partners);
|
|
132
156
|
});
|
|
133
157
|
|
|
@@ -138,8 +162,9 @@ export const JevLive = Layer.effect(Jev)(
|
|
|
138
162
|
return yield* Effect.forEach(
|
|
139
163
|
pairs,
|
|
140
164
|
(pair) =>
|
|
141
|
-
Effect.map(
|
|
142
|
-
|
|
165
|
+
Effect.map(
|
|
166
|
+
ask(contradictBody(config.model, rules, pair), NoulAnswers).pipe(refuseOverflow),
|
|
167
|
+
({ answers }) => pairProbability(answers, pair),
|
|
143
168
|
),
|
|
144
169
|
{ concurrency: 8 },
|
|
145
170
|
);
|
package/src/services/Jev.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Example, examplesOf, type Rule, type RuleId, type Rules } from "#config.ts";
|
|
2
2
|
import { Context, type Effect, Record, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
export type { Rules };
|
|
@@ -8,17 +8,23 @@ export class JevUnavailable extends Schema.TaggedError<JevUnavailable>()("JevUna
|
|
|
8
8
|
message: Schema.String,
|
|
9
9
|
}) {}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Jev's refusal of a request over its context. `fits` keeps such files from
|
|
13
|
+
* being sent, but by an estimate; this is Jev's own count disagreeing.
|
|
14
|
+
*/
|
|
15
|
+
export class JevOverflow extends Schema.TaggedError<JevOverflow>()("JevOverflow", {}) {}
|
|
16
|
+
|
|
11
17
|
export class Jev extends Context.Service<
|
|
12
18
|
Jev,
|
|
13
19
|
{
|
|
14
20
|
readonly judge: (
|
|
15
21
|
lines: Lines,
|
|
16
22
|
rules: Rules,
|
|
17
|
-
) => Effect.Effect<Record<RuleId, number>, JevUnavailable>;
|
|
23
|
+
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow>;
|
|
18
24
|
readonly locate: (
|
|
19
25
|
lines: Lines,
|
|
20
26
|
rules: Rules,
|
|
21
|
-
) => Effect.Effect<Record<RuleId, number>, JevUnavailable>;
|
|
27
|
+
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow>;
|
|
22
28
|
/**
|
|
23
29
|
* Per rule, the partner Jev names as impossible to follow in the same code,
|
|
24
30
|
* as `[rule, partner]` indexes into `rules`. A rule Jev names none for is
|
|
@@ -66,8 +72,9 @@ const encoder = new TextEncoder();
|
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* An estimate, meant to run high, of the tokens Jev reads for a value: one
|
|
69
|
-
* per three bytes of its JSON.
|
|
70
|
-
*
|
|
75
|
+
* per three bytes of its JSON. Jev 1.13 reads numbered TypeScript at 3.2
|
|
76
|
+
* bytes a token, and this ran 12 to 16 percent high on the eval's requests.
|
|
77
|
+
* Text that packs more tokens into its bytes, such as CJK, can outrun it.
|
|
71
78
|
*/
|
|
72
79
|
export const tokensOf = (value: unknown): number =>
|
|
73
80
|
Math.ceil(encoder.encode(JSON.stringify(value)).length / 3);
|
|
@@ -84,16 +91,23 @@ const stateOf = (lines: Lines) => ({
|
|
|
84
91
|
code: lines.map((line, index) => `${index + 1} | ${line}`).join("\n"),
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
/**
|
|
88
|
-
const
|
|
89
|
-
example === undefined ? { what } : { what, examples: [example] };
|
|
94
|
+
/** The field an example goes under in a question: its word, with `should not` as `should_not`. */
|
|
95
|
+
const fieldOf = (example: Example): string => example.word.replace(" ", "_");
|
|
90
96
|
|
|
91
|
-
/** A rule's
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
})
|
|
97
|
+
/** A rule's examples under their words. Code a rule lacks is left out, not sent empty. */
|
|
98
|
+
const exampleFields = (rule: Rule): Record<string, string> => {
|
|
99
|
+
const { good, bad } = examplesOf(rule);
|
|
100
|
+
return {
|
|
101
|
+
...(good === undefined ? {} : { [fieldOf(good)]: good.code }),
|
|
102
|
+
...(bad === undefined ? {} : { [fieldOf(bad)]: bad.code }),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** The rule as its questions carry it: the description, and its examples under their words. */
|
|
107
|
+
const ruleFields = (rule: Rule) => ({ rule: rule.description, ...exampleFields(rule) });
|
|
108
|
+
|
|
109
|
+
/** A rule's text as a comparison of rules reads it. */
|
|
110
|
+
const ruleState = (rule: Rule) => ({ description: rule.description, ...exampleFields(rule) });
|
|
97
111
|
|
|
98
112
|
/**
|
|
99
113
|
* The compared rules a question mentions, keyed by index: ids alone would
|
|
@@ -112,28 +126,53 @@ const comparedState = (rules: ReadonlyArray<ComparedRule>, mentioned: Iterable<n
|
|
|
112
126
|
|
|
113
127
|
/**
|
|
114
128
|
* 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
|
|
116
|
-
*
|
|
117
|
-
* with
|
|
129
|
+
* state, so each question reads its own rule and no other, its code under its
|
|
130
|
+
* own words: `must` and `never`, or a guideline's `should` and `should_not`.
|
|
131
|
+
* A rule with code to write asks whether the file diverges from it, with the
|
|
132
|
+
* code not to write as an example when there is some. A rule without any
|
|
133
|
+
* asks whether the file contains the code not to write. The criteria draw the
|
|
134
|
+
* line at the rule's scope: a file with no code the rule is about is a no.
|
|
135
|
+
*
|
|
136
|
+
* On the eval, "diverge from the pattern shown in `must`" judged better than
|
|
137
|
+
* asking whether code "must be written the way `must` shows": the same words,
|
|
138
|
+
* but a pattern to compare rather than code to match.
|
|
118
139
|
*/
|
|
119
|
-
export const judgeQuestion = (rule: Rule) =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
export const judgeQuestion = (rule: Rule) => {
|
|
141
|
+
const { good, bad } = examplesOf(rule);
|
|
142
|
+
const avoided = fieldOf(bad ?? { word: "never", code: "" });
|
|
143
|
+
const example = bad === undefined ? "" : `, for example by doing what \`${avoided}\` shows`;
|
|
144
|
+
const [question, yes, no] =
|
|
145
|
+
good === undefined
|
|
146
|
+
? [
|
|
147
|
+
`Does \`code\` contain the pattern shown in \`${avoided}\`, which \`rule\` rules out? Answer no if nothing in this file resembles it.`,
|
|
148
|
+
`\`code\` contains the pattern shown in \`${avoided}\``,
|
|
149
|
+
`\`code\` has nothing like \`${avoided}\``,
|
|
150
|
+
]
|
|
151
|
+
: [
|
|
152
|
+
`Does \`code\` diverge from the pattern shown in \`${fieldOf(good)}\`${example}, as described by \`rule\`? Answer no if the pattern does not apply to this file.`,
|
|
153
|
+
`\`code\` diverges from the pattern shown in \`${fieldOf(good)}\`${example}`,
|
|
154
|
+
"`code` follows the pattern",
|
|
155
|
+
];
|
|
156
|
+
return {
|
|
157
|
+
type: "noul" as const,
|
|
158
|
+
instructions: { question, ...ruleFields(rule) },
|
|
159
|
+
criteria: {
|
|
160
|
+
true: `${yes}, in code that \`rule\` is about`,
|
|
161
|
+
false: `${no}, or has no code that \`rule\` is about`,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
130
165
|
|
|
131
|
-
/**
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
166
|
+
/** What the offending line does, for the line and block questions. */
|
|
167
|
+
const offense = (rule: Rule): string => {
|
|
168
|
+
const { good, bad } = examplesOf(rule);
|
|
169
|
+
const avoided = `\`${fieldOf(bad ?? { word: "never", code: "" })}\``;
|
|
170
|
+
return good === undefined
|
|
171
|
+
? `shows the pattern in ${avoided}`
|
|
172
|
+
: bad === undefined
|
|
173
|
+
? `diverges from \`${fieldOf(good)}\``
|
|
174
|
+
: `diverges from \`${fieldOf(good)}\` or resembles ${avoided}`;
|
|
175
|
+
};
|
|
137
176
|
|
|
138
177
|
const lineCriteria = (lines: Lines, from: number, to: number): Record<string, string> => {
|
|
139
178
|
const criteria: Record<string, string> = {};
|
|
@@ -163,7 +202,7 @@ export const locateBody = (
|
|
|
163
202
|
return {
|
|
164
203
|
type: "choice" as const,
|
|
165
204
|
instructions: {
|
|
166
|
-
question:
|
|
205
|
+
question: `Which line of \`code\` most clearly ${offense(rule)}?`,
|
|
167
206
|
...ruleFields(rule),
|
|
168
207
|
},
|
|
169
208
|
criteria:
|
|
@@ -188,7 +227,7 @@ export const blockBody = (model: string, lines: Lines, rules: Rules) => {
|
|
|
188
227
|
questions: Record.map(rules, (rule) => ({
|
|
189
228
|
type: "choice" as const,
|
|
190
229
|
instructions: {
|
|
191
|
-
question:
|
|
230
|
+
question: `Which block of \`code\` contains the line that most clearly ${offense(rule)}?`,
|
|
192
231
|
...ruleFields(rule),
|
|
193
232
|
},
|
|
194
233
|
criteria,
|
package/src/workflows/audit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Examples, examplesOf, type Rule, type RuleId } from "#config.ts";
|
|
2
2
|
import type { ScannedFile, WalkUnavailable } from "#models/Audit.ts";
|
|
3
3
|
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
4
4
|
import { AuditCache, type Judgment, sha256 } from "#services/AuditCache.ts";
|
|
@@ -18,8 +18,7 @@ import { type Crypto, Effect, Record } from "effect";
|
|
|
18
18
|
export interface Finding {
|
|
19
19
|
readonly rule: RuleId;
|
|
20
20
|
readonly description: string;
|
|
21
|
-
readonly
|
|
22
|
-
readonly avoid?: string;
|
|
21
|
+
readonly examples: Examples;
|
|
23
22
|
readonly file: string;
|
|
24
23
|
readonly line: number;
|
|
25
24
|
readonly snippet: string;
|
|
@@ -98,12 +97,16 @@ const rulesOf = (some: Readonly<Record<RuleId, PreparedRule>>) => Record.map(som
|
|
|
98
97
|
const sizeOf = (some: Readonly<Record<string, unknown>>): number => Object.keys(some).length;
|
|
99
98
|
|
|
100
99
|
/** A refusal that names the file it stopped at. Files finished before it are already cached. */
|
|
101
|
-
const inFile =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
const inFile =
|
|
101
|
+
(file: ScannedFile) =>
|
|
102
|
+
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
103
|
+
Effect.mapError(self, (problem) =>
|
|
104
|
+
problem instanceof JevUnavailable
|
|
105
|
+
? JevUnavailable.make({
|
|
106
|
+
message: `${file.path}: ${problem.message}. Files judged before it are cached, so a rerun continues from there.`,
|
|
107
|
+
})
|
|
108
|
+
: problem,
|
|
109
|
+
);
|
|
107
110
|
|
|
108
111
|
export const planAudit: Effect.Effect<
|
|
109
112
|
AuditPlan,
|
|
@@ -181,18 +184,14 @@ export const executeAudit = (
|
|
|
181
184
|
const jev = yield* Jev;
|
|
182
185
|
const cache = yield* AuditCache;
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
/** Judges and locates one file the plan left pending, with the requests that took. */
|
|
188
|
+
const judgeFile = Effect.fn("audit.judgeFile")(function* ({
|
|
185
189
|
file,
|
|
186
|
-
skipped,
|
|
187
190
|
hash,
|
|
188
191
|
prepared,
|
|
189
192
|
kept,
|
|
190
193
|
pending,
|
|
191
194
|
}: FilePlan) {
|
|
192
|
-
if (skipped) {
|
|
193
|
-
yield* progress({ requests: 0, findings: 0 });
|
|
194
|
-
return fileResult("skipped", []);
|
|
195
|
-
}
|
|
196
195
|
const probabilities = isEmpty(pending)
|
|
197
196
|
? {}
|
|
198
197
|
: yield* jev.judge(file.lines, rulesOf(pending)).pipe(inFile(file));
|
|
@@ -233,8 +232,7 @@ export const executeAudit = (
|
|
|
233
232
|
{
|
|
234
233
|
rule: id,
|
|
235
234
|
description: k.rule.description,
|
|
236
|
-
|
|
237
|
-
avoid: k.rule.avoid,
|
|
235
|
+
examples: examplesOf(k.rule),
|
|
238
236
|
file: file.path,
|
|
239
237
|
line: judgment.line,
|
|
240
238
|
snippet: judgment.snippet ?? "",
|
|
@@ -244,13 +242,28 @@ export const executeAudit = (
|
|
|
244
242
|
: [];
|
|
245
243
|
})
|
|
246
244
|
.sort((a, b) => a.line - b.line);
|
|
247
|
-
|
|
245
|
+
return {
|
|
246
|
+
result: fileResult(cached ? "cached" : "judged", findings),
|
|
248
247
|
requests:
|
|
249
248
|
(isEmpty(pending) ? 0 : judgeRequests(file.lines, rulesOf(pending))) +
|
|
250
249
|
(isEmpty(flagged) ? 0 : locateRequests(file.lines, rulesOf(flagged))),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const auditFile = Effect.fn("audit.file")(function* (plan: FilePlan) {
|
|
254
|
+
const { result, requests } = plan.skipped
|
|
255
|
+
? { result: fileResult("skipped", []), requests: 0 }
|
|
256
|
+
: yield* judgeFile(plan).pipe(
|
|
257
|
+
// Jev counted more than `fits` estimated. Its count decides: skipped the same way.
|
|
258
|
+
Effect.catchTag("JevOverflow", () =>
|
|
259
|
+
Effect.succeed({
|
|
260
|
+
result: fileResult("skipped", []),
|
|
261
|
+
requests: judgeRequests(plan.file.lines, rulesOf(plan.pending)),
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
yield* progress({ requests, findings: result.findings.length });
|
|
266
|
+
return result;
|
|
254
267
|
});
|
|
255
268
|
|
|
256
269
|
const results = yield* Effect.forEach(plan.files, auditFile, { concurrency: 8 });
|
package/src/workflows/format.ts
CHANGED
|
@@ -97,16 +97,20 @@ const excerpt = (finding: Finding, root: string | undefined): ReadonlyArray<Line
|
|
|
97
97
|
];
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The code to write as the hint. A rule with only code not to write shows
|
|
102
|
+
* that instead, labeled with its word: `never`, or `should not`.
|
|
103
|
+
*/
|
|
101
104
|
const hint = (finding: Finding): ReadonlyArray<Line> => {
|
|
105
|
+
const { good, bad } = finding.examples;
|
|
102
106
|
const [label, code] =
|
|
103
|
-
|
|
107
|
+
good === undefined ? [bad?.word ?? "hint", bad?.code ?? ""] : ["hint", good.code];
|
|
104
108
|
return hanging(span(` ${label}: `, "label"), codeLines(code));
|
|
105
109
|
};
|
|
106
110
|
|
|
107
111
|
/**
|
|
108
112
|
* One diagnostic, in the frame `vp lint` prints on a terminal: a red header,
|
|
109
|
-
* the offending line, a pink underline, and the
|
|
113
|
+
* the offending line, a pink underline, and the code to write as the hint.
|
|
110
114
|
*/
|
|
111
115
|
const frame = (finding: Finding, root: string | undefined): ReadonlyArray<Line> => [
|
|
112
116
|
header(finding),
|