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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NamedRuleQuery, RuleQuery } from "./contract.js";
2
+ export declare function pickNativeBinary(candidates: string[]): string | null;
2
3
  export declare function resolveAstGrepBinary(): string | null;
3
4
  export type EngineQuery = {
4
5
  op: "pattern";
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
- for (const candidate of candidates) {
48
- try {
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
@@ -8,4 +8,5 @@ export declare function gradeColor(score: number): string;
8
8
  export declare function scoreHeaderTone(score: {
9
9
  score: number;
10
10
  filesTotal: number;
11
+ partialScan?: boolean;
11
12
  }): string;
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: lastLines(out.stderr || "exit " + out.status) });
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-doctor",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Your agent writes the analyzer, fixtures prove it, CI reruns it forever.",
5
5
  "license": "MIT",
6
6
  "bin": {