any-doctor 0.0.1
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/CONTEXT.md +128 -0
- package/README.md +68 -0
- package/bin/capabilities.d.ts +15 -0
- package/bin/capabilities.js +131 -0
- package/bin/cli.d.ts +2 -0
- package/bin/cli.js +426 -0
- package/bin/clipboard.d.ts +1 -0
- package/bin/clipboard.js +10 -0
- package/bin/contract.d.ts +134 -0
- package/bin/contract.js +70 -0
- package/bin/dashboard.d.ts +108 -0
- package/bin/dashboard.js +718 -0
- package/bin/discover.d.ts +24 -0
- package/bin/discover.js +87 -0
- package/bin/doctor-loader.d.mts +1 -0
- package/bin/doctor-loader.mjs +161 -0
- package/bin/engine.d.ts +18 -0
- package/bin/engine.js +22 -0
- package/bin/fuzzy.d.ts +2 -0
- package/bin/fuzzy.js +31 -0
- package/bin/import-guard.mjs +31 -0
- package/bin/keys.d.ts +2 -0
- package/bin/keys.js +72 -0
- package/bin/palette.d.ts +7 -0
- package/bin/palette.js +14 -0
- package/bin/picker.d.ts +12 -0
- package/bin/picker.js +82 -0
- package/bin/report.d.ts +18 -0
- package/bin/report.js +159 -0
- package/bin/runner.d.ts +58 -0
- package/bin/runner.js +271 -0
- package/bin/score.d.ts +13 -0
- package/bin/score.js +39 -0
- package/bin/sdk.d.ts +5 -0
- package/bin/sdk.js +95 -0
- package/bin/search-host.d.ts +6 -0
- package/bin/search-host.js +56 -0
- package/bin/select.d.ts +35 -0
- package/bin/select.js +45 -0
- package/bin/tty.d.ts +38 -0
- package/bin/tty.js +94 -0
- package/docs/REPAIR-LOG.md +45 -0
- package/docs/RESULTS.md +70 -0
- package/docs/decisions.md +450 -0
- package/docs/example-catalog.md +122 -0
- package/docs/features.md +67 -0
- package/docs/first-shot-results.md +18 -0
- package/docs/intents.md +21 -0
- package/docs/kill-test.md +54 -0
- package/docs/research.md +66 -0
- package/docs/vision.md +83 -0
- package/doctors/AGENTS.md +103 -0
- package/doctors/api-route-files-do-import.fixtures.mjs +61 -0
- package/doctors/api-route-files-do-import.mjs +26 -0
- package/doctors/async-doctor.fixtures.mjs +147 -0
- package/doctors/async-doctor.mjs +295 -0
- package/doctors/convex-doctor.fixtures.mjs +177 -0
- package/doctors/convex-doctor.mjs +223 -0
- package/doctors/date-now-used-inside-effect.fixtures.mjs +46 -0
- package/doctors/date-now-used-inside-effect.mjs +132 -0
- package/doctors/json-parse-calls-llm-api.fixtures.mjs +37 -0
- package/doctors/json-parse-calls-llm-api.mjs +85 -0
- package/doctors/route-handlers-touch-database-before.fixtures.mjs +58 -0
- package/doctors/route-handlers-touch-database-before.mjs +98 -0
- package/doctors/z-record-called-with-single.fixtures.mjs +28 -0
- package/doctors/z-record-called-with-single.mjs +19 -0
- package/fixtures/sample-app/src/hooks/useChat.ts +15 -0
- package/fixtures/sample-app/src/lib/ai/client.ts +5 -0
- package/fixtures/sample-app/src/schemas/user.ts +6 -0
- package/fixtures/sample-app/src/services/chat.ts +17 -0
- package/fixtures/sample-app/src/services/user.ts +10 -0
- package/fixtures/sample-app/src/utils/sync.ts +16 -0
- package/package.json +42 -0
- package/skill/any-doctor.skill.md +188 -0
package/CONTEXT.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# CONTEXT.md — domain glossary
|
|
2
|
+
|
|
3
|
+
Canonical vocabulary for any-doctor. Glossary only — no implementation.
|
|
4
|
+
When a term here conflicts with language elsewhere, this file wins.
|
|
5
|
+
|
|
6
|
+
## Doctor program
|
|
7
|
+
|
|
8
|
+
The artifact an LLM writes: a JavaScript module that inspects a target
|
|
9
|
+
codebase through the DoctorCtx and emits findings. One doctor program
|
|
10
|
+
encodes one convention the team cares about. Retired synonym: "lint rule."
|
|
11
|
+
|
|
12
|
+
## Doctor run
|
|
13
|
+
|
|
14
|
+
One execution of a doctor program against one target directory.
|
|
15
|
+
Deterministic for a given engine version: `ctx.search` answers via the host
|
|
16
|
+
(the doctor process cannot spawn — it runs under Confinement, and any-doctor
|
|
17
|
+
runs ast-grep itself, returning matches over a dedicated channel), so
|
|
18
|
+
results can vary across engine upgrades.
|
|
19
|
+
|
|
20
|
+
## RunOutcome
|
|
21
|
+
|
|
22
|
+
One scan invocation's batch of results, assembled once and rendered by the
|
|
23
|
+
report and the dashboard alike: the ReportGroups that ran, the doctor ids
|
|
24
|
+
that crashed (data, named), the slugs Confinement skipped, the doctor
|
|
25
|
+
id → program path map for re-run commands, and the target's file count and
|
|
26
|
+
the batch's wall-clock duration — one defined meaning per field.
|
|
27
|
+
|
|
28
|
+
## Confinement
|
|
29
|
+
|
|
30
|
+
The layered policy that makes a doctor program safe to execute. A doctor is
|
|
31
|
+
a single self-contained file: it may not import anything, and everything
|
|
32
|
+
else reaches it through `ctx`. The layers, in order: the static capability
|
|
33
|
+
scan refuses named capabilities before execution; the run happens under
|
|
34
|
+
Node's permission model (writes, subprocesses, and native addons denied;
|
|
35
|
+
worker threads exist only as the import guard's carrier and inherit every
|
|
36
|
+
denial); an import guard refuses every module resolution a doctor attempts;
|
|
37
|
+
and the network globals are deleted from the process before doctor code
|
|
38
|
+
runs. There is no override in any mode.
|
|
39
|
+
|
|
40
|
+
## Finding
|
|
41
|
+
|
|
42
|
+
One emitted finding: a location (file, line) plus optional per-finding
|
|
43
|
+
message or severity override. "Issue" and "instance" are retired
|
|
44
|
+
synonyms — Finding is the term in code, copy, and prompts. The
|
|
45
|
+
doctor-level truth (id, description, default severity, blind spots)
|
|
46
|
+
lives in the program's meta, not in individual findings.
|
|
47
|
+
|
|
48
|
+
## Check
|
|
49
|
+
|
|
50
|
+
One rule within a doctor program. A finding names its check via `rule`;
|
|
51
|
+
the check's meta supplies description, severity, impact, why, and fix;
|
|
52
|
+
the doctor's meta supplies the defaults when a finding names no check.
|
|
53
|
+
A check id is a short kebab-case noun phrase over [a-z0-9-], unique
|
|
54
|
+
within its doctor, naming the defect (fetch-calls-without-abortsignal,
|
|
55
|
+
filter-table-scan). One doctor program, many checks.
|
|
56
|
+
|
|
57
|
+
## Doctor contract
|
|
58
|
+
|
|
59
|
+
The interface shared by doctor programs, the runner, the report, the
|
|
60
|
+
fixture harness, and the generator prompt. The single place where the
|
|
61
|
+
shape of ctx, meta, findings, and the runner protocol is defined.
|
|
62
|
+
|
|
63
|
+
## DoctorCtx (ctx)
|
|
64
|
+
|
|
65
|
+
The capability boundary a doctor program is expected to use: read-only,
|
|
66
|
+
repo-scoped file access, structural search, and a finding emitter.
|
|
67
|
+
Confinement enforces it: outside ctx there is nothing — no imports, no
|
|
68
|
+
writes, no subprocesses, no network.
|
|
69
|
+
|
|
70
|
+
## Engine
|
|
71
|
+
|
|
72
|
+
The structural-search backend a DoctorCtx uses to answer ctx.search.
|
|
73
|
+
ast-grep is the engine today; oxc is a candidate for TypeScript-heavy
|
|
74
|
+
repos. Engine selection is invisible to doctor programs: one doctor
|
|
75
|
+
program runs unchanged on any engine. One module owns the invocation
|
|
76
|
+
(src/engine.ts); the search host sits on it, and the sdk asks the host —
|
|
77
|
+
there is exactly one path, with no unconfined fallback.
|
|
78
|
+
|
|
79
|
+
## Meta
|
|
80
|
+
|
|
81
|
+
A doctor program's declared data: id, description, default severity,
|
|
82
|
+
optional category, optional checks (one CheckMeta per check), blind
|
|
83
|
+
spots. Authored as data, never as comments; read by the report, the
|
|
84
|
+
review tree, and the generator prompt.
|
|
85
|
+
|
|
86
|
+
## Seed
|
|
87
|
+
|
|
88
|
+
A set of inline files (paths + contents) a fixture materializes into a
|
|
89
|
+
temporary directory so a doctor program can be exercised against
|
|
90
|
+
known input.
|
|
91
|
+
|
|
92
|
+
## Fixture
|
|
93
|
+
|
|
94
|
+
One seed plus the findings expected from running a doctor program against
|
|
95
|
+
it. Expected findings match exactly on (file, line): a missing expected
|
|
96
|
+
finding is a recall failure; an unexpected finding is a precision failure.
|
|
97
|
+
|
|
98
|
+
## Verify
|
|
99
|
+
|
|
100
|
+
Running a doctor program against its fixtures and diffing the findings.
|
|
101
|
+
The trust gate: a doctor program is not considered working until verify
|
|
102
|
+
passes.
|
|
103
|
+
|
|
104
|
+
## Runner
|
|
105
|
+
|
|
106
|
+
The process boundary that loads a doctor program, injects ctx, and
|
|
107
|
+
returns framed results. Today a local node child process; the same
|
|
108
|
+
contract must hold for any future sandbox.
|
|
109
|
+
|
|
110
|
+
## Registry
|
|
111
|
+
|
|
112
|
+
The record of installed doctor programs per scope. Each doctor's meta is
|
|
113
|
+
its registry entry; the doctors/ directory itself is the registry —
|
|
114
|
+
discovery reads it directly, with no separate index or cache.
|
|
115
|
+
|
|
116
|
+
## Scope
|
|
117
|
+
|
|
118
|
+
Where a doctor program lives: repo-local (`./doctors/`, committed with
|
|
119
|
+
the consuming repo) or user-global (`~/.any-doctor/doctors/`, available
|
|
120
|
+
in every repo). Repo-local wins slug collisions. Scanning a target repo
|
|
121
|
+
never writes to any scope.
|
|
122
|
+
|
|
123
|
+
## Skill
|
|
124
|
+
|
|
125
|
+
The instructions any-doctor provides so an agent can create a doctor that
|
|
126
|
+
fits the contract. Planted as `AGENTS.md` in a scope directory and also
|
|
127
|
+
served verbatim by `generate`. Any Doctor equips agents with the skill;
|
|
128
|
+
it never launches, deploys, or speaks for an agent.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# any-doctor
|
|
2
|
+
|
|
3
|
+
> Your agent writes the analyzer. Fixtures prove it. CI reruns it forever.
|
|
4
|
+
|
|
5
|
+
Any Doctor turns a one-sentence convention ("find `.map(async ...)` results
|
|
6
|
+
that are never awaited") into a **doctor program** — a small piece of
|
|
7
|
+
analysis code your LLM writes against our typed `ctx` API. The CLI runs it
|
|
8
|
+
and renders a React-Doctor-style report: score, grouped findings with
|
|
9
|
+
file:line evidence, declared blind spots, an interactive review browser,
|
|
10
|
+
and a copy-paste handoff so your agent can fix what was found. The saved
|
|
11
|
+
program reruns deterministically in CI with zero inference.
|
|
12
|
+
|
|
13
|
+
Any Doctor equips agents; it never deploys them. Everything runs locally.
|
|
14
|
+
No Cloudflare, no API keys, no server.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
any-doctor generate "find fetch calls without an AbortSignal" # copies the exact prompt for your agent
|
|
20
|
+
any-doctor verify doctors/fetch-without-abort-signal.mjs # fixture gate (exact-set)
|
|
21
|
+
any-doctor run doctors/fetch-without-abort-signal.mjs path/to/repo # scan + score + report + review menu
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`generate` plants the skill as `AGENTS.md` in the scope dir (agents load it
|
|
25
|
+
natively) and copies the generation prompt — paste it into your own agent
|
|
26
|
+
session, any agent, GUI or CLI. When it has written the doctor + fixtures,
|
|
27
|
+
`verify` gates it: missing expected findings fail recall, unexpected ones
|
|
28
|
+
fail precision. `run` and `verify` never touch a model or an agent — pipe
|
|
29
|
+
the output (or set `ANY_DOCTOR_HEADLESS=1`) for stable CI output. Requires
|
|
30
|
+
Node ≥ 18.
|
|
31
|
+
|
|
32
|
+
A doctor program is `<name>.mjs` (exports `meta` + `doctor(ctx)`) next to
|
|
33
|
+
its fixture module `<name>.fixtures.mjs` (seeds + expected findings).
|
|
34
|
+
See [CONTEXT.md](CONTEXT.md) for the vocabulary and
|
|
35
|
+
[doctors/async-doctor.mjs](doctors/async-doctor.mjs) for a working
|
|
36
|
+
example: one doctor, many checks — async hygiene as a category, with
|
|
37
|
+
per-check fixtures and the interactive check tree in `run`.
|
|
38
|
+
|
|
39
|
+
## Docs
|
|
40
|
+
|
|
41
|
+
| Doc | What it holds |
|
|
42
|
+
|---|---|
|
|
43
|
+
| [CONTEXT.md](CONTEXT.md) | Domain glossary — canonical terms |
|
|
44
|
+
| [docs/decisions.md](docs/decisions.md) | Decision log (D1–D16). Read first; don't relitigate |
|
|
45
|
+
| [docs/vision.md](docs/vision.md) | Product idea and the lifecycle novelty |
|
|
46
|
+
| [docs/features.md](docs/features.md) | Doctor discovery & registry spec + status |
|
|
47
|
+
| [docs/research.md](docs/research.md) | Landscape, React Doctor teardown |
|
|
48
|
+
| [docs/kill-test.md](docs/kill-test.md) + [docs/RESULTS.md](docs/RESULTS.md) | The validation experiment and its numbers |
|
|
49
|
+
| [docs/REPAIR-LOG.md](docs/REPAIR-LOG.md) | Generation-bug categories — feeds the generation skill |
|
|
50
|
+
| [docs/example-catalog.md](docs/example-catalog.md) | Rule intents across the JS ecosystem |
|
|
51
|
+
|
|
52
|
+
## Where we are (2026-09-03)
|
|
53
|
+
|
|
54
|
+
1. ✅ Contract v0, verify harness, pilot doctor, kill test, packaging
|
|
55
|
+
2. ✅ Generation: skill + prompt handoff (D14 — copy-based, agent-agnostic)
|
|
56
|
+
3. ✅ UI layer: score header, category rollup, review browser, post-report menu
|
|
57
|
+
4. ✅ Discovery & registry: fuzzy picker, repo-local + global scopes, `--all` batch modes
|
|
58
|
+
5. ⬜ Frozen-fixture protocol + authored eval corpus (the trust upgrade — first-shot 10/10 is currently self-graded)
|
|
59
|
+
6. ⬜ Grow `ctx`: symbols/imports resolution, JS-family languages
|
|
60
|
+
7. ⬜ Publish: npm, GitHub, CI workflow, launch post
|
|
61
|
+
|
|
62
|
+
## Principles
|
|
63
|
+
|
|
64
|
+
- The report comes from the generated program's evidence, not the LLM's opinion.
|
|
65
|
+
- Fixtures gate everything: a doctor that hasn't passed `verify` doesn't exist.
|
|
66
|
+
- Programs declare their blind spots as data.
|
|
67
|
+
- Any Doctor equips agents; it never launches, deploys, or speaks for them.
|
|
68
|
+
- Everything runs locally. The runner seam (today a node child process) is where any future sandbox plugs in.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type CapabilityLevel = "red" | "yellow";
|
|
2
|
+
export interface CapabilityFinding {
|
|
3
|
+
capability: string;
|
|
4
|
+
level: CapabilityLevel;
|
|
5
|
+
line: number;
|
|
6
|
+
detail: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CapabilityReport {
|
|
9
|
+
findings: CapabilityFinding[];
|
|
10
|
+
red: CapabilityFinding[];
|
|
11
|
+
yellow: CapabilityFinding[];
|
|
12
|
+
}
|
|
13
|
+
export declare function scanCapabilities(source: string): CapabilityReport;
|
|
14
|
+
export declare function scanDoctorFile(programPath: string): CapabilityReport;
|
|
15
|
+
export declare function capabilitySummary(report: CapabilityReport): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
const MODULE_SPEC = /(?:\bfrom\s+|\brequire\s*\(\s*)["'](node:)?/;
|
|
3
|
+
const RULES = [
|
|
4
|
+
// --- red: no legitimate use in a doctor ---
|
|
5
|
+
{ capability: "network", level: "red", on: "raw", kind: "network module import", pattern: new RegExp(MODULE_SPEC.source + String.raw `(https?|http2|net|tls|dns|dgram|undici)["']`) },
|
|
6
|
+
{ capability: "network", level: "red", on: "masked", kind: "fetch", pattern: /\bfetch\s*\(/ },
|
|
7
|
+
{ capability: "network", level: "red", on: "masked", kind: "http call", pattern: /\bhttps?\s*\.\s*(?:get|request|createServer|Agent)\s*\(/ },
|
|
8
|
+
{ capability: "network", level: "red", on: "masked", kind: "WebSocket/XMLHttpRequest", pattern: /\bnew\s+(?:WebSocket|XMLHttpRequest)\b/ },
|
|
9
|
+
{ capability: "file write", level: "red", on: "masked", kind: "fs write call", pattern: /\bfs\s*\.\s*(?:promises\s*\.\s*)?(?:writeFile|appendFile|createWriteStream|rm|unlink|rename|mkdir|rmdir|truncate|chmod|chown)\w*\s*\(|\bfs\s*\.\s*open\s*\(\s*[^,)]*,\s*["']w/ },
|
|
10
|
+
{ capability: "file write", level: "red", on: "masked", kind: "write call (destructured import)", pattern: /\b(?:writeFile|appendFile|createWriteStream|rmSync|unlinkSync|rm|unlink)\s*\(/ },
|
|
11
|
+
{ capability: "subprocess", level: "red", on: "raw", kind: "child_process import", pattern: new RegExp(MODULE_SPEC.source + String.raw `child_process["']`) },
|
|
12
|
+
{ capability: "subprocess", level: "red", on: "masked", kind: "process spawn call", pattern: /(?<![.\w])(?:exec|execSync|spawn|spawnSync|execFile|execFileSync|fork)\s*\(/ },
|
|
13
|
+
// A doctor is a single self-contained file: no imports of any kind.
|
|
14
|
+
{ capability: "import", level: "red", on: "raw", kind: "import statement (doctors are single-file programs)", pattern: /(?:\bimport\b|\bexport\b)[^\n]*\bfrom\s*["'][^"']*["']/ },
|
|
15
|
+
{ capability: "import", level: "red", on: "raw", kind: "side-effect import (doctors are single-file programs)", pattern: /^\s*import\s*["'][^"']*["']/ },
|
|
16
|
+
{ capability: "import", level: "red", on: "masked", kind: "dynamic import (doctors are single-file programs)", pattern: /\bimport\s*\(/ },
|
|
17
|
+
// --- yellow: legal but out-of-contract, surface it ---
|
|
18
|
+
// (No "fs import" yellow rule exists: every import line is red above —
|
|
19
|
+
// the single-file law — so an fs-import yellow could never fire.)
|
|
20
|
+
{ capability: "direct fs read", level: "yellow", on: "masked", kind: "fs read call (bypasses ctx.files)", pattern: /\bfs\s*\.\s*(?:promises\s*\.\s*)?(?:readFile|readdir|stat|existsSync|createReadStream)\w*\s*\(/ },
|
|
21
|
+
{ capability: "environment", level: "yellow", on: "masked", kind: "process.env", pattern: /\bprocess\s*\.\s*env\b/ },
|
|
22
|
+
];
|
|
23
|
+
function maskNonCode(source) {
|
|
24
|
+
const chars = source.split("");
|
|
25
|
+
let index = 0;
|
|
26
|
+
while (index < source.length) {
|
|
27
|
+
const char = source[index];
|
|
28
|
+
const next = source[index + 1];
|
|
29
|
+
if (char === "/" && next === "/") {
|
|
30
|
+
const end = source.indexOf("\n", index + 2);
|
|
31
|
+
const stop = end === -1 ? source.length : end;
|
|
32
|
+
for (let cursor = index; cursor < stop; cursor += 1)
|
|
33
|
+
chars[cursor] = " ";
|
|
34
|
+
index = stop;
|
|
35
|
+
}
|
|
36
|
+
else if (char === "/" && next === "*") {
|
|
37
|
+
const end = source.indexOf("*/", index + 2);
|
|
38
|
+
const stop = end === -1 ? source.length : end + 2;
|
|
39
|
+
for (let cursor = index; cursor < stop; cursor += 1) {
|
|
40
|
+
if (chars[cursor] !== "\n")
|
|
41
|
+
chars[cursor] = " ";
|
|
42
|
+
}
|
|
43
|
+
index = stop;
|
|
44
|
+
}
|
|
45
|
+
else if (char === "'" || char === '"' || char === "`") {
|
|
46
|
+
const quote = char;
|
|
47
|
+
let cursor = index + 1;
|
|
48
|
+
while (cursor < source.length) {
|
|
49
|
+
if (source[cursor] === "\\")
|
|
50
|
+
cursor += 2;
|
|
51
|
+
else if (source[cursor] === quote) {
|
|
52
|
+
cursor += 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
cursor += 1;
|
|
57
|
+
}
|
|
58
|
+
for (let position = index; position < cursor; position += 1) {
|
|
59
|
+
if (chars[position] !== "\n")
|
|
60
|
+
chars[position] = " ";
|
|
61
|
+
}
|
|
62
|
+
index = cursor;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return chars.join("");
|
|
69
|
+
}
|
|
70
|
+
// One finding per rule per line; the first matching rule wins a line so the
|
|
71
|
+
// report stays a signal, not a wall.
|
|
72
|
+
export function scanCapabilities(source) {
|
|
73
|
+
const maskedLines = maskNonCode(source).split("\n");
|
|
74
|
+
const rawLines = source.split("\n");
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
77
|
+
for (const rule of RULES) {
|
|
78
|
+
const line = rule.on === "raw" ? rawLines[i] : maskedLines[i];
|
|
79
|
+
if (line !== undefined && rule.pattern.test(line)) {
|
|
80
|
+
findings.push({
|
|
81
|
+
capability: rule.capability,
|
|
82
|
+
level: rule.level,
|
|
83
|
+
line: i + 1,
|
|
84
|
+
detail: `${rule.kind} (line ${i + 1})`,
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
findings,
|
|
92
|
+
red: findings.filter(f => f.level === "red"),
|
|
93
|
+
yellow: findings.filter(f => f.level === "yellow"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// One scan per file per process: the gate, verify's capability line, and
|
|
97
|
+
// the tests all read the same report instead of re-scanning.
|
|
98
|
+
const scanCache = new Map();
|
|
99
|
+
export function scanDoctorFile(programPath) {
|
|
100
|
+
const cached = scanCache.get(programPath);
|
|
101
|
+
if (cached !== undefined)
|
|
102
|
+
return cached;
|
|
103
|
+
const report = readAndScan(programPath);
|
|
104
|
+
scanCache.set(programPath, report);
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
107
|
+
function readAndScan(programPath) {
|
|
108
|
+
let source = "";
|
|
109
|
+
try {
|
|
110
|
+
source = fs.readFileSync(programPath, "utf8");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// A missing file is the runner's error to report; the gate stays quiet
|
|
114
|
+
// so the layers don't talk over each other.
|
|
115
|
+
return { findings: [], red: [], yellow: [] };
|
|
116
|
+
}
|
|
117
|
+
return scanCapabilities(source);
|
|
118
|
+
}
|
|
119
|
+
export function capabilitySummary(report) {
|
|
120
|
+
if (report.findings.length === 0) {
|
|
121
|
+
return "reads via ctx only — no imports, no network, no writes, no subprocesses";
|
|
122
|
+
}
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (report.red.length > 0) {
|
|
125
|
+
parts.push(`RED: ${[...new Set(report.red.map(f => f.capability))].join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
if (report.yellow.length > 0) {
|
|
128
|
+
parts.push(`note: ${[...new Set(report.yellow.map(f => f.capability))].join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
return parts.join(" · ");
|
|
131
|
+
}
|
package/bin/cli.d.ts
ADDED