Skip to content

Commit eb4ef5b

Browse files
committed
feat: ship doxy verify v0.1
1 parent ee9ba63 commit eb4ef5b

20 files changed

Lines changed: 1136 additions & 109 deletions

File tree

README.md

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
---
2626

27-
Think of doxy as **"caniuse for npm packages"** — it reads your lockfile versions and curated API data to find mismatches at lint time, with zero runtime cost.
27+
Think of doxy as **"caniuse for npm packages"** — it reads your installed or declared dependency versions and curated API data to find mismatches at lint time, with zero runtime cost.
2828

2929
```
3030
$ doxy verify
@@ -57,11 +57,10 @@ $ doxy verify
5757
- **Removed API detection** — errors when you use APIs removed in your installed version
5858
- **Future API detection** — errors when you use APIs that require a newer version than installed
5959
- **Wrong arity detection** — errors when you call functions with the wrong number of arguments
60-
- **Incremental analysis** — only re-analyzes changed files using git diff + content hashing
6160
- **Inline suppression** — silence specific findings with `// doxy-ignore` comments
6261
- **Config-level suppression** — suppress patterns project-wide with glob-based rules
63-
- **Multiple output formats** — human-readable, JSON, JSONL, SARIF
64-
- **Framework-aware** — understands React/Next.js import patterns and re-exports
62+
- **Multiple output formats** — human-readable, JSON, JSONL
63+
- **Framework-aware** — understands React import patterns and ReactDOM subpaths
6564
- **Fast** — powered by SWC for parsing (~100x faster than TypeScript compiler)
6665

6766
## Installation
@@ -86,14 +85,11 @@ yarn add -D doxy
8685
# Run verification on your project
8786
npx doxy verify
8887

89-
# Only check changed files (great for CI)
90-
npx doxy verify --changed
91-
9288
# Output as JSON for tooling integration
9389
npx doxy verify --json
9490

95-
# Get detailed info about a specific finding
96-
npx doxy explain dxy_a1b2c3d4
91+
# Output as JSONL for agent or streaming workflows
92+
npx doxy verify --jsonl
9793
```
9894

9995
## CLI Reference
@@ -103,32 +99,14 @@ npx doxy explain dxy_a1b2c3d4
10399
| Command | Description |
104100
|---|---|
105101
| `doxy verify [files...]` | Run verification (default command) |
106-
| `doxy init` | Initialize doxy in your project |
107-
| `doxy explain <finding-id>` | Detailed explanation of a finding |
108-
| `doxy cache status` | Show cache statistics |
109-
| `doxy cache clear` | Delete cached data |
110-
| `doxy authority list` | List loaded authority packages |
111-
| `doxy authority update` | Pull latest authority data |
112-
| `doxy authority show <pkg> [export]` | Inspect authority data for a package |
113-
| `doxy fix [files...]` | Apply auto-fixes |
114102

115103
### Verify Flags
116104

117105
| Flag | Description |
118106
|---|---|
119107
| `--json` | Output findings as JSON |
120108
| `--jsonl` | Output findings as newline-delimited JSON |
121-
| `--sarif` | Output findings in SARIF format |
122-
| `--severity <level>` | Minimum severity to report (default: `warning`) |
123-
| `--fail-on <level>` | Exit non-zero threshold (default: `error`) |
124-
| `--changed` | Only analyze changed files |
125-
| `--base <ref>` | Git ref for diff base |
126-
| `--no-cache` | Disable caching |
127-
| `--framework <name@version>` | Override framework detection |
128-
| `--save-baseline` | Save current findings as baseline |
129-
| `--update-baseline` | Update baseline to current findings |
130-
| `--include-baseline` | Show baseline findings in output |
131-
| `--include-suppressed` | Show suppressed findings in output |
109+
| `[files...]` | Optional explicit files to analyze instead of config globs |
132110

133111
### Exit Codes
134112

@@ -217,7 +195,7 @@ Lockfile ─────┘ │ │ │
217195
3. **Query** — Each symbol is checked against curated authority data for your installed version
218196
4. **Emit** — Findings are generated with severity, messages, and fix suggestions
219197
5. **Filter** — Inline and config-level suppressions are applied
220-
6. **Cache**Results are cached per-file with smart invalidation
198+
6. **Report**Findings are emitted in human, JSON, or JSONL format
221199

222200
doxy never executes your code. It reads your lockfile for installed versions and uses curated API specifications to detect issues statically.
223201

@@ -226,9 +204,8 @@ doxy never executes your code. It reads your lockfile for installed versions and
226204
| Framework | Status | Packages |
227205
|---|---|---|
228206
| React | Supported | `react`, `react-dom` |
229-
| Next.js | Planned | `next` |
230207

231-
Authority data currently covers **27 API specs** across React and ReactDOM, including hooks, lifecycle methods, rendering APIs, and more.
208+
Authority data currently covers React and ReactDOM APIs, including hooks, lifecycle methods, rendering APIs, and deprecation timelines.
232209

233210
## Finding Kinds
234211

@@ -238,7 +215,6 @@ Authority data currently covers **27 API specs** across React and ReactDOM, incl
238215
| `removed-api` | error | API was removed in your installed version |
239216
| `future-api` | error | API requires a newer version than installed |
240217
| `wrong-arity` | error | Function called with wrong number of arguments |
241-
| `wrong-param` | error | Function called with wrong parameter names |
242218
| `unknown-export` | info | Export not found in authority data |
243219

244220
## CI Integration
@@ -249,12 +225,6 @@ Authority data currently covers **27 API specs** across React and ReactDOM, incl
249225
run: npx doxy verify --fail-on error
250226
```
251227
252-
```yaml
253-
# With JSON output for annotations
254-
- name: Check API compatibility
255-
run: npx doxy verify --json > doxy-results.json
256-
```
257-
258228
doxy writes **findings to stdout** and **everything else to stderr**, making it easy to pipe and parse output in CI pipelines.
259229
260230
## Contributing
@@ -273,17 +243,17 @@ npm run check # typecheck + lint + test
273243
```
274244
doxy/
275245
├── src/
276-
│ ├── cli/ CLI entry point + commands
246+
│ ├── cli/ `verify` command + reporters
277247
│ ├── core/ Pure analysis logic
278248
│ │ ├── types/ All shared type definitions
279249
│ │ ├── repo-context/ Version detection from manifests
250+
│ │ ├── files/ File discovery from config globs
280251
│ │ ├── import-resolver/ Import → package/export mapping
252+
│ │ ├── analyzer/ Per-file analysis orchestration
281253
│ │ ├── suppression/ Inline + config suppression
282-
│ │ └── analyzer/ Per-file analysis orchestration
283254
│ ├── authority/ Authority data store
284255
│ ├── adapters/ Framework-specific adapters
285256
│ ├── parser/ SWC-based AST parsing
286-
│ └── incremental/ Git diff + caching
287257
├── authority-data/ Curated API spec datasets
288258
└── fixtures/ Test fixture mini-projects
289259
```

