@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/workflows/format.ts
CHANGED
|
@@ -288,9 +288,10 @@ const legend = (
|
|
|
288
288
|
result: AuditResult,
|
|
289
289
|
threshold: number,
|
|
290
290
|
sufficiencyThreshold: number,
|
|
291
|
+
answerer = "Jev",
|
|
291
292
|
): ReadonlyArray<Line> => {
|
|
292
293
|
const rows = [
|
|
293
|
-
["confidence", cellsOf(threshold),
|
|
294
|
+
["confidence", cellsOf(threshold), `${answerer}'s probability that the file breaks the rule`],
|
|
294
295
|
[
|
|
295
296
|
"context",
|
|
296
297
|
cellsOf(sufficiencyThreshold),
|
|
@@ -370,6 +371,8 @@ export interface RenderOptions {
|
|
|
370
371
|
readonly threshold?: number;
|
|
371
372
|
/** Below this, a finding's context is low and it carries a warning. Defaults to 0.6. */
|
|
372
373
|
readonly sufficiencyThreshold?: number;
|
|
374
|
+
/** The model of the config's provider, which the legend names in place of Jev. */
|
|
375
|
+
readonly provider?: string;
|
|
373
376
|
}
|
|
374
377
|
|
|
375
378
|
/**
|
|
@@ -384,7 +387,12 @@ export const render = (result: AuditResult, options: RenderOptions = {}): Readon
|
|
|
384
387
|
...frame(finding, options.root, sufficiencyThreshold),
|
|
385
388
|
[],
|
|
386
389
|
]),
|
|
387
|
-
...legend(
|
|
390
|
+
...legend(
|
|
391
|
+
result,
|
|
392
|
+
options.threshold ?? DEFAULT_THRESHOLD,
|
|
393
|
+
sufficiencyThreshold,
|
|
394
|
+
options.provider,
|
|
395
|
+
),
|
|
388
396
|
...summary(result),
|
|
389
397
|
...blockedFiles(result, options.root),
|
|
390
398
|
].map((line) => serialize(line, options.color === true));
|
|
@@ -444,51 +452,100 @@ const costLine = (plan: AuditPlan): string => {
|
|
|
444
452
|
return `Those carry about ${tokensSaid(plan.tokens)} input tokens${priced}, and more for locating findings.`;
|
|
445
453
|
};
|
|
446
454
|
|
|
455
|
+
/** Who answers the plan's requests: Jev, or the model of the config's provider. */
|
|
456
|
+
const answererOf = (plan: AuditPlan): string => plan.provider ?? "Jev";
|
|
457
|
+
|
|
458
|
+
const NATIVE_READS_CAVEAT =
|
|
459
|
+
"Estimates cover only code and workspace-package context; symbol/reference input, possible extra request splits, and size-skips are determined at send time.";
|
|
460
|
+
|
|
447
461
|
export const describePlan = (plan: AuditPlan, limits: RunLimits = {}): ReadonlyArray<string> => {
|
|
462
|
+
const native = plan.nativeReads === undefined ? [] : [NATIVE_READS_CAVEAT];
|
|
463
|
+
const locating = plan.cachedLocateRequests ?? 0;
|
|
448
464
|
const cached = plan.cached > 0 ? `, ${plan.cached} cached` : "";
|
|
449
465
|
const skipped =
|
|
450
466
|
plan.skipped > 0 ? `, ${counted(plan.skipped, "file", "files")} too long to judge` : "";
|
|
451
467
|
const matching = (limits.filter?.length ?? 0) > 0 ? " matching the filter" : "";
|
|
452
468
|
const found = `${counted(plan.files.length, "file", "files")}${matching} and ${counted(plan.rules, "rule", "rules")}: ${counted(plan.checks, "check", "checks")}${cached}${skipped}.`;
|
|
453
|
-
const pending = plan.checks - plan.cached;
|
|
454
469
|
const waiting =
|
|
455
470
|
plan.deferred > 0
|
|
456
|
-
? `
|
|
471
|
+
? ` ${counted(plan.deferred, "file", "files")} ${plan.deferred === 1 ? "waits" : "wait"} for a later run.`
|
|
457
472
|
: "";
|
|
473
|
+
if (plan.planOnly === true) {
|
|
474
|
+
return [
|
|
475
|
+
found,
|
|
476
|
+
"Plan only (--limit 0): no native reads will be resolved and no requests will be sent.",
|
|
477
|
+
...(plan.deferred > 0
|
|
478
|
+
? [`${counted(plan.deferred, "file", "files")} left for a later run.`]
|
|
479
|
+
: []),
|
|
480
|
+
...native,
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
if (plan.requests === 0 && locating > 0) {
|
|
484
|
+
const pace =
|
|
485
|
+
limits.rpm === undefined
|
|
486
|
+
? ""
|
|
487
|
+
: ` At ${limits.rpm} a minute, they take about ${durationAt(locating, limits.rpm)}.`;
|
|
488
|
+
return [
|
|
489
|
+
found,
|
|
490
|
+
`Locating cached findings takes an estimated ${counted(locating, "request", "requests")} to ${answererOf(plan)}.${pace}${waiting}`,
|
|
491
|
+
"Locating input tokens and cost are not estimated.",
|
|
492
|
+
...native,
|
|
493
|
+
];
|
|
494
|
+
}
|
|
458
495
|
if (plan.requests === 0) {
|
|
459
496
|
const none =
|
|
460
497
|
plan.deferred > 0
|
|
461
498
|
? plan.deferred === 1
|
|
462
|
-
? "The limit leaves
|
|
463
|
-
: `The limit leaves all ${plan.deferred}
|
|
499
|
+
? "The limit leaves 1 file needing requests for a later run."
|
|
500
|
+
: `The limit leaves all ${plan.deferred} files needing requests for a later run.`
|
|
464
501
|
: plan.checks === 0
|
|
465
502
|
? "Nothing to judge."
|
|
466
503
|
: "The cache answers every check.";
|
|
467
|
-
return [found, none];
|
|
504
|
+
return [found, none, ...native];
|
|
468
505
|
}
|
|
469
|
-
const judging =
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
: plan.cached > 0
|
|
474
|
-
? `the other ${pending}`
|
|
475
|
-
: "them";
|
|
506
|
+
const judging = plan.files.filter(
|
|
507
|
+
(file) => !file.skipped && !file.deferred && Object.keys(file.pending).length > 0,
|
|
508
|
+
).length;
|
|
509
|
+
const rest = counted(judging, "file", "files");
|
|
476
510
|
const pace =
|
|
477
511
|
limits.rpm === undefined
|
|
478
512
|
? ""
|
|
479
|
-
: ` At ${limits.rpm} a minute, they take about ${durationAt(plan.requests, limits.rpm)}.`;
|
|
513
|
+
: ` At ${limits.rpm} a minute, they take about ${durationAt(plan.requests + locating, limits.rpm)}.`;
|
|
480
514
|
return [
|
|
481
515
|
found,
|
|
482
|
-
`Judging ${rest} takes ${counted(plan.requests, "request", "requests")} to
|
|
516
|
+
`Judging ${rest} takes ${plan.nativeReads === undefined ? "" : "an estimated "}${counted(plan.requests, "request", "requests")} to ${answererOf(plan)}, plus 1 or more for each file with a finding.${pace}${waiting}`,
|
|
483
517
|
...(plan.tokens > 0 ? [costLine(plan)] : []),
|
|
518
|
+
...(locating > 0
|
|
519
|
+
? [
|
|
520
|
+
`Known cached findings need an estimated ${counted(locating, "request", "requests")} to locate; locating input tokens and cost are not included.`,
|
|
521
|
+
]
|
|
522
|
+
: []),
|
|
523
|
+
...native,
|
|
484
524
|
];
|
|
485
525
|
};
|
|
486
526
|
|
|
487
527
|
/** The question before a run sends anything. */
|
|
488
528
|
export const sendQuestion = (plan: AuditPlan): string => {
|
|
529
|
+
if (plan.planOnly === true) {
|
|
530
|
+
return "Plan only (--limit 0): no native reads will be resolved and no requests will be sent.";
|
|
531
|
+
}
|
|
489
532
|
const cost = costOf(plan);
|
|
490
533
|
const priced = cost === undefined || plan.tokens === 0 ? "" : `, ${dollars(cost)}`;
|
|
491
|
-
|
|
534
|
+
const estimate = plan.nativeReads === undefined ? "" : "an estimated ";
|
|
535
|
+
const caveat = plan.nativeReads === undefined ? "" : `${NATIVE_READS_CAVEAT} `;
|
|
536
|
+
const locating = plan.cachedLocateRequests ?? 0;
|
|
537
|
+
if (locating > 0) {
|
|
538
|
+
const work =
|
|
539
|
+
plan.requests === 0
|
|
540
|
+
? "to locate cached findings"
|
|
541
|
+
: `(${counted(plan.requests, "request", "requests")} judging, ${counted(locating, "request", "requests")} locating cached findings)`;
|
|
542
|
+
const costs =
|
|
543
|
+
plan.requests === 0 || cost === undefined || plan.tokens === 0
|
|
544
|
+
? "Locating input tokens and cost are not estimated. "
|
|
545
|
+
: `Judging costs ${dollars(cost)}; locating input tokens and cost are not estimated. `;
|
|
546
|
+
return `${caveat}${costs}Send an estimated ${counted(plan.requests + locating, "request", "requests")} to ${answererOf(plan)} ${work}?`;
|
|
547
|
+
}
|
|
548
|
+
return `${caveat}Send ${estimate}${counted(plan.requests, "request", "requests")} to ${answererOf(plan)}${priced}?`;
|
|
492
549
|
};
|
|
493
550
|
|
|
494
551
|
/** A run's progress so far: every file it finished, with the requests and findings they added up to. */
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { workspacePackagesIn } from "./workspaces.ts";
|
|
3
|
+
|
|
4
|
+
const catalog = {
|
|
5
|
+
"@repo/foo": "packages/foo",
|
|
6
|
+
"@repo/types": "packages/types",
|
|
7
|
+
"@repo/reexport": "packages/reexport",
|
|
8
|
+
"@repo/dynamic": "packages/dynamic",
|
|
9
|
+
legacy: "packages/legacy",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("file-local workspace packages", () => {
|
|
13
|
+
it("keeps static and type imports and re-exports under canonical package names", () => {
|
|
14
|
+
const code = `
|
|
15
|
+
import Foo, { value } from "@repo/foo/utils";
|
|
16
|
+
import type { Model } from "@repo/types";
|
|
17
|
+
import type DefaultModel from "@repo/types/model";
|
|
18
|
+
import { type OtherModel } from "@repo/types/other";
|
|
19
|
+
import * as Types from "@repo/types";
|
|
20
|
+
import "legacy/setup";
|
|
21
|
+
export { value as renamed } from "@repo/reexport/value";
|
|
22
|
+
export type { Shape } from "@repo/reexport/types";
|
|
23
|
+
export type * from "@repo/reexport";
|
|
24
|
+
export * as all from "@repo/reexport/all";
|
|
25
|
+
import Missing from "@repo/foobar";
|
|
26
|
+
import External from "external";
|
|
27
|
+
import Relative from "../@repo/foo";
|
|
28
|
+
`;
|
|
29
|
+
expect(workspacePackagesIn(code, catalog)).toEqual({
|
|
30
|
+
"@repo/foo": "packages/foo",
|
|
31
|
+
"@repo/types": "packages/types",
|
|
32
|
+
"@repo/reexport": "packages/reexport",
|
|
33
|
+
legacy: "packages/legacy",
|
|
34
|
+
});
|
|
35
|
+
expect(workspacePackagesIn("export const value = 1;", catalog)).toEqual({});
|
|
36
|
+
expect(catalog).toHaveProperty("@repo/dynamic");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("ignores import text in comments, strings, regexes, and templates, and computed modules", () => {
|
|
40
|
+
const code = `
|
|
41
|
+
// import Foo from "@repo/foo";
|
|
42
|
+
/* export * from "@repo/reexport"; */
|
|
43
|
+
const quoted = 'import type { Model } from "@repo/types";';
|
|
44
|
+
const pattern = /require("legacy")/;
|
|
45
|
+
const template = \`import("@repo/dynamic")\`;
|
|
46
|
+
api.require("legacy");
|
|
47
|
+
api.import("@repo/foo");
|
|
48
|
+
api?.require("@repo/types");
|
|
49
|
+
import(moduleName);
|
|
50
|
+
require("@repo/foo/" + name);
|
|
51
|
+
import("@repo/dynamic" + suffix);
|
|
52
|
+
`;
|
|
53
|
+
expect(workspacePackagesIn(code, catalog)).toEqual({});
|
|
54
|
+
expect(workspacePackagesIn('import "@repo/foo";', {})).toEqual({});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("reads escaped specifiers and literal import/require calls without a compiler", () => {
|
|
58
|
+
const code = String.raw`
|
|
59
|
+
import type { Model } from "\u0040repo\u002ftypes/model";
|
|
60
|
+
export * from '\x40repo/reexport';
|
|
61
|
+
const dynamic = import /* loader */ ("@repo/dynamic/utils", { with: { type: "json" } });
|
|
62
|
+
type Model = import("@repo/types").Model;
|
|
63
|
+
const legacy = require /* loader */ ('legacy/subpath',);
|
|
64
|
+
import Legacy = require("legacy");
|
|
65
|
+
const foo = require("\u{40}repo\/foo");
|
|
66
|
+
`;
|
|
67
|
+
expect(workspacePackagesIn(code, catalog)).toEqual(catalog);
|
|
68
|
+
expect(workspacePackagesIn("import(`@repo/foo/utils`)", catalog)).toEqual({
|
|
69
|
+
"@repo/foo": "packages/foo",
|
|
70
|
+
});
|
|
71
|
+
expect(workspacePackagesIn("import(`@repo/foo/${name}`)", catalog)).toEqual({});
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/workspaces.ts
CHANGED
|
@@ -1,8 +1,99 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
import { scan, type Token } from "#highlight.ts";
|
|
2
3
|
|
|
3
4
|
/** A workspace's packages: each one's name, with its directory from the root. */
|
|
4
5
|
export type WorkspacePackages = Readonly<Record<string, string>>;
|
|
5
6
|
|
|
7
|
+
const ESCAPES: Readonly<Record<string, string>> = {
|
|
8
|
+
n: "\n",
|
|
9
|
+
r: "\r",
|
|
10
|
+
t: "\t",
|
|
11
|
+
b: "\b",
|
|
12
|
+
f: "\f",
|
|
13
|
+
v: "\v",
|
|
14
|
+
"0": "\0",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Decode a quoted module name without evaluating code; only calls accept plain templates. */
|
|
18
|
+
const specifierOf = (token: Token | undefined, templates = false): string | undefined => {
|
|
19
|
+
if (token?.kind !== "string") return undefined;
|
|
20
|
+
const quote = token.text[0];
|
|
21
|
+
if (quote !== '"' && quote !== "'" && !(templates && quote === "`")) return undefined;
|
|
22
|
+
if (token.text.at(-1) !== quote) return undefined;
|
|
23
|
+
const body = token.text.slice(1, -1);
|
|
24
|
+
if (quote === "`" && /(?:^|[^\\])(?:\\\\)*\$\{/.test(body)) return undefined;
|
|
25
|
+
return body.replace(
|
|
26
|
+
/\\(u\{[\da-fA-F]{1,6}\}|u[\da-fA-F]{4}|x[\da-fA-F]{2}|[\s\S])/g,
|
|
27
|
+
(_, escaped: string) => {
|
|
28
|
+
if (/^(?:u\{|u[\da-fA-F]{4}$|x[\da-fA-F]{2}$)/.test(escaped)) {
|
|
29
|
+
const point = Number.parseInt(
|
|
30
|
+
escaped.startsWith("u{") ? escaped.slice(2, -1) : escaped.slice(1),
|
|
31
|
+
16,
|
|
32
|
+
);
|
|
33
|
+
return point <= 0x10ffff ? String.fromCodePoint(point) : `\\${escaped}`;
|
|
34
|
+
}
|
|
35
|
+
return ESCAPES[escaped] ?? escaped;
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Only packages named by this file's literal imports, type imports, re-exports,
|
|
42
|
+
* or import/require calls. Tokens keep comments, strings, regexes, and templates
|
|
43
|
+
* opaque; computed modules and imports inside template substitutions are not resolved.
|
|
44
|
+
* Subpaths identify their canonical package, without walking the global catalog.
|
|
45
|
+
*/
|
|
46
|
+
export const workspacePackagesIn = (
|
|
47
|
+
code: string,
|
|
48
|
+
catalog: WorkspacePackages,
|
|
49
|
+
): WorkspacePackages => {
|
|
50
|
+
const tokens = scan(code).flatMap((token) => {
|
|
51
|
+
if (token.kind === "comment" || token.text.trim() === "") return [];
|
|
52
|
+
// Highlighting groups punctuation; module clauses need individual delimiters.
|
|
53
|
+
return token.kind === undefined && /^[^\w$]+$/.test(token.text)
|
|
54
|
+
? [...token.text].map((text): Token => ({ text }))
|
|
55
|
+
: [token];
|
|
56
|
+
});
|
|
57
|
+
const names = new Set<string>();
|
|
58
|
+
const keep = (specifier: string | undefined) => {
|
|
59
|
+
if (specifier === undefined || specifier.startsWith(".") || specifier.startsWith("/")) return;
|
|
60
|
+
const name = specifier.split("/", specifier.startsWith("@") ? 2 : 1).join("/");
|
|
61
|
+
if (Object.hasOwn(catalog, name)) names.add(name);
|
|
62
|
+
};
|
|
63
|
+
for (const [index, token] of tokens.entries()) {
|
|
64
|
+
if (tokens[index - 1]?.text === ".") continue;
|
|
65
|
+
const next = tokens[index + 1];
|
|
66
|
+
if ((token.text === "import" || token.text === "require") && next?.text === "(") {
|
|
67
|
+
const end = tokens[index + 3]?.text;
|
|
68
|
+
if (end === ")" || end === ",") {
|
|
69
|
+
keep(specifierOf(tokens[index + 2], true));
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (token.kind !== "keyword" || (token.text !== "import" && token.text !== "export")) continue;
|
|
74
|
+
if (token.text === "import" && next?.kind === "string") {
|
|
75
|
+
keep(specifierOf(next));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const head = next?.text === "type" ? tokens[index + 2]?.text : next?.text;
|
|
79
|
+
if (token.text === "export" && head !== "{" && head !== "*") continue;
|
|
80
|
+
let depth = 0;
|
|
81
|
+
for (let at = index + 1; at < tokens.length; at += 1) {
|
|
82
|
+
const part = tokens[at]!;
|
|
83
|
+
if ([";", "=", "(", ")", "."].includes(part.text)) break;
|
|
84
|
+
if (part.text === "{") depth += 1;
|
|
85
|
+
else if (part.text === "}") depth -= 1;
|
|
86
|
+
if (depth !== 0) continue;
|
|
87
|
+
if (part.text === "from" && tokens[at + 1]?.kind === "string") {
|
|
88
|
+
keep(specifierOf(tokens[at + 1]));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (part.kind === "keyword" && !["type", "as", "from"].includes(part.text)) break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Object.fromEntries([...names].sort().map((name) => [name, catalog[name]!]));
|
|
95
|
+
};
|
|
96
|
+
|
|
6
97
|
/** What JSON text holds, or nothing when it is not JSON. */
|
|
7
98
|
const parsed = (text: string): unknown => {
|
|
8
99
|
try {
|