any-doctor 0.0.8 → 0.0.9
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/bin/engine.d.ts +1 -0
- package/bin/engine.js +45 -13
- package/bin/palette.d.ts +1 -0
- package/bin/palette.js +1 -1
- package/bin/runner.js +12 -1
- package/bin/score.d.ts +2 -0
- package/bin/score.js +9 -2
- package/bin/summary.js +6 -0
- package/package.json +1 -1
package/bin/engine.d.ts
CHANGED
package/bin/engine.js
CHANGED
|
@@ -19,6 +19,44 @@ import * as path from "path";
|
|
|
19
19
|
// 4. the loud error — unsupported platforms only.
|
|
20
20
|
const require_ = createRequire(import.meta.url);
|
|
21
21
|
let cachedBinary;
|
|
22
|
+
// A candidate counts only if it is a NATIVE executable, never a script.
|
|
23
|
+
// Under --ignore-scripts the postinstall never runs and @ast-grep/cli's
|
|
24
|
+
// bin files remain #!/usr/bin/env node shims — spawning one where env
|
|
25
|
+
// cannot find node fails confusingly (the review's probe). Native
|
|
26
|
+
// binaries begin with magic bytes; every script begins "#!". The
|
|
27
|
+
// platform package's binary is always the postinstall-copied original,
|
|
28
|
+
// so it is tried FIRST and the shim can never shadow it.
|
|
29
|
+
function isNativeExecutable(p) {
|
|
30
|
+
try {
|
|
31
|
+
const fd = fs.openSync(p, "r");
|
|
32
|
+
try {
|
|
33
|
+
const buf = Buffer.alloc(2);
|
|
34
|
+
const n = fs.readSync(fd, buf, 0, 2, 0);
|
|
35
|
+
return n === 2 && !(buf[0] === 0x23 && buf[1] === 0x21);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// The picker, pure over its candidate list (ordered by preference) so
|
|
46
|
+
// the shim-shadowing law is pinnable without a real stripped install.
|
|
47
|
+
export function pickNativeBinary(candidates) {
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
try {
|
|
50
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
51
|
+
if (isNativeExecutable(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// next candidate
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
22
60
|
export function resolveAstGrepBinary() {
|
|
23
61
|
var _a;
|
|
24
62
|
if (cachedBinary !== undefined)
|
|
@@ -26,9 +64,10 @@ export function resolveAstGrepBinary() {
|
|
|
26
64
|
const candidates = [];
|
|
27
65
|
try {
|
|
28
66
|
const pkgRoot = path.dirname(require_.resolve("@ast-grep/cli/package.json"));
|
|
29
|
-
candidates.push(path.join(pkgRoot, "ast-grep"), path.join(pkgRoot, "ast-grep.exe"));
|
|
30
67
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
|
|
31
68
|
const platformSuffix = `-${process.platform}-${process.arch}`;
|
|
69
|
+
// 1. the platform package's own binary — native by construction,
|
|
70
|
+
// present even under --ignore-scripts.
|
|
32
71
|
for (const dep of Object.keys((_a = pkg.optionalDependencies) !== null && _a !== void 0 ? _a : {})) {
|
|
33
72
|
if (dep.includes(platformSuffix)) {
|
|
34
73
|
try {
|
|
@@ -40,22 +79,15 @@ export function resolveAstGrepBinary() {
|
|
|
40
79
|
}
|
|
41
80
|
}
|
|
42
81
|
}
|
|
82
|
+
// 2. the postinstall copy inside @ast-grep/cli — same bytes after a
|
|
83
|
+
// normal install; a shim (never native, never picked) before one.
|
|
84
|
+
candidates.push(path.join(pkgRoot, "ast-grep"), path.join(pkgRoot, "ast-grep.exe"));
|
|
43
85
|
}
|
|
44
86
|
catch {
|
|
45
87
|
// @ast-grep/cli absent (unsupported install) — PATH is next
|
|
46
88
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
50
|
-
cachedBinary = candidate;
|
|
51
|
-
return cachedBinary;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// next candidate
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
cachedBinary = null;
|
|
58
|
-
return null;
|
|
89
|
+
cachedBinary = pickNativeBinary(candidates);
|
|
90
|
+
return cachedBinary;
|
|
59
91
|
}
|
|
60
92
|
// One batched query over a real repo emits tens of megabytes of JSON (the
|
|
61
93
|
// async-doctor pilot's ten patterns produce 24MB over a 979-file repo) —
|
package/bin/palette.d.ts
CHANGED
package/bin/palette.js
CHANGED
|
@@ -20,5 +20,5 @@ export function gradeColor(score) {
|
|
|
20
20
|
// structurally typed: palette's score imports stay value-level
|
|
21
21
|
// (gradeFor, isEmptyScan) — no type dependency rides along.
|
|
22
22
|
export function scoreHeaderTone(score) {
|
|
23
|
-
return isEmptyScan(score) ? YELLOW : gradeColor(score.score);
|
|
23
|
+
return isEmptyScan(score) || score.partialScan === true ? YELLOW : gradeColor(score.score);
|
|
24
24
|
}
|
package/bin/runner.js
CHANGED
|
@@ -157,6 +157,17 @@ function spawnLoader(programPath, mode, timeoutMs, nodeFlags) {
|
|
|
157
157
|
function lastLines(s, n = 8) {
|
|
158
158
|
return s.trim().split("\n").slice(-n).join("\n");
|
|
159
159
|
}
|
|
160
|
+
// A crash's REASON lives at the top of stderr ("Error: <why>"), the
|
|
161
|
+
// stack trails beneath it. Keeping only the tail showed frames and
|
|
162
|
+
// swallowed the reason — including the engine's "ships with any-doctor"
|
|
163
|
+
// explanation (review probe). Keep both ends: the leading lines that
|
|
164
|
+
// state the cause, the trailing lines that locate it.
|
|
165
|
+
function crashSummary(s) {
|
|
166
|
+
const lines = s.trim().split("\n");
|
|
167
|
+
if (lines.length <= 8)
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
return [...lines.slice(0, 3), " …", ...lines.slice(-4)].join("\n");
|
|
170
|
+
}
|
|
160
171
|
// Loader frames are authored by our own doctor-loader — trusted construction.
|
|
161
172
|
// The guards below separate "a usable frame" from "not a frame"; they are not
|
|
162
173
|
// schema validation of the doctor contract.
|
|
@@ -186,7 +197,7 @@ const execLoader = (programPath, mode, timeoutMs = DEFAULT_TIMEOUT_MS) => Effect
|
|
|
186
197
|
findings: ["the runtime refused a forbidden capability:", ...lastLines(out.stderr, 2).split("\n")],
|
|
187
198
|
});
|
|
188
199
|
}
|
|
189
|
-
return yield* new DoctorCrashed({ programPath: abs, detail:
|
|
200
|
+
return yield* new DoctorCrashed({ programPath: abs, detail: crashSummary(out.stderr || "exit " + out.status) });
|
|
190
201
|
}
|
|
191
202
|
const lines = out.stdout.split("\n");
|
|
192
203
|
// The last sentinel line wins. A doctor writing directly to
|
package/bin/score.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface ScoreResult {
|
|
|
4
4
|
grade: string;
|
|
5
5
|
filesClean: number;
|
|
6
6
|
filesTotal: number;
|
|
7
|
+
partialScan?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export declare function findingSeverity(g: ReportGroup, f: Finding): Severity;
|
|
9
10
|
export declare function gradeFor(score: number): string;
|
|
@@ -19,6 +20,7 @@ export interface ScoreHeader {
|
|
|
19
20
|
scoreLine: string;
|
|
20
21
|
cleanLine: string | null;
|
|
21
22
|
emptyScan: boolean;
|
|
23
|
+
partialScan: boolean;
|
|
22
24
|
}
|
|
23
25
|
export declare function scoreHeaderLines(s: ScoreResult): ScoreHeader;
|
|
24
26
|
export declare function categoryRollup(groups: ReportGroup[]): {
|
package/bin/score.js
CHANGED
|
@@ -55,15 +55,22 @@ export function isEmptyScan(s) {
|
|
|
55
55
|
// dashboard render these strings, never re-compose them. The clean line
|
|
56
56
|
// is null for an empty scan — there is nothing to be clean against —
|
|
57
57
|
// and so is any score claim: "100 — Excellent" over nothing checked is
|
|
58
|
-
// a false green, so the header says n/a instead.
|
|
58
|
+
// a false green, so the header says n/a instead. A PARTIAL scan (doctors
|
|
59
|
+
// crashed) gets the same refusal: findings stay listed and the exit
|
|
60
|
+
// stays failing, but the aggregate grade is withheld — the crashed
|
|
61
|
+
// doctors' silence must never read as cleanliness.
|
|
59
62
|
export function scoreHeaderLines(s) {
|
|
60
63
|
if (isEmptyScan(s)) {
|
|
61
|
-
return { scoreLine: "Score: n/a — no files scanned", cleanLine: null, emptyScan: true };
|
|
64
|
+
return { scoreLine: "Score: n/a — no files scanned", cleanLine: null, emptyScan: true, partialScan: false };
|
|
65
|
+
}
|
|
66
|
+
if (s.partialScan === true) {
|
|
67
|
+
return { scoreLine: "Score: n/a — partial scan (a doctor crashed; results above are incomplete)", cleanLine: null, emptyScan: false, partialScan: true };
|
|
62
68
|
}
|
|
63
69
|
return {
|
|
64
70
|
scoreLine: `Score: ${s.score} / 100 — ${s.grade}`,
|
|
65
71
|
cleanLine: `${s.filesClean}/${s.filesTotal} files clean`,
|
|
66
72
|
emptyScan: false,
|
|
73
|
+
partialScan: false,
|
|
67
74
|
};
|
|
68
75
|
}
|
|
69
76
|
export function categoryRollup(groups) {
|
package/bin/summary.js
CHANGED
|
@@ -54,9 +54,15 @@ function dedupeGroups(groups) {
|
|
|
54
54
|
return { groups: out, hidden };
|
|
55
55
|
}
|
|
56
56
|
export function deriveSummary(outcome) {
|
|
57
|
+
var _a, _b;
|
|
57
58
|
const { groups, hidden } = dedupeGroups(outcome.groups);
|
|
58
59
|
const total = groups.reduce((n, g) => n + g.findings.length, 0);
|
|
59
60
|
const score = computeScore(groups, outcome.fileCount);
|
|
61
|
+
// A crashed doctor contributes no findings, so the raw score reads its
|
|
62
|
+
// silence as cleanliness — the derivation is where crashes are known,
|
|
63
|
+
// and where the partial flag is set for every surface to honor.
|
|
64
|
+
if (((_b = (_a = outcome.crashed) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0)
|
|
65
|
+
score.partialScan = true;
|
|
60
66
|
const header = scoreHeaderLines(score);
|
|
61
67
|
const severityCounts = { error: 0, warning: 0, info: 0 };
|
|
62
68
|
for (const g of groups) {
|