fixtures/react-18-wrong-arity/expected-findings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[
22
{
3-
"longId": "dxy:react/useState:src/App.tsx:5:32",
3+
"longId": "dxy:react/useState:src/App.tsx:5:29",
44
"kind": "wrong-arity",
55
"severity": "error",
66
"message": "react.useState expects 0-1 arguments, but was called with 2."
77
},
88
{
9-
"longId": "dxy:react/useReducer:src/App.tsx:8:36",
9+
"longId": "dxy:react/useReducer:src/App.tsx:8:29",
1010
"kind": "wrong-arity",
1111
"severity": "error",
1212
"message": "react.useReducer expects 2-3 arguments, but was called with 0."

fixtures/react-19-removed/expected-findings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"message": "react-dom.findDOMNode was removed in 19.0.0. Use refs instead."
1313
},
1414
{
15-
"longId": "dxy:react/PropTypes:src/App.tsx:21:14",
15+
"longId": "dxy:react/PropTypes:src/App.tsx:21:12",
1616
"kind": "removed-api",
1717
"severity": "error",
1818
"message": "react.PropTypes was removed in 19.0.0. Use the 'prop-types' package instead."

package-lock.json

Lines changed: 13 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
"dependencies": {
3333
"@swc/core": "^1.10.0",
3434
"citty": "^0.1.6",
35+
"picomatch": "^4.0.4",
3536
"semver": "^7.6.0",
3637
"simple-git": "^3.27.0",
3738
"zod": "^3.24.0"
3839
},
3940
"devDependencies": {
4041
"@types/node": "^22.0.0",
42+
"@types/picomatch": "^4.0.3",
4143
"@types/semver": "^7.5.0",
4244
"eslint": "^9.0.0",
4345
"typescript": "^5.7.0",

src/cli/config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { readFile } from "node:fs/promises";
2+
import { resolve } from "node:path";
3+
import {
4+
DEFAULT_CONFIG,
5+
DoxyConfigSchema,
6+
type DoxyConfig,
7+
} from "../core/types/index.js";
8+
9+
export async function loadConfig(root: string): Promise<DoxyConfig> {
10+
const configPath = resolve(root, "doxy.config.json");
11+
12+
try {
13+
const raw = await readFile(configPath, "utf-8");
14+
return DoxyConfigSchema.parse(JSON.parse(raw));
15+
} catch (error) {
16+
if (isMissingFile(error)) {
17+
return DEFAULT_CONFIG;
18+
}
19+
throw error;
20+
}
21+
}
22+
23+
function isMissingFile(error: unknown): boolean {
24+
return (
25+
error instanceof Error &&
26+
"code" in error &&
27+
error.code === "ENOENT"
28+
);
29+
}

src/cli/index.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env node
2+
3+
import { ExitCode } from "../core/types/index.js";
4+
import { runVerify } from "./verify.js";
5+
6+
type ReportFormat = "human" | "json" | "jsonl";
7+
8+
async function main(): Promise<void> {
9+
const rawArgs = process.argv.slice(2);
10+
const args = rawArgs[0] === "verify" ? rawArgs.slice(1) : rawArgs;
11+
12+
let format: ReportFormat = "human";
13+
const files: string[] = [];
14+
15+
for (const arg of args) {
16+
if (arg === "--json") {
17+
format = "json";
18+
continue;
19+
}
20+
if (arg === "--jsonl") {
21+
format = "jsonl";
22+
continue;
23+
}
24+
files.push(arg);
25+
}
26+
27+
if (args.includes("--json") && args.includes("--jsonl")) {
28+
process.stderr.write("Choose either --json or --jsonl, not both.\n");
29+
process.exitCode = ExitCode.CONFIG_ERROR;
30+
return;
31+
}
32+
33+
const result = await runVerify({
34+
root: process.cwd(),
35+
files,
36+
format,
37+
});
38+
39+
if (result.stdout) {
40+
process.stdout.write(result.stdout);
41+
}
42+
if (result.stderr) {
43+
process.stderr.write(result.stderr);
44+
}
45+
46+
process.exitCode = result.exitCode;
47+
}
48+
49+
await main();

src/cli/reporters.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Finding } from "../core/types/index.js";
2+
3+
export type ReportFormat = "human" | "json" | "jsonl";
4+
5+
export interface ReportSummary {
6+
total: number;
7+
errors: number;
8+
warnings: number;
9+
info: number;
10+
}
11+
12+
export function summarizeFindings(findings: Finding[]): ReportSummary {
13+
return {
14+
total: findings.length,
15+
errors: findings.filter((finding) => finding.severity === "error").length,
16+
warnings: findings.filter((finding) => finding.severity === "warning").length,
17+
info: findings.filter((finding) => finding.severity === "info").length,
18+
};
19+
}
20+
21+
export function renderReport(
22+
findings: Finding[],
23+
format: ReportFormat,
24+
): string {
25+
const summary = summarizeFindings(findings);
26+
27+
switch (format) {
28+
case "json":
29+
return JSON.stringify(
30+
{
31+
tool: "doxy",
32+
version: "0.1.0",
33+
findings,
34+
summary,
35+
},
36+
null,
37+
2,
38+
);
39+
case "jsonl":
40+
return [
41+
...findings.map((finding) => JSON.stringify({ type: "finding", finding })),
42+
JSON.stringify({ type: "summary", summary }),
43+
].join("\n");
44+
case "human":
45+
default:
46+
return renderHuman(findings, summary);
47+
}
48+
}
49+
50+
function renderHuman(findings: Finding[], summary: ReportSummary): string {
51+
if (findings.length === 0) {
52+
return "0 findings\n";
53+
}
54+
55+
const lines: string[] = [];
56+
let currentFile: string | undefined;
57+
58+
for (const finding of findings) {
59+
if (finding.location.file !== currentFile) {
60+
if (currentFile) {
61+
lines.push("");
62+
}
63+
currentFile = finding.location.file;
64+
lines.push(currentFile);
65+
}
66+
67+
lines.push(
68+
` ${finding.location.line}:${finding.location.column} ${finding.severity} ${finding.message} ${finding.kind} ${finding.id}`,
69+
);
70+
71+
const bestFix = finding.fixes[0];
72+
if (bestFix?.description) {
73+
lines.push(` ${bestFix.description}`);
74+
}
75+
}
76+
77+
lines.push("");
78+
lines.push(
79+
`${summary.total} findings (${summary.errors} errors, ${summary.warnings} warnings, ${summary.info} info)`,
80+
);
81+
82+
return `${lines.join("\n")}\n`;
83+
}

0 commit comments

Comments
 (0)