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
@@ -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."
@@ -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
+ ];