@drkmttr/adhere 0.3.1 → 0.4.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 +30 -11
- package/package.json +6 -6
- package/skills/adhere/SKILL.md +10 -3
- package/src/config.ts +11 -2
- package/src/init.ts +10 -0
- package/src/markdown.ts +42 -10
- package/src/services/Jev.ts +33 -9
- package/src/workflows/audit.ts +17 -2
- package/src/workflows/format.ts +9 -5
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# adhere
|
|
2
2
|
|
|
3
|
-
A linter for rules a normal linter cannot check. A rule is a
|
|
4
|
-
reference code
|
|
5
|
-
asks [Jev](https://typesafe.ai) (TypeSafe AI's System One
|
|
6
|
-
file
|
|
3
|
+
A linter for rules a normal linter cannot check. A rule is a one-sentence
|
|
4
|
+
description plus correct reference code, code to avoid, or both. For each
|
|
5
|
+
source file, adhere asks [Jev](https://typesafe.ai) (TypeSafe AI's System One
|
|
6
|
+
model) whether the file breaks each rule, gets a calibrated probability per
|
|
7
7
|
rule, and reports the ones above a threshold with the line Jev points at. The
|
|
8
8
|
report uses the same frame as `vp lint`.
|
|
9
9
|
|
|
@@ -54,7 +54,8 @@ Found 1 error.
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
The header is the rule id, Jev's probability, and the rule's description. The
|
|
57
|
-
hint is the rule's reference
|
|
57
|
+
hint is the rule's reference; a rule with only code to avoid shows that code,
|
|
58
|
+
labeled `avoid:`, instead. The exit code is 0 when nothing is reported and 1
|
|
58
59
|
when something is. It is also 1 when the run refuses, for example on an invalid
|
|
59
60
|
config or rule file, a missing `TYPESAFE_API_KEY`, or an unknown command or
|
|
60
61
|
flag; a refusal prints its reason.
|
|
@@ -128,6 +129,7 @@ export default {
|
|
|
128
129
|
const UserId = Schema.String.pipe(Schema.brand("UserId"))
|
|
129
130
|
type UserId = typeof UserId.Type
|
|
130
131
|
`,
|
|
132
|
+
avoid: "type UserId = string", // optional: what a violation looks like
|
|
131
133
|
threshold: 0.8, // optional per-rule override
|
|
132
134
|
},
|
|
133
135
|
},
|
|
@@ -154,7 +156,8 @@ directory exists, it is read without any config.
|
|
|
154
156
|
|
|
155
157
|
A file is front matter, then a body. The first fenced code block in the body
|
|
156
158
|
is the reference; prose around it renders on GitHub and is ignored. Without a
|
|
157
|
-
fence, the whole body is the reference.
|
|
159
|
+
fence, the whole body is the reference. A fence tagged `avoid` after its
|
|
160
|
+
language holds code to avoid instead: what a violation looks like.
|
|
158
161
|
|
|
159
162
|
````md
|
|
160
163
|
---
|
|
@@ -170,11 +173,25 @@ const Port = Schema.Int.pipe(
|
|
|
170
173
|
Schema.brand("Port"),
|
|
171
174
|
);
|
|
172
175
|
```
|
|
176
|
+
|
|
177
|
+
Not this:
|
|
178
|
+
|
|
179
|
+
```ts avoid
|
|
180
|
+
const port: number = Number(process.env.PORT);
|
|
181
|
+
```
|
|
173
182
|
````
|
|
174
183
|
|
|
175
|
-
`description` is required
|
|
176
|
-
validation refuses the run with its
|
|
177
|
-
from the package root does the same
|
|
184
|
+
`description` is required, with a reference, code to avoid, or both.
|
|
185
|
+
`threshold` is optional. A file that fails validation refuses the run with its
|
|
186
|
+
path in the message. `loadRules(directory)` from the package root does the same
|
|
187
|
+
load for your own tooling.
|
|
188
|
+
|
|
189
|
+
With a reference, Jev is asked whether the file diverges from it, and code to
|
|
190
|
+
avoid, when there is some, is its example of diverging. With only code to
|
|
191
|
+
avoid, Jev is asked whether the file contains it. That suits a rule with no
|
|
192
|
+
single correct form to show, such as a hand-rolled retry loop or an error
|
|
193
|
+
caught and dropped. Neither goes in the other's place: code to avoid in the
|
|
194
|
+
reference reads to Jev as the pattern to follow.
|
|
178
195
|
|
|
179
196
|
Nested `.adhere/` directories are also discovered, except under
|
|
180
197
|
`node_modules/`, `dist/`, and the other skipped directories (above). A rule in
|
|
@@ -221,8 +238,10 @@ beats all of the above for that rule.
|
|
|
221
238
|
## How a file is judged
|
|
222
239
|
|
|
223
240
|
1. One Jev request per file, with the file's numbered lines and every rule as
|
|
224
|
-
the state, and one `noul` (yes/no probability) question per rule
|
|
225
|
-
|
|
241
|
+
the state, and one `noul` (yes/no probability) question per rule: whether
|
|
242
|
+
the file diverges from the reference or, for a rule with only code to
|
|
243
|
+
avoid, whether it contains that code. Every question shares the state cost
|
|
244
|
+
of the request.
|
|
226
245
|
2. A second request only when at least one rule's probability is above its
|
|
227
246
|
threshold: one `choice` question per flagged rule over the file's non-blank
|
|
228
247
|
lines, which yields the line to report. A file with more than 255 non-blank
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
71
|
+
"@drkmttr/adhere-darwin-x64": "0.4.0",
|
|
72
|
+
"@drkmttr/adhere-linux-arm64": "0.4.0",
|
|
73
|
+
"@drkmttr/adhere-linux-x64": "0.4.0",
|
|
74
|
+
"@drkmttr/adhere-win32-x64": "0.4.0"
|
|
75
75
|
}
|
|
76
76
|
}
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -61,15 +61,22 @@ Optional prose for readers on GitHub. adhere ignores it.
|
|
|
61
61
|
```ts
|
|
62
62
|
// Correct code lifted from this repo, trimmed to the pattern.
|
|
63
63
|
```
|
|
64
|
+
|
|
65
|
+
```ts avoid
|
|
66
|
+
// Optional: the incorrect form this rule catches, as it appeared in the repo.
|
|
67
|
+
```
|
|
64
68
|
````
|
|
65
69
|
|
|
66
70
|
Rules for the rule:
|
|
67
71
|
|
|
68
72
|
- `description` is one sentence, positive, specific. Jev reads it with the
|
|
69
73
|
reference; vague words ("properly", "correctly") give it nothing to judge.
|
|
70
|
-
- The reference is real code from the repo, not invented
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
- The reference is real code from the repo, not invented, and only correct
|
|
75
|
+
code: Jev compares files to it, so incorrect code there teaches the wrong
|
|
76
|
+
thing. Incorrect code goes in a fence tagged `avoid`, which Jev reads as what
|
|
77
|
+
a violation looks like. Add one when violations have a recognizable shape,
|
|
78
|
+
ideally one found in the repo's history. A rule with no single correct form
|
|
79
|
+
can be only an `avoid` block.
|
|
73
80
|
- One pattern per file. Two patterns in one reference blur the probability.
|
|
74
81
|
- `threshold` is optional. Start without it; set it in step 5 if needed.
|
|
75
82
|
|
package/src/config.ts
CHANGED
|
@@ -6,9 +6,18 @@ export type RuleId = string;
|
|
|
6
6
|
|
|
7
7
|
export const Rule = Schema.Struct({
|
|
8
8
|
description: Schema.String,
|
|
9
|
-
|
|
9
|
+
/** Correct code: the pattern a file should follow. */
|
|
10
|
+
reference: Schema.optionalKey(Schema.String),
|
|
11
|
+
/** Incorrect code: what a violation looks like. */
|
|
12
|
+
avoid: Schema.optionalKey(Schema.String),
|
|
10
13
|
threshold: Schema.optionalKey(Schema.Finite),
|
|
11
|
-
})
|
|
14
|
+
}).check(
|
|
15
|
+
Schema.makeFilter((rule) =>
|
|
16
|
+
rule.reference !== undefined || rule.avoid !== undefined
|
|
17
|
+
? undefined
|
|
18
|
+
: "a rule needs a reference, code to avoid, or both",
|
|
19
|
+
),
|
|
20
|
+
);
|
|
12
21
|
export interface Rule extends Schema.Schema.Type<typeof Rule> {}
|
|
13
22
|
|
|
14
23
|
export type Rules = Readonly<Record<RuleId, Rule>>;
|
package/src/init.ts
CHANGED
|
@@ -43,6 +43,16 @@ export const loadCustomerProfile = (customerId: CustomerId) =>
|
|
|
43
43
|
return yield* CustomerStore.find(customerId);
|
|
44
44
|
});
|
|
45
45
|
\`\`\`
|
|
46
|
+
|
|
47
|
+
A fence tagged \`avoid\` shows what a violation looks like. It is optional, and
|
|
48
|
+
a rule can have one without the block above.
|
|
49
|
+
|
|
50
|
+
\`\`\`ts avoid
|
|
51
|
+
export const handle = (id: string) =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
return yield* CustomerStore.find(id);
|
|
54
|
+
});
|
|
55
|
+
\`\`\`
|
|
46
56
|
`;
|
|
47
57
|
|
|
48
58
|
const CONFIG_PATH = CONFIG_FILES[0];
|
package/src/markdown.ts
CHANGED
|
@@ -14,8 +14,13 @@ import { Effect, Schema } from "effect";
|
|
|
14
14
|
* const reference = "the correct code"
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* ```ts avoid
|
|
18
|
+
* const avoided = "what a violation looks like"
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The reference is the first fenced code block; `avoid` in a fence's info
|
|
22
|
+
* string marks the code to avoid instead. A rule has either or both. Without a
|
|
23
|
+
* fence, the whole body is the reference. `threshold` is optional.
|
|
19
24
|
*/
|
|
20
25
|
const FrontMatter = Schema.Struct({
|
|
21
26
|
description: Schema.String,
|
|
@@ -23,7 +28,8 @@ const FrontMatter = Schema.Struct({
|
|
|
23
28
|
});
|
|
24
29
|
|
|
25
30
|
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
26
|
-
const FENCE = /^ {0,3}(`{3,}|~{3,})[^\n]
|
|
31
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n {0,3}\1[ \t]*$/gm;
|
|
32
|
+
const AVOID = "avoid";
|
|
27
33
|
|
|
28
34
|
const unquote = (value: string): string => {
|
|
29
35
|
const trimmed = value.trim();
|
|
@@ -44,9 +50,35 @@ const parseFrontMatter = (block: string): Record<string, string> => {
|
|
|
44
50
|
return fields;
|
|
45
51
|
};
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
interface Fence {
|
|
54
|
+
readonly avoid: boolean;
|
|
55
|
+
readonly code: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fencesOf = (body: string): ReadonlyArray<Fence> =>
|
|
59
|
+
Array.from(body.matchAll(FENCE), (match) => ({
|
|
60
|
+
avoid: (match[2] ?? "")
|
|
61
|
+
.trim()
|
|
62
|
+
.split(/\s+/)
|
|
63
|
+
.some((word) => word.toLowerCase() === AVOID),
|
|
64
|
+
code: (match[3] ?? "").trim(),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
/** An empty block counts as no block. */
|
|
68
|
+
const nonEmpty = (code: string | undefined): string | undefined =>
|
|
69
|
+
code === undefined || code.length === 0 ? undefined : code;
|
|
70
|
+
|
|
71
|
+
/** The reference and the code to avoid, each left out when the body has none. */
|
|
72
|
+
const codeOf = (body: string): Pick<Rule, "reference" | "avoid"> => {
|
|
73
|
+
const fences = fencesOf(body);
|
|
74
|
+
const reference = nonEmpty(
|
|
75
|
+
fences.length === 0 ? body.trim() : fences.find((fence) => !fence.avoid)?.code,
|
|
76
|
+
);
|
|
77
|
+
const avoid = nonEmpty(fences.find((fence) => fence.avoid)?.code);
|
|
78
|
+
return {
|
|
79
|
+
...(reference === undefined ? {} : { reference }),
|
|
80
|
+
...(avoid === undefined ? {} : { avoid }),
|
|
81
|
+
};
|
|
50
82
|
};
|
|
51
83
|
|
|
52
84
|
export const parseRuleMarkdown = (
|
|
@@ -65,11 +97,11 @@ export const parseRuleMarkdown = (
|
|
|
65
97
|
ConfigUnavailable.make({ message: `${file}: ${problem.message}` }),
|
|
66
98
|
),
|
|
67
99
|
);
|
|
68
|
-
const
|
|
69
|
-
if (reference.
|
|
100
|
+
const code = codeOf(match[2]);
|
|
101
|
+
if (code.reference === undefined && code.avoid === undefined) {
|
|
70
102
|
return yield* ConfigUnavailable.make({
|
|
71
|
-
message: `${file}: the body
|
|
103
|
+
message: `${file}: the body needs reference code, a fence tagged avoid, or both`,
|
|
72
104
|
});
|
|
73
105
|
}
|
|
74
|
-
return { ...front,
|
|
106
|
+
return { ...front, ...code };
|
|
75
107
|
});
|
package/src/services/Jev.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RuleId, Rules } from "#config.ts";
|
|
1
|
+
import type { Rule, RuleId, Rules } from "#config.ts";
|
|
2
2
|
import { Context, type Effect, Record, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
export type { Rules };
|
|
@@ -36,12 +36,36 @@ export const needsBlocks = (lines: Lines): boolean =>
|
|
|
36
36
|
|
|
37
37
|
const stateOf = (lines: Lines, rules: Rules) => ({
|
|
38
38
|
code: lines.map((line, index) => `${index + 1} | ${line}`).join("\n"),
|
|
39
|
-
rules: Record.map(rules, ({ description, reference }) => ({
|
|
39
|
+
rules: Record.map(rules, ({ description, reference, avoid }) => ({
|
|
40
40
|
description,
|
|
41
|
-
reference,
|
|
41
|
+
...(reference === undefined ? {} : { reference }),
|
|
42
|
+
...(avoid === undefined ? {} : { avoid }),
|
|
42
43
|
})),
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
const field = (id: RuleId, key: "avoid" | "description" | "reference") =>
|
|
47
|
+
`state.rules["${id}"].${key}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A rule with a reference asks whether the file diverges from it, with the
|
|
51
|
+
* code to avoid as an example when there is some. A rule without one asks
|
|
52
|
+
* whether the file contains the code to avoid.
|
|
53
|
+
*/
|
|
54
|
+
const judgeInstructions = (id: RuleId, rule: Rule): string =>
|
|
55
|
+
rule.reference === undefined
|
|
56
|
+
? `Does state.code contain the pattern shown in ${field(id, "avoid")}, which ${field(id, "description")} rules out? Answer no if nothing in this file resembles it.`
|
|
57
|
+
: `Does state.code diverge from the pattern shown in ${field(id, "reference")}${
|
|
58
|
+
rule.avoid === undefined ? "" : `, for example by doing what ${field(id, "avoid")} shows`
|
|
59
|
+
}, as described by ${field(id, "description")}? Answer no if the pattern does not apply to this file.`;
|
|
60
|
+
|
|
61
|
+
/** What the offending line does, for the line and block questions. */
|
|
62
|
+
const offense = (id: RuleId, rule: Rule): string =>
|
|
63
|
+
rule.reference === undefined
|
|
64
|
+
? `shows the pattern in ${field(id, "avoid")}`
|
|
65
|
+
: rule.avoid === undefined
|
|
66
|
+
? `diverges from ${field(id, "reference")}`
|
|
67
|
+
: `diverges from ${field(id, "reference")} or resembles ${field(id, "avoid")}`;
|
|
68
|
+
|
|
45
69
|
const lineCriteria = (lines: Lines, from: number, to: number): Record<string, string> => {
|
|
46
70
|
const criteria: Record<string, string> = {};
|
|
47
71
|
for (let n = from; n <= Math.min(to, lines.length); n++) {
|
|
@@ -54,9 +78,9 @@ const lineCriteria = (lines: Lines, from: number, to: number): Record<string, st
|
|
|
54
78
|
export const judgeBody = (model: string, lines: Lines, rules: Rules) => ({
|
|
55
79
|
model,
|
|
56
80
|
state: stateOf(lines, rules),
|
|
57
|
-
questions: Record.map(rules, (
|
|
81
|
+
questions: Record.map(rules, (rule, id) => ({
|
|
58
82
|
type: "noul" as const,
|
|
59
|
-
instructions:
|
|
83
|
+
instructions: judgeInstructions(id, rule),
|
|
60
84
|
})),
|
|
61
85
|
});
|
|
62
86
|
|
|
@@ -68,11 +92,11 @@ export const locateBody = (
|
|
|
68
92
|
) => ({
|
|
69
93
|
model,
|
|
70
94
|
state: stateOf(lines, rules),
|
|
71
|
-
questions: Record.map(rules, (
|
|
95
|
+
questions: Record.map(rules, (rule, id) => {
|
|
72
96
|
const block = blocks?.[id];
|
|
73
97
|
return {
|
|
74
98
|
type: "choice" as const,
|
|
75
|
-
instructions: `Which line of state.code most clearly
|
|
99
|
+
instructions: `Which line of state.code most clearly ${offense(id, rule)}?`,
|
|
76
100
|
criteria:
|
|
77
101
|
block === undefined
|
|
78
102
|
? lineCriteria(lines, 1, lines.length)
|
|
@@ -92,9 +116,9 @@ export const blockBody = (model: string, lines: Lines, rules: Rules) => {
|
|
|
92
116
|
return {
|
|
93
117
|
model,
|
|
94
118
|
state: stateOf(lines, rules),
|
|
95
|
-
questions: Record.map(rules, (
|
|
119
|
+
questions: Record.map(rules, (rule, id) => ({
|
|
96
120
|
type: "choice" as const,
|
|
97
|
-
instructions: `Which block of state.code contains the line that most clearly
|
|
121
|
+
instructions: `Which block of state.code contains the line that most clearly ${offense(id, rule)}?`,
|
|
98
122
|
criteria,
|
|
99
123
|
})),
|
|
100
124
|
};
|
package/src/workflows/audit.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { type Crypto, Effect, Record } from "effect";
|
|
|
10
10
|
export interface Finding {
|
|
11
11
|
readonly rule: RuleId;
|
|
12
12
|
readonly description: string;
|
|
13
|
-
readonly reference
|
|
13
|
+
readonly reference?: string;
|
|
14
|
+
readonly avoid?: string;
|
|
14
15
|
readonly file: string;
|
|
15
16
|
readonly line: number;
|
|
16
17
|
readonly snippet: string;
|
|
@@ -43,6 +44,19 @@ const fileResult = (
|
|
|
43
44
|
|
|
44
45
|
const isEmpty = Record.isEmptyReadonlyRecord;
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* What a judgment depends on besides the file. A rule without code to avoid
|
|
49
|
+
* hashes what every rule hashed before `avoid` existed, so its cached
|
|
50
|
+
* judgments survive the upgrade.
|
|
51
|
+
*/
|
|
52
|
+
const fingerprintOf = (model: string, rule: Rule) =>
|
|
53
|
+
sha256(
|
|
54
|
+
model +
|
|
55
|
+
rule.description +
|
|
56
|
+
(rule.reference ?? "") +
|
|
57
|
+
(rule.avoid === undefined ? "" : `\u0000avoid\u0000${rule.avoid}`),
|
|
58
|
+
);
|
|
59
|
+
|
|
46
60
|
export const runAudit: Effect.Effect<
|
|
47
61
|
AuditResult,
|
|
48
62
|
WalkUnavailable | JevUnavailable,
|
|
@@ -66,7 +80,7 @@ export const runAudit: Effect.Effect<
|
|
|
66
80
|
Object.entries(configuredRules(file.path)),
|
|
67
81
|
([id, rule]) =>
|
|
68
82
|
Effect.map(
|
|
69
|
-
|
|
83
|
+
fingerprintOf(config.model, rule),
|
|
70
84
|
(fingerprint) =>
|
|
71
85
|
[
|
|
72
86
|
id,
|
|
@@ -116,6 +130,7 @@ export const runAudit: Effect.Effect<
|
|
|
116
130
|
rule: id,
|
|
117
131
|
description: k.rule.description,
|
|
118
132
|
reference: k.rule.reference,
|
|
133
|
+
avoid: k.rule.avoid,
|
|
119
134
|
file: file.path,
|
|
120
135
|
line: judgment.line,
|
|
121
136
|
snippet: judgment.snippet ?? "",
|
package/src/workflows/format.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface RenderOptions {
|
|
|
24
24
|
|
|
25
25
|
const isBlank = (line: string): boolean => line.trim().length === 0;
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
const lines =
|
|
27
|
+
const codeLines = (code: string): ReadonlyArray<string> => {
|
|
28
|
+
const lines = code.split("\n");
|
|
29
29
|
const first = lines.findIndex((line) => !isBlank(line));
|
|
30
30
|
if (first < 0) return [];
|
|
31
31
|
let last = lines.length - 1;
|
|
@@ -36,12 +36,16 @@ const referenceLines = (reference: string): ReadonlyArray<string> => {
|
|
|
36
36
|
/**
|
|
37
37
|
* One diagnostic, in the frame `vp lint` prints on a terminal: a red header,
|
|
38
38
|
* the offending line, a pink underline, and the reference code as the hint.
|
|
39
|
+
* A rule without a reference shows its code to avoid instead, labeled so.
|
|
39
40
|
*/
|
|
40
41
|
const frame = (finding: Finding, options: RenderOptions): ReadonlyArray<string> => {
|
|
41
42
|
const color = options.color === true;
|
|
42
43
|
const digits = String(finding.line);
|
|
43
44
|
const gutter = " ".repeat(digits.length + 2);
|
|
44
|
-
const [
|
|
45
|
+
const [label, code] =
|
|
46
|
+
finding.reference === undefined ? ["avoid", finding.avoid ?? ""] : ["hint", finding.reference];
|
|
47
|
+
const prefix = ` ${label}: `;
|
|
48
|
+
const [hint = "", ...rest] = codeLines(code);
|
|
45
49
|
const header = `${finding.rule} (${finding.probability.toFixed(2)})`;
|
|
46
50
|
return [
|
|
47
51
|
` ${red("×", color)} ${red(header, color)}: ${red(finding.description, color)}`,
|
|
@@ -49,8 +53,8 @@ const frame = (finding: Finding, options: RenderOptions): ReadonlyArray<string>
|
|
|
49
53
|
` ${dim(digits, color)} │ ${finding.snippet}`,
|
|
50
54
|
`${gutter}· ${pink("─".repeat(Math.max(1, finding.snippet.trim().length)), color)}`,
|
|
51
55
|
`${gutter}╰────`,
|
|
52
|
-
`${helpTint(
|
|
53
|
-
...rest.map((line) => (isBlank(line) ? "" :
|
|
56
|
+
`${helpTint(prefix, color)}${hint}`,
|
|
57
|
+
...rest.map((line) => (isBlank(line) ? "" : `${" ".repeat(prefix.length)}${line}`)),
|
|
54
58
|
];
|
|
55
59
|
};
|
|
56
60
|
|