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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# The kill test
|
|
2
|
+
|
|
3
|
+
**The one empirical question:** can an agent generate ast-grep rules good
|
|
4
|
+
enough that we'd trust them in CI — from intent-only descriptions, on a real
|
|
5
|
+
messy repo?
|
|
6
|
+
|
|
7
|
+
Everything after this (spec, harness, skill, launch) is gated on the answer.
|
|
8
|
+
This is prototype-shaped: throwaway code that answers a question.
|
|
9
|
+
|
|
10
|
+
## Method
|
|
11
|
+
|
|
12
|
+
1. Pick a **real, messy repo** (Nick's Effect codebase — actual conventions,
|
|
13
|
+
actual aliasing, actual ugly code).
|
|
14
|
+
2. Write **rule intents as one-liners only** (the exact sentences from
|
|
15
|
+
[example-catalog.md](example-catalog.md)). No hand-tuning allowed.
|
|
16
|
+
3. Have the agent generate each rule + its fixtures into ast-grep YAML.
|
|
17
|
+
4. Run `ast-grep scan` on the repo. **Hand-count** false positives and check
|
|
18
|
+
seeded/known violations for recall.
|
|
19
|
+
5. **One repair round** allowed per rule: "this finding is wrong because X"
|
|
20
|
+
→ agent revises rule *and* fixtures.
|
|
21
|
+
6. Re-count. Record everything.
|
|
22
|
+
|
|
23
|
+
## The example spread (tier-sampled)
|
|
24
|
+
|
|
25
|
+
| Intent | Tier |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Find every remaining import of the old design-system Button | P |
|
|
28
|
+
| Find direct imports of `openai`/`anthropic` outside `lib/ai/` | P |
|
|
29
|
+
| Find route handlers that touch the DB before calling `requireUser()` | R |
|
|
30
|
+
| Find `fetch` calls without an `AbortSignal` | R |
|
|
31
|
+
| Find `.map(async ...)` results never wrapped in `Promise.all` | R |
|
|
32
|
+
| Only `api/` may import from `db/` | M |
|
|
33
|
+
|
|
34
|
+
(Adjust to the target repo's real conventions — the point is the tier spread,
|
|
35
|
+
not these exact rules.)
|
|
36
|
+
|
|
37
|
+
## Success criteria
|
|
38
|
+
|
|
39
|
+
- **Precision ≥ 90%** on the relational rules after one repair round
|
|
40
|
+
(CI-guard tolerance; pattern rules should be higher)
|
|
41
|
+
- **Recall ≥ 80%** on seeded/known violations
|
|
42
|
+
- Generated rules readable at a glance by a skeptical human in < 2 minutes
|
|
43
|
+
|
|
44
|
+
## Kill / pivot criteria
|
|
45
|
+
|
|
46
|
+
- Pattern tier can't express more than half the ruleset → evidence for the
|
|
47
|
+
oxlint semantic tier (D4 trigger), *before* any SDK is built
|
|
48
|
+
- Precision lands < 75% even after repair → the premise is shaky; write the
|
|
49
|
+
post-mortem anyway (it's still the launch post)
|
|
50
|
+
|
|
51
|
+
## Deliverable
|
|
52
|
+
|
|
53
|
+
The numbers themselves are the launch content:
|
|
54
|
+
"Can an agent write correct lint rules? I measured it."
|
package/docs/research.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Research notes
|
|
2
|
+
|
|
3
|
+
Landscape as of 2026-08-28. Refresh before making claims public.
|
|
4
|
+
|
|
5
|
+
## React Doctor (millionco/react-doctor) — verified teardown
|
|
6
|
+
|
|
7
|
+
The inspiration. Monorepo (pnpm/Turborepo, TypeScript) with a telling
|
|
8
|
+
structure:
|
|
9
|
+
|
|
10
|
+
- `core` — shared rule engine (rules authored once)
|
|
11
|
+
- `eslint-plugin-react-doctor` + `oxlint-plugin-react-doctor` — **two
|
|
12
|
+
first-class executors**; rules delivered through both ESLint and oxlint
|
|
13
|
+
- `evals` + `fuzz` — rule-quality measurement as first-class packages
|
|
14
|
+
- `react-doctor` CLI — what neither linter can do: project-level analysis,
|
|
15
|
+
CI mode (PR-diff-scoped reporting: "only issues your change introduced"),
|
|
16
|
+
and runtime tracing (`scan <url>` launches isolated-profile Chrome, records
|
|
17
|
+
a DevTools perf trace with per-render component outlines, `--cdp` attach
|
|
18
|
+
for authed sessions)
|
|
19
|
+
|
|
20
|
+
**Lessons for us:** (1) multi-executor endgame is validated — shared rule
|
|
21
|
+
semantics, adapters per engine; (2) eval infrastructure is where the value
|
|
22
|
+
lives — they built `evals`/`fuzz` packages; (3) diff-scoped CI reporting is
|
|
23
|
+
the adoption trick for opinionated rules despite legacy backlogs.
|
|
24
|
+
|
|
25
|
+
Docs: https://www.react.doctor/docs/configuration/eslint-and-oxlint-plugins
|
|
26
|
+
|
|
27
|
+
## Competitive landscape
|
|
28
|
+
|
|
29
|
+
- **Semgrep** — AI rule writing; [Multimodal](https://semgrep.dev/products/semgrep-multimodal)
|
|
30
|
+
combines LLM reasoning with rule-based scanning. Market has already
|
|
31
|
+
converged on "deterministic rules + AI assist" rather than "LLM judges
|
|
32
|
+
everything." Validates the inversion; security-oriented.
|
|
33
|
+
- **ast-grep** — has an [agent skill for NL→YAML rules](https://mcpservers.org/agent-skills/ast-grep/ast-grep).
|
|
34
|
+
We build ON it, not against it. https://github.com/ast-grep/ast-grep
|
|
35
|
+
- **Codemod.com** — NL→codemods (transformation). We do detection-only, which
|
|
36
|
+
is far easier and nobody has productized "temporary migration linters."
|
|
37
|
+
- **Copilot code review / custom instructions** — NL rules applied
|
|
38
|
+
non-deterministically on every PR. The zero-infra baseline.
|
|
39
|
+
- **The free baseline to beat:** ask Claude Code/Cursor to write the rule and
|
|
40
|
+
commit it. What's missing end-to-end today: sentence → *auditable rule with
|
|
41
|
+
fixtures* → CI forever. The harness, the fixture discipline, and the trust
|
|
42
|
+
artifacts are the product; generation is commoditizing.
|
|
43
|
+
|
|
44
|
+
## ast-grep vs oxc (decision D3/D4 summary)
|
|
45
|
+
|
|
46
|
+
| | ast-grep | oxc/oxlint |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Rule artifact | YAML, declarative, ~10–30 lines | JS visitor code, free-form |
|
|
49
|
+
| Analysis | per-file patterns + `inside`/`has`/`precedes` | scope tree, symbols, CFG, types (tsgolint) |
|
|
50
|
+
| Alias/import resolution | no | yes |
|
|
51
|
+
| Native tests | `ast-grep test` snapshots | roll your own |
|
|
52
|
+
| Languages | dozens (tree-sitter) | JS/TS |
|
|
53
|
+
| Generated-artifact safety | data | arbitrary code in repo/CI |
|
|
54
|
+
|
|
55
|
+
oxc plugins docs: https://oxc.rs/docs/guide/usage/linter/plugins.html
|
|
56
|
+
|
|
57
|
+
## Key technical facts
|
|
58
|
+
|
|
59
|
+
- ast-grep relational operators (`inside`, `has`, `precedes`, `follows`) make
|
|
60
|
+
"X inside Y" and even auth-*ordering* rules declarative.
|
|
61
|
+
- `ast-grep test` + YAML test cases = the fixture harness, mostly free.
|
|
62
|
+
- Effect is a deceptively *friendly* first domain: very regular idioms
|
|
63
|
+
(gen/yield*/Tag), strong model priors — good for demo, stacks the deck;
|
|
64
|
+
validate on a messier domain before generalizing.
|
|
65
|
+
- Precision/recall tolerance flips by mode: audit scanner wants recall, CI
|
|
66
|
+
guard wants ~95%+ precision (muted rules die).
|
package/docs/vision.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Vision
|
|
2
|
+
|
|
3
|
+
## The problem
|
|
4
|
+
|
|
5
|
+
Every team has a dozen conventions nobody ever wrote a linter for: things
|
|
6
|
+
that keep going wrong, enforced by code review and Slack threads and
|
|
7
|
+
senior-dev memory. Hand-writing lint rules is the friction that kills
|
|
8
|
+
adoption — so the rules never get written. Meanwhile "just have the LLM
|
|
9
|
+
read the codebase and find violations" is non-deterministic, costs
|
|
10
|
+
inference on every run, and can't gate a CI pipeline.
|
|
11
|
+
|
|
12
|
+
## The core inversion
|
|
13
|
+
|
|
14
|
+
> The LLM writes the analyzer. Fixtures prove it. CI reruns it forever.
|
|
15
|
+
|
|
16
|
+
One LLM invocation compiles your one-sentence convention into a doctor
|
|
17
|
+
program — real code written against our typed `ctx` SDK (the pattern
|
|
18
|
+
Cloudflare calls Code Mode; we run it entirely locally, no vendor). After
|
|
19
|
+
that: same repo + same commit = same findings, forever, at zero inference
|
|
20
|
+
cost.
|
|
21
|
+
|
|
22
|
+
## The experience
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
any-doctor generate "find fetch calls without an AbortSignal"
|
|
26
|
+
any-doctor run . # report, then walk the findings interactively
|
|
27
|
+
any-doctor verify --all # the trust gate, for every doctor you own
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The bar is React Doctor's CLI: fast scan line, grouped findings with
|
|
31
|
+
file:line evidence, severity glyphs, declared blind spots, and a
|
|
32
|
+
hand-off point where the findings feed your LLM to fix. The analyzer
|
|
33
|
+
itself is generated on demand and scoped to exactly what you asked about.
|
|
34
|
+
|
|
35
|
+
## The novel axis: lifecycle, not domain
|
|
36
|
+
|
|
37
|
+
Existing linters assume rules are permanent artifacts written by experts
|
|
38
|
+
and shipped in a registry. Any Doctor's moves:
|
|
39
|
+
|
|
40
|
+
1. **Authoring cost → a sentence.** From intent to fixture-gated doctor
|
|
41
|
+
in one agent session.
|
|
42
|
+
2. **Rules can be temporary.** Migration audits ("find every remaining
|
|
43
|
+
import of the old design-system Button") are write-one-run-once-delete.
|
|
44
|
+
Upgrade doctors compile a library changelog into pre-upgrade detection
|
|
45
|
+
rules — `tsc` catches mechanical breaks *after* you upgrade; this
|
|
46
|
+
estimates the cost *before*.
|
|
47
|
+
3. **Doctors encode *your* architecture**, not a framework's — the
|
|
48
|
+
opinionated house rules that could never live in a public plugin.
|
|
49
|
+
|
|
50
|
+
The JS family (TS/JS/JSX/Vue/Svelte) is the native range; Effect was the
|
|
51
|
+
first dogfood domain because its idioms are regular and underserved.
|
|
52
|
+
|
|
53
|
+
## Trust model
|
|
54
|
+
|
|
55
|
+
- Every doctor ships fixtures (seeded files + exact-set expected findings).
|
|
56
|
+
`verify` is the gate: missing expected findings fail recall, unexpected
|
|
57
|
+
findings fail precision.
|
|
58
|
+
- Generated programs declare blind spots as data; reports print them.
|
|
59
|
+
- The CLI independently re-verifies anything an agent produces — an
|
|
60
|
+
agent's output is never trusted on its own word.
|
|
61
|
+
- Doctors are boring files in your repo: auditable, diffable, reviewable
|
|
62
|
+
in PRs. The runner seam (today a node child process) is where a
|
|
63
|
+
technical sandbox plugs in when doctors become third-party.
|
|
64
|
+
|
|
65
|
+
## Two modes, opposite tolerances
|
|
66
|
+
|
|
67
|
+
- **Audit scanner** ("find every external HTTP endpoint we call"): recall
|
|
68
|
+
matters; humans triage.
|
|
69
|
+
- **CI guard** (saved doctors gating PRs): precision is king — a rule with
|
|
70
|
+
a few percent false-positive rate gets muted within a week.
|
|
71
|
+
|
|
72
|
+
The funnel is audit → save → CI, and the tolerances flip mid-funnel.
|
|
73
|
+
|
|
74
|
+
## Non-goals (v0)
|
|
75
|
+
|
|
76
|
+
- ~~Scores/dashboards until precision justifies them~~ — revisited: the
|
|
77
|
+
score header and review browser shipped, because findings carry
|
|
78
|
+
file/line evidence and fixture-proven precision, so the number
|
|
79
|
+
summarizes rather than substitutes.
|
|
80
|
+
- Languages beyond the JS family.
|
|
81
|
+
- Hand-rolled parser/engine internals — engines live behind `ctx` as
|
|
82
|
+
replaceable primitives (ast-grep today, oxc-backed semantics later).
|
|
83
|
+
- Cloudflare or any hosted component. The Code Mode pattern, fully local.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# any-doctor — doctor generation skill
|
|
2
|
+
|
|
3
|
+
You are writing a **doctor program**: a small analysis tool that detects one
|
|
4
|
+
codebase convention, plus the fixtures that prove it works. The INTENT is
|
|
5
|
+
the entire specification. Derive everything from the intent alone — never
|
|
6
|
+
scan a target repository to tune your doctor against its code.
|
|
7
|
+
|
|
8
|
+
## The contract
|
|
9
|
+
|
|
10
|
+
Two files, exactly:
|
|
11
|
+
|
|
12
|
+
`doctors/<slug>.mjs` (slug = short kebab-case from the intent):
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
export const meta = {
|
|
16
|
+
id: "<slug>", // kebab-case, matches the filename
|
|
17
|
+
description: "<one-line finding text, shown in reports>",
|
|
18
|
+
severity: "warning", // "error" | "warning" | "info"
|
|
19
|
+
blindSpots: [ // what this approach CANNOT see — required
|
|
20
|
+
"<honest limitation>",
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function doctor(ctx) {
|
|
25
|
+
// inspect the codebase, emit findings
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`doctors/<slug>.fixtures.mjs`:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
export const fixtures = [
|
|
33
|
+
{
|
|
34
|
+
name: "<what this case proves>",
|
|
35
|
+
seed: { "src/example.ts": "...inline file contents..." },
|
|
36
|
+
expected: [{ file: "src/example.ts", line: 2 }], // exact set on (file, line)
|
|
37
|
+
},
|
|
38
|
+
// include cases where expected: [] — innocent lookalikes must stay silent
|
|
39
|
+
];
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ctx` is the only capability a doctor has:
|
|
43
|
+
|
|
44
|
+
- `ctx.files.list(exts?)` → relative paths (default .ts/.tsx/.js/.jsx/.mjs)
|
|
45
|
+
- `ctx.files.read(rel)` → file contents
|
|
46
|
+
- `ctx.search.pattern(pattern, language?)` → `[{ file, line, column, text }]`
|
|
47
|
+
(ast-grep pattern syntax, e.g. `"fetch($URL)"`; requires ast-grep installed)
|
|
48
|
+
- `ctx.report.finding({ file, line, column?, message?, severity? })`
|
|
49
|
+
|
|
50
|
+
Zero dependencies. Node >= 18. The only imports allowed are `node:` builtins.
|
|
51
|
+
|
|
52
|
+
## Hard workflow
|
|
53
|
+
|
|
54
|
+
1. Write both files.
|
|
55
|
+
2. Run: `any-doctor verify doctors/<slug>.mjs` (or `node bin/cli.js verify doctors/<slug>.mjs`).
|
|
56
|
+
3. ALL fixtures must pass. Iterate until green — a failed fixture is the
|
|
57
|
+
system telling you your rule or your expectations are wrong; fix the rule,
|
|
58
|
+
or fix the expectation if the expectation itself was wrong.
|
|
59
|
+
4. Report: id, what it detects, declared blind spots, fixture count.
|
|
60
|
+
|
|
61
|
+
## Fixture discipline
|
|
62
|
+
|
|
63
|
+
- Matching is exact-set on (file, line): a missing expected finding fails
|
|
64
|
+
(recall); an unexpected extra finding fails (precision). Get both sides right.
|
|
65
|
+
- Every doctor needs at least one `expected: []` fixture containing code that
|
|
66
|
+
LOOKS like a violation but isn't — the lookalike that a naive implementation
|
|
67
|
+
would wrongly flag.
|
|
68
|
+
- Cover the simplest violation and the sneakiest one. Vary quote styles where
|
|
69
|
+
syntax allows.
|
|
70
|
+
- Findings are locations. Wording lives in `meta.description`; use
|
|
71
|
+
per-finding `message` only when one violation needs its own explanation.
|
|
72
|
+
|
|
73
|
+
## Honesty rules
|
|
74
|
+
|
|
75
|
+
- `blindSpots` is required and is data: declare what your approach cannot
|
|
76
|
+
see (aliased imports, cross-file references, dynamic constructs).
|
|
77
|
+
- Never silently narrow the intent. If the intent is not fully statically
|
|
78
|
+
checkable, implement the closest honest version and declare the gap.
|
|
79
|
+
- A clean run must mean clean. Never swallow errors into empty findings.
|
|
80
|
+
|
|
81
|
+
## Failure lessons (each of these shipped in a real doctor — do not repeat)
|
|
82
|
+
|
|
83
|
+
1. `String.replace(text, x)` replaces only the FIRST occurrence. When
|
|
84
|
+
substituting placeholders into a regex source string, use a global regex:
|
|
85
|
+
`.replace(/NAME/g, escaped)`.
|
|
86
|
+
2. Escape anything variable you interpolate into a RegExp
|
|
87
|
+
(`s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")`).
|
|
88
|
+
3. Match the innermost construct once. `await fetch(x)` contains a
|
|
89
|
+
`fetch(x)` node — a pattern for the inner call already matches it;
|
|
90
|
+
adding wrapper variants double-reports.
|
|
91
|
+
4. Read each file once per pass. Don't re-read inside loops.
|
|
92
|
+
5. If the intent is not fully statically checkable (cross-file references,
|
|
93
|
+
runtime behavior, types), implement the closest honest version and
|
|
94
|
+
declare the gap in `blindSpots`. Never drop an intent condition silently.
|
|
95
|
+
6. Your own `console.log` goes to stderr, not the report. Don't rely on
|
|
96
|
+
stdout — stdout carries only the structured result.
|
|
97
|
+
|
|
98
|
+
## What NOT to do
|
|
99
|
+
|
|
100
|
+
- Don't touch any file other than the two contract files.
|
|
101
|
+
- Don't add dependencies or imports beyond `node:` builtins.
|
|
102
|
+
- Don't scan a target repository to "tune" the doctor against its code.
|
|
103
|
+
- Don't mark a fixture green by weakening the expectation — strengthen the doctor.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const fixtures = [
|
|
2
|
+
{
|
|
3
|
+
name: "reports an app API route without an auth middleware import",
|
|
4
|
+
seed: {
|
|
5
|
+
"src/app/api/users/route.ts": `export async function GET() {
|
|
6
|
+
return Response.json([]);
|
|
7
|
+
}`,
|
|
8
|
+
},
|
|
9
|
+
expected: [{ file: "src/app/api/users/route.ts", line: 1 }],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "reports a pages API route without an auth middleware import",
|
|
13
|
+
seed: {
|
|
14
|
+
"src/pages/api/account.js": `export default function handler(request, response) {
|
|
15
|
+
response.status(200).json({ ok: true });
|
|
16
|
+
}`,
|
|
17
|
+
},
|
|
18
|
+
expected: [{ file: "src/pages/api/account.js", line: 1 }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "allows a route with a named auth middleware import",
|
|
22
|
+
seed: {
|
|
23
|
+
"src/app/api/private/route.ts": `import { requireUser } from "@/middleware/auth";
|
|
24
|
+
|
|
25
|
+
export async function GET(request) {
|
|
26
|
+
return Response.json({ user: await requireUser(request) });
|
|
27
|
+
}`,
|
|
28
|
+
},
|
|
29
|
+
expected: [],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "allows a side-effect auth import with single quotes",
|
|
33
|
+
seed: {
|
|
34
|
+
"src/pages/api/session.ts": `import '../auth';
|
|
35
|
+
|
|
36
|
+
export default function handler(request, response) {
|
|
37
|
+
response.status(204).end();
|
|
38
|
+
}`,
|
|
39
|
+
},
|
|
40
|
+
expected: [],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "does not treat an auth-looking comment as an import",
|
|
44
|
+
seed: {
|
|
45
|
+
"src/app/api/status/route.ts": `// import { requireUser } from '@/middleware/auth';
|
|
46
|
+
export async function GET() {
|
|
47
|
+
return Response.json({ status: "ok" });
|
|
48
|
+
}`,
|
|
49
|
+
},
|
|
50
|
+
expected: [{ file: "src/app/api/status/route.ts", line: 1 }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "does not inspect non-API modules with auth-looking code",
|
|
54
|
+
seed: {
|
|
55
|
+
"src/components/auth-card.tsx": `export function AuthCard() {
|
|
56
|
+
return <div>API route</div>;
|
|
57
|
+
}`,
|
|
58
|
+
},
|
|
59
|
+
expected: [],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
id: "api-route-files-do-import",
|
|
3
|
+
description: "API route file does not import auth middleware.",
|
|
4
|
+
severity: "warning",
|
|
5
|
+
blindSpots: [
|
|
6
|
+
"Identifies API route files only by an api path segment, so unconventional route locations are not checked and non-route modules within an api directory can be reported.",
|
|
7
|
+
"Recognizes auth middleware only through static ES module import specifiers containing auth or middleware; aliases, dynamic imports, re-exports, and middleware applied by framework configuration are not recognized.",
|
|
8
|
+
"Cannot determine whether an imported auth or middleware module actually protects the route at runtime.",
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const apiDirectory = /(?:^|\/)api(?:\/|$)/;
|
|
13
|
+
const authMiddlewareImport = /^\s*import\s*(?:[\s\S]*?\sfrom\s*)?["']([^"']*(?:auth|middleware)[^"']*)["']\s*;?/gim;
|
|
14
|
+
|
|
15
|
+
export async function doctor(ctx) {
|
|
16
|
+
for (const file of await ctx.files.list()) {
|
|
17
|
+
if (!apiDirectory.test(file)) continue;
|
|
18
|
+
|
|
19
|
+
const source = await ctx.files.read(file);
|
|
20
|
+
authMiddlewareImport.lastIndex = 0;
|
|
21
|
+
|
|
22
|
+
if (!authMiddlewareImport.test(source)) {
|
|
23
|
+
ctx.report.finding({ file, line: 1 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const fixtures = [
|
|
2
|
+
{
|
|
3
|
+
"name": "reports a fetch call without options",
|
|
4
|
+
"seed": {
|
|
5
|
+
"src/request.ts": "fetch('/api/users');\n"
|
|
6
|
+
},
|
|
7
|
+
"expected": [
|
|
8
|
+
{
|
|
9
|
+
"file": "src/request.ts",
|
|
10
|
+
"line": 1
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "reports options that omit signal",
|
|
16
|
+
"seed": {
|
|
17
|
+
"src/request.ts": "fetch(\"/api/users\", { method: 'POST' });\n"
|
|
18
|
+
},
|
|
19
|
+
"expected": [
|
|
20
|
+
{
|
|
21
|
+
"file": "src/request.ts",
|
|
22
|
+
"line": 1
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "accepts an explicit signal property among other options",
|
|
28
|
+
"seed": {
|
|
29
|
+
"src/request.ts": "fetch('/api/users', { method: 'GET', signal: controller.signal });\n"
|
|
30
|
+
},
|
|
31
|
+
"expected": []
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "accepts a shorthand signal property",
|
|
35
|
+
"seed": {
|
|
36
|
+
"src/request.ts": "fetch('/api/users', { signal, headers: {} });\n"
|
|
37
|
+
},
|
|
38
|
+
"expected": []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "reports a nested signal that is not a RequestInit signal",
|
|
42
|
+
"seed": {
|
|
43
|
+
"src/request.ts": "fetch('/api/users', { headers: { signal: 'not-an-abort-signal' } });\n"
|
|
44
|
+
},
|
|
45
|
+
"expected": [
|
|
46
|
+
{
|
|
47
|
+
"file": "src/request.ts",
|
|
48
|
+
"line": 1
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "reports an options variable whose signal cannot be determined",
|
|
54
|
+
"seed": {
|
|
55
|
+
"src/request.ts": "const options = { signal: controller.signal };\nfetch('/api/users', options);\n"
|
|
56
|
+
},
|
|
57
|
+
"expected": [
|
|
58
|
+
{
|
|
59
|
+
"file": "src/request.ts",
|
|
60
|
+
"line": 2
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "unawaited map(async) is flagged",
|
|
66
|
+
"seed": {
|
|
67
|
+
"src/a.ts": "export function work(ids: string[]) {\n const results = ids.map(async id => load(id))\n return results.length\n}"
|
|
68
|
+
},
|
|
69
|
+
"expected": [
|
|
70
|
+
{
|
|
71
|
+
"file": "src/a.ts",
|
|
72
|
+
"line": 2
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "Promise.all-consumed result is not flagged",
|
|
78
|
+
"seed": {
|
|
79
|
+
"src/b.ts": "export async function work(ids: string[]) {\n const results = ids.map(async id => load(id))\n return Promise.all(results)\n}"
|
|
80
|
+
},
|
|
81
|
+
"expected": []
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "directly awaited result is not flagged",
|
|
85
|
+
"seed": {
|
|
86
|
+
"src/c.ts": "export async function work(ids: string[]) {\n const results = ids.map(async id => load(id))\n await results\n}"
|
|
87
|
+
},
|
|
88
|
+
"expected": []
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "allSettled-consumed result in a later batch loop is not flagged",
|
|
92
|
+
"seed": {
|
|
93
|
+
"src/d.ts": "export async function work(items: string[]) {\n const allPromises = items.map(async item => load(item))\n const allResults = await Promise.allSettled(allPromises)\n return allResults\n}"
|
|
94
|
+
},
|
|
95
|
+
"expected": []
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "reports an assigned timeout without cleanup",
|
|
99
|
+
"seed": {
|
|
100
|
+
"src/example.tsx": "useEffect(() => {\n const timer = setTimeout(() => save(), 1000);\n}, []);\n"
|
|
101
|
+
},
|
|
102
|
+
"expected": [
|
|
103
|
+
{
|
|
104
|
+
"file": "src/example.tsx",
|
|
105
|
+
"line": 2
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "allows cleanup of the assigned timeout",
|
|
111
|
+
"seed": {
|
|
112
|
+
"src/example.tsx": "useEffect(() => {\n const timer = setTimeout(() => save(), 1000);\n return () => clearTimeout(timer);\n}, []);\n"
|
|
113
|
+
},
|
|
114
|
+
"expected": []
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "reports only the timer missing from multi-timer cleanup",
|
|
118
|
+
"seed": {
|
|
119
|
+
"src/example.jsx": "useEffect(() => {\n const fast = setTimeout(work, 50);\n let slow = setTimeout(work, 500);\n return () => clearTimeout(fast);\n}, []);\n"
|
|
120
|
+
},
|
|
121
|
+
"expected": [
|
|
122
|
+
{
|
|
123
|
+
"file": "src/example.jsx",
|
|
124
|
+
"line": 3
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "reports an unassigned timer despite unrelated cleanup",
|
|
130
|
+
"seed": {
|
|
131
|
+
"src/example.js": "useEffect(() => {\n setTimeout(notify, 100);\n return () => clearTimeout(existingTimer);\n}, []);\n"
|
|
132
|
+
},
|
|
133
|
+
"expected": [
|
|
134
|
+
{
|
|
135
|
+
"file": "src/example.js",
|
|
136
|
+
"line": 2
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "ignores timeout lookalikes outside effects and in comments",
|
|
142
|
+
"seed": {
|
|
143
|
+
"src/example.ts": "const message = 'setTimeout(callback, 10)';\n// useEffect(() => setTimeout(callback, 10))\nfunction later() {\n setTimeout(callback, 10);\n}\n"
|
|
144
|
+
},
|
|
145
|
+
"expected": []
|
|
146
|
+
}
|
|
147
|
+
];
|