@drkmttr/adhere 0.7.1 → 0.7.3
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/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +7 -6
- package/src/services/Jev.http.ts +26 -6
- package/src/services/Jev.ts +12 -2
- package/src/workflows/audit.ts +42 -5
- package/src/workflows/format.ts +51 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darkmatter Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -107,6 +107,13 @@ the report goes to stdout. A request Jev refuses stops the run with the file,
|
|
|
107
107
|
the HTTP status, and Jev's reason. Files finished before it are cached, so
|
|
108
108
|
running again continues from there.
|
|
109
109
|
|
|
110
|
+
The firewall in front of Jev's API can refuse a request whose code reads to
|
|
111
|
+
it as an attack, with a 403 and an HTML page rather than Jev's JSON. It refuses
|
|
112
|
+
the same request every run, so that file is skipped, the run goes on, and the
|
|
113
|
+
report lists each such file with Cloudflare's Ray ID, which TypeSafe AI can
|
|
114
|
+
look the block up by. When only the question that finds the line is refused,
|
|
115
|
+
the file's judgments are kept, and a rerun sends that question alone.
|
|
116
|
+
|
|
110
117
|
### Limiting a run
|
|
111
118
|
|
|
112
119
|
Three flags bound what a run does. `--limit <checks>` judges at most that many
|
|
@@ -431,3 +438,7 @@ runs. The publish workflow can be run manually for an already-created release
|
|
|
431
438
|
tag if a publish needs to be retried; it skips every package whose version is
|
|
432
439
|
already on npm. The main package's `files` list keeps it to `bin/`, `src/`,
|
|
433
440
|
`presets/`, and `skills/`.
|
|
441
|
+
|
|
442
|
+
## License
|
|
443
|
+
|
|
444
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/darkmatter/adhere.git"
|
|
@@ -67,10 +68,10 @@
|
|
|
67
68
|
}
|
|
68
69
|
},
|
|
69
70
|
"optionalDependencies": {
|
|
70
|
-
"@drkmttr/adhere-darwin-arm64": "0.7.
|
|
71
|
-
"@drkmttr/adhere-darwin-x64": "0.7.
|
|
72
|
-
"@drkmttr/adhere-linux-arm64": "0.7.
|
|
73
|
-
"@drkmttr/adhere-linux-x64": "0.7.
|
|
74
|
-
"@drkmttr/adhere-win32-x64": "0.7.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.7.3",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.7.3",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.7.3",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.7.3",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.7.3"
|
|
75
76
|
}
|
|
76
77
|
}
|
package/src/services/Jev.http.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
conflictBody,
|
|
8
8
|
contradictBody,
|
|
9
9
|
Jev,
|
|
10
|
+
JevBlocked,
|
|
10
11
|
JevOverflow,
|
|
11
12
|
JevUnavailable,
|
|
12
13
|
judgeBody,
|
|
@@ -48,6 +49,11 @@ const Overflowed = Schema.fromJsonString(
|
|
|
48
49
|
const isOverflow = (body: string): boolean =>
|
|
49
50
|
Option.isSome(Schema.decodeUnknownOption(Overflowed)(body));
|
|
50
51
|
|
|
52
|
+
/** A firewall's block page: HTML where Jev answers JSON, even for its own errors. */
|
|
53
|
+
const isBlockPage = (response: HttpClientResponse.HttpClientResponse, body: string): boolean =>
|
|
54
|
+
(response.headers["content-type"] ?? "").includes("text/html") ||
|
|
55
|
+
body.trimStart().startsWith("<");
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* A request that failed. A 400 saying the request is over Jev's context is a
|
|
53
59
|
* `JevOverflow`, which skips the file. Any other answer outside 2xx refuses
|
|
@@ -63,19 +69,33 @@ const failed = (
|
|
|
63
69
|
const { response } = problem.reason;
|
|
64
70
|
return Effect.flatMap(
|
|
65
71
|
Effect.orElseSucceed(response.text, () => ""),
|
|
66
|
-
(body): Effect.Effect<never, JevOverflow | JevUnavailable> =>
|
|
72
|
+
(body): Effect.Effect<never, JevOverflow | JevBlocked | JevUnavailable> =>
|
|
67
73
|
response.status === 400 && isOverflow(body)
|
|
68
74
|
? Effect.fail(JevOverflow.make())
|
|
69
|
-
:
|
|
75
|
+
: response.status === 403 && isBlockPage(response, body)
|
|
76
|
+
? Effect.fail(JevBlocked.make({ ray: response.headers["cf-ray"] ?? "none given" }))
|
|
77
|
+
: Effect.fail(refused(`Jev answered HTTP ${response.status}${detailOf(body)}`)),
|
|
70
78
|
);
|
|
71
79
|
};
|
|
72
80
|
|
|
73
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Comparing rules has no file to skip, so a request over Jev's context, or one
|
|
83
|
+
* the firewall blocks, refuses instead.
|
|
84
|
+
*/
|
|
74
85
|
const refuseOverflow = <A>(
|
|
75
|
-
self: Effect.Effect<A, JevOverflow | JevUnavailable>,
|
|
86
|
+
self: Effect.Effect<A, JevOverflow | JevBlocked | JevUnavailable>,
|
|
76
87
|
): Effect.Effect<A, JevUnavailable> =>
|
|
77
|
-
|
|
78
|
-
Effect.
|
|
88
|
+
self.pipe(
|
|
89
|
+
Effect.catchTag("JevOverflow", () =>
|
|
90
|
+
Effect.fail(refused("Comparing the rules took a request over Jev's context")),
|
|
91
|
+
),
|
|
92
|
+
Effect.catchTag("JevBlocked", ({ ray }) =>
|
|
93
|
+
Effect.fail(
|
|
94
|
+
refused(
|
|
95
|
+
`The firewall in front of Jev's API blocked comparing the rules (Cloudflare Ray ID ${ray})`,
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
79
99
|
);
|
|
80
100
|
|
|
81
101
|
export const JevLive = Layer.effect(Jev)(
|
package/src/services/Jev.ts
CHANGED
|
@@ -14,17 +14,27 @@ export class JevUnavailable extends Schema.TaggedError<JevUnavailable>()("JevUna
|
|
|
14
14
|
*/
|
|
15
15
|
export class JevOverflow extends Schema.TaggedError<JevOverflow>()("JevOverflow", {}) {}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A request the firewall in front of Jev's API refused: a 403 with an HTML
|
|
19
|
+
* block page where Jev answers JSON. It reads some code as an attack, so the
|
|
20
|
+
* same request is refused every time while the rest of a run goes through.
|
|
21
|
+
* `ray` is Cloudflare's Ray ID, which TypeSafe AI can look the block up by.
|
|
22
|
+
*/
|
|
23
|
+
export class JevBlocked extends Schema.TaggedError<JevBlocked>()("JevBlocked", {
|
|
24
|
+
ray: Schema.String,
|
|
25
|
+
}) {}
|
|
26
|
+
|
|
17
27
|
export class Jev extends Context.Service<
|
|
18
28
|
Jev,
|
|
19
29
|
{
|
|
20
30
|
readonly judge: (
|
|
21
31
|
lines: Lines,
|
|
22
32
|
rules: Rules,
|
|
23
|
-
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow>;
|
|
33
|
+
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow | JevBlocked>;
|
|
24
34
|
readonly locate: (
|
|
25
35
|
lines: Lines,
|
|
26
36
|
rules: Rules,
|
|
27
|
-
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow>;
|
|
37
|
+
) => Effect.Effect<Record<RuleId, number>, JevUnavailable | JevOverflow | JevBlocked>;
|
|
28
38
|
/**
|
|
29
39
|
* Per rule, the partner Jev names as impossible to follow in the same code,
|
|
30
40
|
* as `[rule, partner]` indexes into `rules`. A rule Jev names none for is
|
package/src/workflows/audit.ts
CHANGED
|
@@ -32,12 +32,21 @@ export interface AuditResult {
|
|
|
32
32
|
readonly skipped: number;
|
|
33
33
|
/** Files whose checks `--limit` left for a later run, none judged in this one. */
|
|
34
34
|
readonly waiting: number;
|
|
35
|
+
/** Files the firewall in front of Jev's API refused, with the ID to report each by. */
|
|
36
|
+
readonly blocked: ReadonlyArray<Blocked>;
|
|
35
37
|
readonly findings: ReadonlyArray<Finding>;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** A file whose request the firewall refused, and Cloudflare's Ray ID for the refusal. */
|
|
41
|
+
export interface Blocked {
|
|
42
|
+
readonly file: string;
|
|
43
|
+
readonly ray: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
interface FileResult {
|
|
39
|
-
readonly status: "judged" | "cached" | "skipped" | "waiting";
|
|
47
|
+
readonly status: "judged" | "cached" | "skipped" | "waiting" | "blocked";
|
|
40
48
|
readonly findings: ReadonlyArray<Finding>;
|
|
49
|
+
readonly blocked?: Blocked;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface PreparedRule {
|
|
@@ -257,9 +266,22 @@ export const executeAudit = (
|
|
|
257
266
|
judgment.line === undefined
|
|
258
267
|
);
|
|
259
268
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
// A blocked line question still leaves the judgments worth caching: a rerun
|
|
270
|
+
// asks only for the lines again, not for every rule.
|
|
271
|
+
const {
|
|
272
|
+
lines,
|
|
273
|
+
blocked,
|
|
274
|
+
}: { readonly lines: Readonly<Record<RuleId, number>>; readonly blocked?: Blocked } = isEmpty(
|
|
275
|
+
flagged,
|
|
276
|
+
)
|
|
277
|
+
? { lines: {}, blocked: undefined }
|
|
278
|
+
: yield* jev.locate(file.lines, rulesOf(flagged)).pipe(
|
|
279
|
+
Effect.map((lines) => ({ lines, blocked: undefined })),
|
|
280
|
+
Effect.catchTag("JevBlocked", ({ ray }) =>
|
|
281
|
+
Effect.succeed({ lines: {}, blocked: { file: file.path, ray } }),
|
|
282
|
+
),
|
|
283
|
+
inFile(file),
|
|
284
|
+
);
|
|
263
285
|
const located = Record.map(judged, (judgment, id) => {
|
|
264
286
|
const line = lines[id];
|
|
265
287
|
return line === undefined
|
|
@@ -291,7 +313,10 @@ export const executeAudit = (
|
|
|
291
313
|
})
|
|
292
314
|
.sort((a, b) => a.line - b.line);
|
|
293
315
|
return {
|
|
294
|
-
result:
|
|
316
|
+
result:
|
|
317
|
+
blocked === undefined
|
|
318
|
+
? fileResult(cached ? (deferred > 0 ? "waiting" : "cached") : "judged", findings)
|
|
319
|
+
: { status: "blocked", findings, blocked },
|
|
295
320
|
requests:
|
|
296
321
|
(isEmpty(pending) ? 0 : judgeRequests(file.lines, rulesOf(pending))) +
|
|
297
322
|
(isEmpty(flagged) ? 0 : locateRequests(file.lines, rulesOf(flagged))),
|
|
@@ -309,6 +334,17 @@ export const executeAudit = (
|
|
|
309
334
|
requests: judgeRequests(plan.file.lines, rulesOf(plan.pending)),
|
|
310
335
|
}),
|
|
311
336
|
),
|
|
337
|
+
// The firewall refuses this file's code every time; the rest of the run goes on.
|
|
338
|
+
Effect.catchTag("JevBlocked", ({ ray }) =>
|
|
339
|
+
Effect.succeed({
|
|
340
|
+
result: {
|
|
341
|
+
status: "blocked",
|
|
342
|
+
findings: [],
|
|
343
|
+
blocked: { file: plan.file.path, ray },
|
|
344
|
+
} satisfies FileResult,
|
|
345
|
+
requests: judgeRequests(plan.file.lines, rulesOf(plan.pending)),
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
312
348
|
);
|
|
313
349
|
yield* progress({ requests, findings: result.findings.length });
|
|
314
350
|
return result;
|
|
@@ -323,6 +359,7 @@ export const executeAudit = (
|
|
|
323
359
|
cached: count("cached"),
|
|
324
360
|
skipped: count("skipped"),
|
|
325
361
|
waiting: count("waiting"),
|
|
362
|
+
blocked: results.flatMap((result) => (result.blocked === undefined ? [] : [result.blocked])),
|
|
326
363
|
findings: results.flatMap((result) => result.findings),
|
|
327
364
|
};
|
|
328
365
|
});
|
package/src/workflows/format.ts
CHANGED
|
@@ -6,7 +6,15 @@ import type { AuditPlan, AuditResult, FileDone, Finding } from "#workflows/audit
|
|
|
6
6
|
* token of code, named by its role. Layout measures text; only `serialize`
|
|
7
7
|
* turns a role into color, so no width or blank line ever sees an escape code.
|
|
8
8
|
*/
|
|
9
|
-
type Role =
|
|
9
|
+
type Role =
|
|
10
|
+
| "error"
|
|
11
|
+
| "probability"
|
|
12
|
+
| "description"
|
|
13
|
+
| "path"
|
|
14
|
+
| "lineNumber"
|
|
15
|
+
| "underline"
|
|
16
|
+
| "label"
|
|
17
|
+
| Kind;
|
|
10
18
|
|
|
11
19
|
interface Span {
|
|
12
20
|
readonly text: string;
|
|
@@ -16,17 +24,22 @@ interface Span {
|
|
|
16
24
|
type Line = ReadonlyArray<Span>;
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
|
-
* SGR parameters per role. The frame's
|
|
20
|
-
* (`38;2;R;G;B`), bold with a trailing `;1`,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
27
|
+
* SGR parameters per role. The frame's are set colors, from the 256-color
|
|
28
|
+
* palette (`38;5;N`) or truecolor (`38;2;R;G;B`), bold with a trailing `;1`,
|
|
29
|
+
* and a dim line number. Of the header, only the `×` and the rule are red,
|
|
30
|
+
* since a whole line of red is hard to read: the probability is an accent, and
|
|
31
|
+
* the description a soft white. The code's are the terminal's own colors
|
|
32
|
+
* (`3N`), so the user's theme picks shades that read on its background; none
|
|
33
|
+
* is magenta, which would run into the label.
|
|
23
34
|
*/
|
|
24
35
|
const THEME: Readonly<Record<Role, string>> = {
|
|
25
|
-
error: "38;
|
|
36
|
+
error: "38;5;197;1",
|
|
37
|
+
probability: "38;2;250;179;135",
|
|
38
|
+
description: "38;2;242;205;205",
|
|
26
39
|
path: "38;2;5;125;160;1",
|
|
27
40
|
lineNumber: "2",
|
|
28
41
|
underline: "38;2;255;0;175",
|
|
29
|
-
label: "38;
|
|
42
|
+
label: "38;5;212",
|
|
30
43
|
comment: "2",
|
|
31
44
|
string: "32",
|
|
32
45
|
constant: "33",
|
|
@@ -77,9 +90,11 @@ const header = (finding: Finding): Line => [
|
|
|
77
90
|
span(" "),
|
|
78
91
|
span("×", "error"),
|
|
79
92
|
span(" "),
|
|
80
|
-
span(
|
|
81
|
-
span("
|
|
82
|
-
span(finding.
|
|
93
|
+
span(finding.rule, "error"),
|
|
94
|
+
span(" ("),
|
|
95
|
+
span(finding.probability.toFixed(2), "probability"),
|
|
96
|
+
span("): "),
|
|
97
|
+
span(finding.description, "description"),
|
|
83
98
|
];
|
|
84
99
|
|
|
85
100
|
/** The path, the offending line numbered in the gutter, and an underline as long as the line. */
|
|
@@ -109,8 +124,9 @@ const hint = (finding: Finding): ReadonlyArray<Line> => {
|
|
|
109
124
|
};
|
|
110
125
|
|
|
111
126
|
/**
|
|
112
|
-
* One diagnostic, in the frame `vp lint` prints on a terminal: a
|
|
113
|
-
* the offending line, a pink underline, and the code to write
|
|
127
|
+
* One diagnostic, in the frame `vp lint` prints on a terminal: a header with
|
|
128
|
+
* the rule in red, the offending line, a pink underline, and the code to write
|
|
129
|
+
* as the hint.
|
|
114
130
|
*/
|
|
115
131
|
const frame = (finding: Finding, root: string | undefined): ReadonlyArray<Line> => [
|
|
116
132
|
header(finding),
|
|
@@ -125,6 +141,7 @@ const summary = (result: AuditResult): ReadonlyArray<Line> => {
|
|
|
125
141
|
`${result.cached} cached`,
|
|
126
142
|
...(result.skipped > 0 ? [`${result.skipped} skipped`] : []),
|
|
127
143
|
...(result.waiting > 0 ? [`${result.waiting} waiting`] : []),
|
|
144
|
+
...(result.blocked.length > 0 ? [`${result.blocked.length} blocked`] : []),
|
|
128
145
|
];
|
|
129
146
|
return [
|
|
130
147
|
[span(`Found ${counted(result.findings.length, "error", "errors")}.`)],
|
|
@@ -132,6 +149,27 @@ const summary = (result: AuditResult): ReadonlyArray<Line> => {
|
|
|
132
149
|
];
|
|
133
150
|
};
|
|
134
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The files the firewall in front of Jev's API refused, each with the Ray ID
|
|
154
|
+
* TypeSafe AI can look the block up by, so a gap in the audit says where it is.
|
|
155
|
+
*/
|
|
156
|
+
const blockedFiles = (result: AuditResult, root: string | undefined): ReadonlyArray<Line> =>
|
|
157
|
+
result.blocked.length === 0
|
|
158
|
+
? []
|
|
159
|
+
: [
|
|
160
|
+
[],
|
|
161
|
+
[
|
|
162
|
+
span(
|
|
163
|
+
`The firewall in front of Jev's API blocked ${counted(result.blocked.length, "file", "files")}, so ${result.blocked.length === 1 ? "it is" : "they are"} not judged in full. TypeSafe AI can look each block up by its Ray ID:`,
|
|
164
|
+
),
|
|
165
|
+
],
|
|
166
|
+
...result.blocked.map(({ file, ray }) => [
|
|
167
|
+
span(" "),
|
|
168
|
+
span(displayPath(file, root), "path"),
|
|
169
|
+
span(` (Ray ID ${ray})`),
|
|
170
|
+
]),
|
|
171
|
+
];
|
|
172
|
+
|
|
135
173
|
export interface RenderOptions {
|
|
136
174
|
/** Color the frame the way `vp lint` does on a terminal, and highlight the code. */
|
|
137
175
|
readonly color?: boolean;
|
|
@@ -147,6 +185,7 @@ export const render = (result: AuditResult, options: RenderOptions = {}): Readon
|
|
|
147
185
|
[
|
|
148
186
|
...result.findings.flatMap((finding) => [...frame(finding, options.root), []]),
|
|
149
187
|
...summary(result),
|
|
188
|
+
...blockedFiles(result, options.root),
|
|
150
189
|
].map((line) => serialize(line, options.color === true));
|
|
151
190
|
|
|
152
191
|
/**
|