@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 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.1",
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.1",
71
- "@drkmttr/adhere-darwin-x64": "0.7.1",
72
- "@drkmttr/adhere-linux-arm64": "0.7.1",
73
- "@drkmttr/adhere-linux-x64": "0.7.1",
74
- "@drkmttr/adhere-win32-x64": "0.7.1"
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
  }
@@ -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
- : Effect.fail(refused(`Jev answered HTTP ${response.status}${detailOf(body)}`)),
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
- /** Comparing rules has no file to skip, so a request over Jev's context refuses instead. */
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
- Effect.catchTag(self, "JevOverflow", () =>
78
- Effect.fail(refused("Comparing the rules took a request over Jev's context")),
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)(
@@ -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
@@ -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
- const lines = isEmpty(flagged)
261
- ? {}
262
- : yield* jev.locate(file.lines, rulesOf(flagged)).pipe(inFile(file));
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: fileResult(cached ? (deferred > 0 ? "waiting" : "cached") : "judged", findings),
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
  });
@@ -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 = "error" | "path" | "lineNumber" | "underline" | "label" | Kind;
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 match `vp lint`: truecolor
20
- * (`38;2;R;G;B`), bold with a trailing `;1`, and a dim line number. The code's
21
- * are the terminal's own colors (`3N`), so the user's theme picks shades that
22
- * read on its background; none is magenta, which would run into the label.
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;2;219;91;81;1",
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;2;180;105;245",
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(`${finding.rule} (${finding.probability.toFixed(2)})`, "error"),
81
- span(": "),
82
- span(finding.description, "error"),
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 red header,
113
- * the offending line, a pink underline, and the code to write as the hint.
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
  /**