Skip to content

Commit 542af7f

Browse files
fix(core): relativize pushed finding paths + point docs urls at docs.the-doctor.report
Two dashboard-facing bugs in the findings push pipeline: 1. stripFindings forwarded the auditor's absolute file paths, so the SaaS dashboard rendered CI runner internals like /home/runner/work/v-clappr/v-clappr/src/… for every finding. stripFindings now takes the project rootDir and relativizes absolute paths against it (paths outside rootDir stay untouched rather than escaping via ../). Both CLIs pass rootDir through. 2. docsUrl still pointed at the dead docs.doctor.geoql.in domain (DNS no longer resolves) — the dashboard's per-rule docs links 404'd before even reaching the docs site. Now docs.the-doctor.report, matching the live docs deployment. Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>
1 parent 8455aa9 commit 542af7f

15 files changed

Lines changed: 143 additions & 40 deletions

File tree

README.md

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,104 @@
22

33
> Your agent writes bad Vue/Nuxt. This catches it.
44
5-
A pair of CLIs and oxlint plugins that audit Vue 3 + Nuxt 4 apps for performance, correctness, security, and AI-agent anti-patterns. It does not scaffold or generate — it _critiques_ the code your agent just wrote and gives it a deterministic health score.
5+
[![npm](https://img.shields.io/npm/v/@geoql/vue-doctor?label=vue-doctor)](https://www.npmjs.com/package/@geoql/vue-doctor)
6+
[![npm](https://img.shields.io/npm/v/@geoql/nuxt-doctor?label=nuxt-doctor)](https://www.npmjs.com/package/@geoql/nuxt-doctor)
7+
[![JSR](https://jsr.io/badges/@geoql/vue-doctor)](https://jsr.io/@geoql/vue-doctor)
8+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
69

7-
**Documentation:** [docs.the-doctor.report](https://docs.the-doctor.report)
10+
## What is this?
11+
12+
A pnpm monorepo of CLIs and oxlint plugins that audit Vue 3 and Nuxt 4 apps for performance, correctness, security, and AI-agent anti-patterns. It does not scaffold or generate. It critiques the code your agent just wrote and gives it a deterministic 0-100 health score. The audit runs fully local and offline: same code in, same score out.
13+
14+
Full docs, rule reference, config, and scoring live at [docs.the-doctor.report](https://docs.the-doctor.report).
15+
16+
## Ecosystem
17+
18+
| Repo | What | Link |
19+
| -------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
20+
| `geoql/doctor` (this repo) | OSS CLIs + oxlint rule plugins | [github.com/geoql/doctor](https://github.com/geoql/doctor) |
21+
| `geoql/doctor-action` | GitHub Action wrapper | [github.com/geoql/doctor-action](https://github.com/geoql/doctor-action) |
22+
| `the-doctor.report` | Hosted SaaS dashboard (private) | [app.the-doctor.report](https://app.the-doctor.report) |
23+
| Docs | Rules, CLI, scoring reference | [docs.the-doctor.report](https://docs.the-doctor.report) |
824

925
## Packages
1026

11-
| Package | Description | Version |
12-
| ------------------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
13-
| [`@geoql/doctor-core`](packages/doctor-core) | Audit engine, scoring, reporters | [![npm](https://img.shields.io/npm/v/@geoql/doctor-core)](https://www.npmjs.com/package/@geoql/doctor-core) |
14-
| [`@geoql/oxlint-plugin-vue-doctor`](packages/oxlint-plugin-vue-doctor) | oxlint JS plugin: Vue 3 rules | [![npm](https://img.shields.io/npm/v/@geoql/oxlint-plugin-vue-doctor)](https://www.npmjs.com/package/@geoql/oxlint-plugin-vue-doctor) |
15-
| [`@geoql/oxlint-plugin-nuxt-doctor`](packages/oxlint-plugin-nuxt-doctor) | oxlint JS plugin: Nuxt 4 rules | [![npm](https://img.shields.io/npm/v/@geoql/oxlint-plugin-nuxt-doctor)](https://www.npmjs.com/package/@geoql/oxlint-plugin-nuxt-doctor) |
16-
| [`@geoql/vue-doctor`](packages/vue-doctor) | CLI: `npx -y @geoql/vue-doctor` | [![npm](https://img.shields.io/npm/v/@geoql/vue-doctor)](https://www.npmjs.com/package/@geoql/vue-doctor) |
17-
| [`@geoql/nuxt-doctor`](packages/nuxt-doctor) | CLI: `npx -y @geoql/nuxt-doctor` | [![npm](https://img.shields.io/npm/v/@geoql/nuxt-doctor)](https://www.npmjs.com/package/@geoql/nuxt-doctor) |
27+
| Package | Version | Description |
28+
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
29+
| [`@geoql/doctor-core`](packages/doctor-core) | [![npm](https://img.shields.io/npm/v/@geoql/doctor-core)](https://www.npmjs.com/package/@geoql/doctor-core) | Audit engine: scoring, reporters, hybrid two-pass analysis |
30+
| [`@geoql/vue-doctor`](packages/vue-doctor) | [![npm](https://img.shields.io/npm/v/@geoql/vue-doctor)](https://www.npmjs.com/package/@geoql/vue-doctor) | CLI for Vue 3 projects |
31+
| [`@geoql/nuxt-doctor`](packages/nuxt-doctor) | [![npm](https://img.shields.io/npm/v/@geoql/nuxt-doctor)](https://www.npmjs.com/package/@geoql/nuxt-doctor) | CLI for Nuxt 4 projects |
32+
| [`@geoql/oxlint-plugin-vue-doctor`](packages/oxlint-plugin-vue-doctor) | [![npm](https://img.shields.io/npm/v/@geoql/oxlint-plugin-vue-doctor)](https://www.npmjs.com/package/@geoql/oxlint-plugin-vue-doctor) | oxlint JS plugin: Vue 3 rules |
33+
| [`@geoql/oxlint-plugin-nuxt-doctor`](packages/oxlint-plugin-nuxt-doctor) | [![npm](https://img.shields.io/npm/v/@geoql/oxlint-plugin-nuxt-doctor)](https://www.npmjs.com/package/@geoql/oxlint-plugin-nuxt-doctor) | oxlint JS plugin: Nuxt 4 rules |
34+
35+
The two CLIs ship at `1.1.1`, published on [npm](https://www.npmjs.com/org/geoql) and [JSR](https://jsr.io/@geoql) with OIDC provenance.
1836

1937
## Quick start
2038

2139
```sh
22-
# Audit a Vue 3 project
40+
# Audit a Vue 3 project (npm)
2341
npx -y @geoql/vue-doctor
2442

25-
# Audit a Nuxt 4 project
43+
# Audit a Nuxt 4 project (npm)
2644
npx -y @geoql/nuxt-doctor
45+
46+
# Run from JSR (Deno)
47+
deno run -A jsr:@geoql/vue-doctor
48+
deno run -A jsr:@geoql/nuxt-doctor
2749
```
2850

29-
## Documentation
51+
Both CLIs print a health score with grouped diagnostics and exit non-zero when findings breach your `--fail-on` threshold, so they drop straight into CI.
52+
53+
## Monorepo layout
54+
55+
```
56+
geoql/doctor/
57+
├── packages/
58+
│ ├── doctor-core/ # @geoql/doctor-core — audit engine
59+
│ ├── vue-doctor/ # @geoql/vue-doctor — Vue 3 CLI
60+
│ ├── nuxt-doctor/ # @geoql/nuxt-doctor — Nuxt 4 CLI
61+
│ ├── oxlint-plugin-vue-doctor/ # Vue 3 oxlint rules
62+
│ ├── oxlint-plugin-nuxt-doctor/ # Nuxt 4 oxlint rules
63+
│ └── benchmark/ # perf workspace (not published)
64+
├── apps/docs/ # docs.the-doctor.report (Nuxt)
65+
└── docs/SPEC.md # the locked spec
66+
```
67+
68+
## CLI flags
69+
70+
Both `vue-doctor` and `nuxt-doctor` share the same surface. Key flags:
3071

31-
Full docs — rules, CLI reference, config, and scoring — live at **[docs.the-doctor.report](https://docs.the-doctor.report)**.
72+
| Flag | Purpose |
73+
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
74+
| `--preset <name>` | Base preset: `minimal` \| `recommended` \| `strict` \| `all` |
75+
| `--format <kind>` | `agent` (default) \| `pretty` \| `json` \| `json-compact` \| `sarif` \| `html` \| `pr-comment` |
76+
| `--fail-on <level>` | Exit non-zero on `error` \| `warn` \| `none` (default `error`) |
77+
| `--threshold <n>` | Minimum passing score 0-100 |
78+
| `--rule <id:level>` | Override one rule (repeatable), e.g. `--rule a/b:off` |
79+
| `--fix` | Auto-fix oxlint-pass findings in place (full scan only) |
80+
| `--diff` / `--staged` / `--full` | Scope to changed, staged, or all files |
81+
| `--score` | Print only the integer score |
82+
| `--output <file>` | Write the report to a file |
83+
| `--pr-comment` | Emit a Markdown PR-comment body |
84+
| `--push` | POST privacy-stripped findings to the SaaS (needs `--api-key`) |
85+
| `--push-url <url>` | Findings endpoint (default `https://app.the-doctor.report/api/v1/findings`) |
86+
| `--api-key <key>` | the-doctor.report API key (`doc_…`), sent as the `x-api-key` header |
87+
| `--push-project <slug>` | Dashboard project slug, e.g. `owner/repo` (defaults to the audited dir name) |
3288

33-
Both CLIs run a deterministic audit, print a health score (0–100) with grouped
34-
diagnostics, and exit non-zero when findings breach your `--fail-on` threshold —
35-
so they drop straight into CI. See the [docs](https://docs.the-doctor.report)
36-
for flags (`--fix`, `--ci`, `--sarif`, `--json`, presets) and the full rule set.
89+
Subcommands: `list-rules`, `explain <ruleId>`, `inspect [dir]`, `init [dir]`. Run `vue-doctor --help` for the complete list.
3790

38-
## Status
91+
## Scoring model
3992

40-
`v0.1.0` — published on [npm](https://www.npmjs.com/org/geoql) and [JSR](https://jsr.io/@geoql) with provenance. See the [v1.0 milestone](https://github.com/geoql/doctor/milestone/3) for the roadmap.
93+
Internal severities are `error`, `warn`, and `info` with weights `5`, `2`, and `0.5`. The score uses square-root decay: each repeated occurrence of a rule contributes `weight × 1/√(i+1)`, so the first hit costs full weight and repeats cost less. The final score is `max(0, round(100 − penalty))`. `--fail-on` accepts `error`, `warn`, or `none`. An invalid value exits `2`.
4194

4295
## Development
4396

4497
```sh
4598
pnpm install
46-
pnpm run lint
47-
pnpm run format
99+
pnpm run lint # vp lint
100+
pnpm run format # vp fmt
101+
pnpm run build # vp pack per package
102+
pnpm run coverage # vitest run --coverage (100% gate)
48103
```
49104

50105
Requires Node `>=24` and pnpm `11.4.0`.

packages/doctor-core/src/push.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isAbsolute, relative } from 'node:path';
12
import { RULE_REGISTRY } from './rule-registry.js';
23
import type { Diagnostic, Severity } from './types.js';
34

@@ -37,6 +38,8 @@ export interface PushOptions {
3738
warnCount: number;
3839
infoCount: number;
3940
diagnostics: Diagnostic[];
41+
/** Project root used to relativize absolute finding file paths. */
42+
rootDir?: string;
4043
url: string;
4144
apiKey: string;
4245
fetchImpl?: typeof fetch;
@@ -58,17 +61,32 @@ function lookupCategory(ruleId: string): string {
5861
return categoryByRuleId.get(ruleId) ?? 'unknown';
5962
}
6063

64+
function toRepoRelative(file: string, rootDir: string | undefined): string {
65+
if (!rootDir || !isAbsolute(file)) return file;
66+
const rel = relative(rootDir, file);
67+
// Paths outside rootDir would relativize to ../../… which is more
68+
// confusing (and more revealing) than the original — keep those as-is.
69+
if (rel.startsWith('..')) return file;
70+
return rel;
71+
}
72+
6173
/**
6274
* Strip a list of Diagnostics down to the 6 allowed push fields.
6375
* Privacy boundary: this function is the single point of enforcement.
6476
* If you add a new privacy-sensitive field to Diagnostic, add it to the deny list here.
77+
*
78+
* Absolute file paths are relativized against rootDir so dashboards show
79+
* `src/Foo.vue` rather than the CI runner's `/home/runner/work/<repo>/…`.
6580
*/
66-
export function stripFindings(diagnostics: Diagnostic[]): PushedFinding[] {
81+
export function stripFindings(
82+
diagnostics: Diagnostic[],
83+
rootDir?: string,
84+
): PushedFinding[] {
6785
const out: PushedFinding[] = new Array(diagnostics.length);
6886
for (let i = 0; i < diagnostics.length; i++) {
6987
const d = diagnostics[i]!;
7088
out[i] = {
71-
file: d.file,
89+
file: toRepoRelative(d.file, rootDir),
7290
line: d.line,
7391
col: d.column,
7492
ruleId: d.ruleId,
@@ -119,7 +137,7 @@ export function buildPushPayload(input: PushPayloadInput): PushPayload {
119137
* exit code is not affected by SaaS downtime.
120138
*/
121139
export async function pushFindings(opts: PushOptions): Promise<PushResult> {
122-
const findings = stripFindings(opts.diagnostics);
140+
const findings = stripFindings(opts.diagnostics, opts.rootDir);
123141
const payload = buildPushPayload({
124142
project: opts.project,
125143
score: opts.score,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export function docsUrl(ruleId: string): string {
2-
return `https://docs.doctor.geoql.in/rules/${ruleId}`;
2+
return `https://docs.the-doctor.report/rules/${ruleId}`;
33
}

packages/doctor-core/src/reporters/pr-comment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Diagnostic } from '../types.js';
33
import { compareStrings } from './render.js';
44
import type { ReporterInput } from './types.js';
55

6-
const DOCS_BASE = 'https://docs.doctor.geoql.in/rules/';
6+
const DOCS_BASE = 'https://docs.the-doctor.report/rules/';
77
const MAX_PER_SECTION = 5;
88

99
function binName(toolName: string): string {

packages/doctor-core/tests/docs-url.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { docsUrl } from '../src/reporters/docs-url.js';
44
describe('docsUrl', () => {
55
it('builds a deterministic docs URL from a ruleId', () => {
66
expect(docsUrl('reactivity/no-destructure-reactive')).toBe(
7-
'https://docs.doctor.geoql.in/rules/reactivity/no-destructure-reactive',
7+
'https://docs.the-doctor.report/rules/reactivity/no-destructure-reactive',
88
);
99
});
1010

1111
it('is pure: same input yields the same output', () => {
1212
const id = 'vue-doctor/template/v-for-has-key';
1313
expect(docsUrl(id)).toBe(docsUrl(id));
14-
expect(docsUrl(id)).toBe(`https://docs.doctor.geoql.in/rules/${id}`);
14+
expect(docsUrl(id)).toBe(`https://docs.the-doctor.report/rules/${id}`);
1515
});
1616
});

packages/doctor-core/tests/pr-comment.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('prCommentReport', () => {
141141

142142
it('footer links to the docs base and the top finding explain command', () => {
143143
const out = prCommentReport(makeInput([diag({ severity: 'error' })]));
144-
expect(out).toContain('https://docs.doctor.geoql.in/rules/');
144+
expect(out).toContain('https://docs.the-doctor.report/rules/');
145145
expect(out).toContain(
146146
'Run `vue-doctor explain vue-doctor/template/v-for-has-key`',
147147
);

packages/doctor-core/tests/push.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,34 @@ describe('stripFindings', () => {
6464
expect(json).not.toContain('SECRET RECOMMENDATION');
6565
});
6666

67+
it('relativizes absolute file paths against rootDir (no CI runner paths leak)', () => {
68+
const out = stripFindings(
69+
[
70+
makeDiag({
71+
file: '/home/runner/work/v-clappr/v-clappr/src/components/Player.vue',
72+
}),
73+
],
74+
'/home/runner/work/v-clappr/v-clappr',
75+
);
76+
expect(out[0]!.file).toBe('src/components/Player.vue');
77+
});
78+
79+
it('leaves already-relative file paths untouched when rootDir is given', () => {
80+
const out = stripFindings(
81+
[makeDiag({ file: 'src/a.ts' })],
82+
'/home/runner/work/v-clappr/v-clappr',
83+
);
84+
expect(out[0]!.file).toBe('src/a.ts');
85+
});
86+
87+
it('keeps absolute paths outside rootDir relative-safe (no ../ escapes)', () => {
88+
const out = stripFindings(
89+
[makeDiag({ file: '/tmp/elsewhere/file.ts' })],
90+
'/home/runner/work/v-clappr/v-clappr',
91+
);
92+
expect(out[0]!.file).toBe('/tmp/elsewhere/file.ts');
93+
});
94+
6795
it('looks up the category from RULE_REGISTRY (not the ruleId prefix)', () => {
6896
const out = stripFindings([makeDiag({ ruleId: 'dead-code/unused-file' })]);
6997
expect(out[0]!.category).toBe('dead-code');

packages/doctor-core/tests/reporters.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('agent reporter', () => {
114114
' code: const { name, age } = props\n' +
115115
' fix: const { name } = toRefs(props)\n' +
116116
' why: Destructuring a reactive object loses reactivity.\n' +
117-
' docs: https://docs.doctor.geoql.in/rules/reactivity/no-destructure-reactive\n' +
117+
' docs: https://docs.the-doctor.report/rules/reactivity/no-destructure-reactive\n' +
118118
'\n' +
119119
'NEXT STEPS:\n' +
120120
' −5 pts 1× reactivity/no-destructure-reactive\n' +
@@ -256,7 +256,7 @@ describe('agent reporter', () => {
256256
it('renders the docs URL for the ruleId', () => {
257257
const out = agentReport(makeInput({ diagnostics: [diag({})] }));
258258
expect(out).toContain(
259-
' docs: https://docs.doctor.geoql.in/rules/reactivity/no-destructure-reactive\n',
259+
' docs: https://docs.the-doctor.report/rules/reactivity/no-destructure-reactive\n',
260260
);
261261
});
262262

packages/doctor-core/tests/reporters/html.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ describe('htmlReport', () => {
109109
expect(html).toContain('rule/with&lt;brackets&gt;');
110110
});
111111

112-
it('includes a docs link per finding pointing at docs.doctor.geoql.in', () => {
112+
it('includes a docs link per finding pointing at docs.the-doctor.report', () => {
113113
const html = htmlReport(makeInput([ERROR_DIAG]));
114-
expect(html).toContain('docs.doctor.geoql.in');
114+
expect(html).toContain('docs.the-doctor.report');
115115
expect(html).toContain('rel="noopener"');
116116
});
117117

packages/doctor-core/tests/reporters/sarif.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,14 @@ describe('sarifReport', () => {
297297
expect(new Set(hashes).size).toBe(hashes.length);
298298
});
299299

300-
it('emits helpUri on each rule descriptor pointing at docs.doctor.geoql.in', () => {
300+
it('emits helpUri on each rule descriptor pointing at docs.the-doctor.report', () => {
301301
const parsed = JSON.parse(sarifReport(makeInput([ERROR_DIAG]))) as {
302302
runs: {
303303
tool: { driver: { rules: { id: string; helpUri: string }[] } };
304304
}[];
305305
};
306306
const rule = parsed.runs[0]!.tool.driver.rules[0]!;
307-
expect(rule.helpUri).toContain('docs.doctor.geoql.in');
307+
expect(rule.helpUri).toContain('docs.the-doctor.report');
308308
expect(rule.helpUri).toContain('vue-doctor/template/v-for-has-key');
309309
});
310310

0 commit comments

Comments
 (0)