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.
Files changed (74) hide show
  1. package/CONTEXT.md +128 -0
  2. package/README.md +68 -0
  3. package/bin/capabilities.d.ts +15 -0
  4. package/bin/capabilities.js +131 -0
  5. package/bin/cli.d.ts +2 -0
  6. package/bin/cli.js +426 -0
  7. package/bin/clipboard.d.ts +1 -0
  8. package/bin/clipboard.js +10 -0
  9. package/bin/contract.d.ts +134 -0
  10. package/bin/contract.js +70 -0
  11. package/bin/dashboard.d.ts +108 -0
  12. package/bin/dashboard.js +718 -0
  13. package/bin/discover.d.ts +24 -0
  14. package/bin/discover.js +87 -0
  15. package/bin/doctor-loader.d.mts +1 -0
  16. package/bin/doctor-loader.mjs +161 -0
  17. package/bin/engine.d.ts +18 -0
  18. package/bin/engine.js +22 -0
  19. package/bin/fuzzy.d.ts +2 -0
  20. package/bin/fuzzy.js +31 -0
  21. package/bin/import-guard.mjs +31 -0
  22. package/bin/keys.d.ts +2 -0
  23. package/bin/keys.js +72 -0
  24. package/bin/palette.d.ts +7 -0
  25. package/bin/palette.js +14 -0
  26. package/bin/picker.d.ts +12 -0
  27. package/bin/picker.js +82 -0
  28. package/bin/report.d.ts +18 -0
  29. package/bin/report.js +159 -0
  30. package/bin/runner.d.ts +58 -0
  31. package/bin/runner.js +271 -0
  32. package/bin/score.d.ts +13 -0
  33. package/bin/score.js +39 -0
  34. package/bin/sdk.d.ts +5 -0
  35. package/bin/sdk.js +95 -0
  36. package/bin/search-host.d.ts +6 -0
  37. package/bin/search-host.js +56 -0
  38. package/bin/select.d.ts +35 -0
  39. package/bin/select.js +45 -0
  40. package/bin/tty.d.ts +38 -0
  41. package/bin/tty.js +94 -0
  42. package/docs/REPAIR-LOG.md +45 -0
  43. package/docs/RESULTS.md +70 -0
  44. package/docs/decisions.md +450 -0
  45. package/docs/example-catalog.md +122 -0
  46. package/docs/features.md +67 -0
  47. package/docs/first-shot-results.md +18 -0
  48. package/docs/intents.md +21 -0
  49. package/docs/kill-test.md +54 -0
  50. package/docs/research.md +66 -0
  51. package/docs/vision.md +83 -0
  52. package/doctors/AGENTS.md +103 -0
  53. package/doctors/api-route-files-do-import.fixtures.mjs +61 -0
  54. package/doctors/api-route-files-do-import.mjs +26 -0
  55. package/doctors/async-doctor.fixtures.mjs +147 -0
  56. package/doctors/async-doctor.mjs +295 -0
  57. package/doctors/convex-doctor.fixtures.mjs +177 -0
  58. package/doctors/convex-doctor.mjs +223 -0
  59. package/doctors/date-now-used-inside-effect.fixtures.mjs +46 -0
  60. package/doctors/date-now-used-inside-effect.mjs +132 -0
  61. package/doctors/json-parse-calls-llm-api.fixtures.mjs +37 -0
  62. package/doctors/json-parse-calls-llm-api.mjs +85 -0
  63. package/doctors/route-handlers-touch-database-before.fixtures.mjs +58 -0
  64. package/doctors/route-handlers-touch-database-before.mjs +98 -0
  65. package/doctors/z-record-called-with-single.fixtures.mjs +28 -0
  66. package/doctors/z-record-called-with-single.mjs +19 -0
  67. package/fixtures/sample-app/src/hooks/useChat.ts +15 -0
  68. package/fixtures/sample-app/src/lib/ai/client.ts +5 -0
  69. package/fixtures/sample-app/src/schemas/user.ts +6 -0
  70. package/fixtures/sample-app/src/services/chat.ts +17 -0
  71. package/fixtures/sample-app/src/services/user.ts +10 -0
  72. package/fixtures/sample-app/src/utils/sync.ts +16 -0
  73. package/package.json +42 -0
  74. 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): Promise<number>;