@geoql/doctor-core 0.2.0-alpha.0 → 1.0.0
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/README.md +70 -10
- package/dist/index.d.ts +108 -3
- package/dist/index.js +1245 -200
- package/dist/index.js.map +1 -1
- package/dist/{load-DiK7Zv0B.js → load-yXDi-5ti.js} +337 -2
- package/dist/load-yXDi-5ti.js.map +1 -0
- package/package.json +3 -2
- package/dist/load-DiK7Zv0B.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,34 +1,94 @@
|
|
|
1
1
|
# `@geoql/doctor-core`
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The audit engine behind [`@geoql/vue-doctor`](../vue-doctor) and [`@geoql/nuxt-doctor`](../nuxt-doctor).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This is the library that does the work. The two CLIs are thin `cac` wrappers; everything real lives here: the hybrid scan, scoring, reporters, config loading, and project detection. You usually depend on a CLI, not on this package directly. Reach for `doctor-core` only when you're embedding the audit into your own tool.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. **Script pass** — spawns `oxlint` as a subprocess with a generated `.oxlintrc.json` that activates oxlint's built-in `vue` plugin AND a custom `jsPlugins` entry (`@geoql/oxlint-plugin-vue-doctor`).
|
|
7
|
+
## What it does
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
`doctor-core` runs a **hybrid two-pass scan** over a Vue 3 or Nuxt 4 project, as locked in [`docs/SPEC.md`](../../docs/SPEC.md) §10:
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **Pass 1 — template AST.** Parses `.vue` SFCs with `@vue/compiler-sfc` and walks the template AST in-process. This is the only pass that can see the template, so template-shaped rules (`v-for` missing `:key`, `v-if`/`v-for` on the same node, inline object props in lists) live here.
|
|
12
|
+
- **Pass 2 — script ESTree.** Spawns `oxlint` as a subprocess against a generated `.oxlintrc.json` that turns on oxlint's native `vue` plugin plus the doctor JS plugins (`@geoql/oxlint-plugin-vue-doctor`, `@geoql/oxlint-plugin-nuxt-doctor`). oxlint only hands JS plugins the extracted `<script>`, so all script-level rules run here.
|
|
13
|
+
|
|
14
|
+
Diagnostics from both passes are merged and deduped on `(file, line, column, ruleId)`, then collapsed into a deterministic 0–100 score. Scoring uses fixed severity weights (`error` = 5, `warn` = 2, `info` = 0.5) with √-decay on repeat offenders so one noisy rule can't tank the whole score. Same code in, same score out: no randomness, no config-tunable weights.
|
|
15
|
+
|
|
16
|
+
It also owns the pieces the CLIs surface: the config loader (`doctor.config.ts` via `c12`), project/capability detection used to gate rules, the rule registry, and every reporter format.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm i @geoql/doctor-core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Published on [npm](https://www.npmjs.com/package/@geoql/doctor-core) and [JSR](https://jsr.io/@geoql/doctor-core) at `v0.1.0` with provenance. ESM-only. `knip` and `oxlint` are peer dependencies (the dead-code and script passes shell out to them).
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
The main entry is `audit()`. It returns a report carrying diagnostics, the numeric score, the score breakdown, detected project info, and a process exit code.
|
|
13
29
|
|
|
14
30
|
```ts
|
|
15
|
-
import { audit } from '@geoql/doctor-core';
|
|
31
|
+
import { audit, format } from '@geoql/doctor-core';
|
|
16
32
|
|
|
17
33
|
const report = await audit({
|
|
18
34
|
rootDir: process.cwd(),
|
|
19
35
|
include: ['**/*.vue', '**/*.ts'],
|
|
36
|
+
exclude: ['node_modules', 'dist', '.nuxt', '.output'],
|
|
20
37
|
failOn: 'error',
|
|
21
38
|
});
|
|
22
39
|
|
|
23
|
-
for (const
|
|
40
|
+
for (const d of report.diagnostics) {
|
|
24
41
|
console.log(
|
|
25
|
-
`${
|
|
42
|
+
`${d.file}:${d.line}:${d.column} ${d.severity} ${d.ruleId}: ${d.message}`,
|
|
26
43
|
);
|
|
27
44
|
}
|
|
45
|
+
|
|
28
46
|
console.log(`Score: ${report.score}/100`);
|
|
47
|
+
console.log(`Exit code: ${report.exitCode}`);
|
|
48
|
+
|
|
49
|
+
// Render any reporter format. `format()` takes a ReporterInput,
|
|
50
|
+
// which you assemble from the report.
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
format(
|
|
53
|
+
{
|
|
54
|
+
toolName: '@geoql/vue-doctor',
|
|
55
|
+
toolVersion: '0.1.0',
|
|
56
|
+
rootDirectory: report.rootDir,
|
|
57
|
+
analyzedFileCount: report.filesScanned,
|
|
58
|
+
elapsedMs: report.elapsedMs,
|
|
59
|
+
diagnostics: report.diagnostics,
|
|
60
|
+
score: report.scoreResult,
|
|
61
|
+
projectInfo: report.projectInfo,
|
|
62
|
+
},
|
|
63
|
+
'agent',
|
|
64
|
+
),
|
|
65
|
+
);
|
|
29
66
|
```
|
|
30
67
|
|
|
31
|
-
|
|
68
|
+
Severity is `error | warn | info` throughout. `failOn` accepts `error | warn | none`.
|
|
69
|
+
|
|
70
|
+
### Key exports
|
|
71
|
+
|
|
72
|
+
| Export | Purpose |
|
|
73
|
+
| ------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
74
|
+
| `audit` | Run the two-pass scan and return the full report. |
|
|
75
|
+
| `format` | Render a report as `agent`, `pretty`, `json`, `json-compact`, `sarif`, or `html`. |
|
|
76
|
+
| `loadDoctorConfig`, `defineConfig`, `mergeCliOverrides` | Load `doctor.config.ts` (via `c12`) and layer CLI overrides on top. |
|
|
77
|
+
| `detectProject` | Detect framework, versions, and capability tokens used to gate rules. |
|
|
78
|
+
| `listRules`, `RULE_REGISTRY`, `loadRuleDoc` | Inspect the rule registry and per-rule docs. |
|
|
79
|
+
| `scoreDiagnostics` | Deterministic 0–100 scoring with the breakdown. |
|
|
80
|
+
| `listChangedFiles` | Resolve diff/staged file scopes for incremental runs. |
|
|
81
|
+
| `encodeAnnotations` | Emit GitHub Actions `::error::` / `::warning::` lines. |
|
|
82
|
+
|
|
83
|
+
Types ship alongside (`AuditConfig`, `AuditReport`, `Diagnostic`, `Severity`, `ReporterFormat`, `ProjectInfo`, `ScoreResult`, and more).
|
|
84
|
+
|
|
85
|
+
## Scope
|
|
86
|
+
|
|
87
|
+
Vue 3 and Nuxt 4 only. Nuxt detection expects the `app/` directory layout with `compatibilityVersion: 4`.
|
|
88
|
+
|
|
89
|
+
## Architecture
|
|
90
|
+
|
|
91
|
+
See [`docs/SPEC.md`](../../docs/SPEC.md) §10 for the full two-pass design, the empirical oxlint findings that shaped it, and the canonical `Diagnostic` shape.
|
|
32
92
|
|
|
33
93
|
## License
|
|
34
94
|
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ interface ProjectInfo {
|
|
|
41
41
|
readonly nitroPreset: string | null;
|
|
42
42
|
readonly nuxtCompatibilityVersion: 3 | 4 | null;
|
|
43
43
|
readonly monorepoKind: MonorepoKind;
|
|
44
|
+
readonly nuxtConfigPath: string | null;
|
|
45
|
+
readonly hasAppDir: boolean;
|
|
46
|
+
readonly appDirPath: string | null;
|
|
47
|
+
readonly hasServerDir: boolean;
|
|
48
|
+
readonly hasPagesDir: boolean;
|
|
49
|
+
readonly hasWranglerConfig: boolean;
|
|
44
50
|
readonly capabilities: ReadonlySet<Capability>;
|
|
45
51
|
}
|
|
46
52
|
//#endregion
|
|
@@ -94,6 +100,8 @@ interface AuditConfig {
|
|
|
94
100
|
threshold?: number;
|
|
95
101
|
scopeFiles?: string[];
|
|
96
102
|
respectInlineDisables?: boolean;
|
|
103
|
+
fix?: boolean;
|
|
104
|
+
fixExcludes?: string[];
|
|
97
105
|
}
|
|
98
106
|
interface AuditTimings {
|
|
99
107
|
template: number;
|
|
@@ -142,6 +150,19 @@ interface DoctorUserConfig {
|
|
|
142
150
|
preset?: string;
|
|
143
151
|
rules?: Record<string, Severity | 'off'>;
|
|
144
152
|
extends?: string[];
|
|
153
|
+
fixExcludes?: string[];
|
|
154
|
+
}
|
|
155
|
+
interface ResolvedDoctorConfig {
|
|
156
|
+
rootDir: string;
|
|
157
|
+
include: string[];
|
|
158
|
+
exclude: string[];
|
|
159
|
+
failOn: 'error' | 'warn' | 'none';
|
|
160
|
+
threshold: number;
|
|
161
|
+
rules: Record<string, Severity>;
|
|
162
|
+
preset: 'minimal' | 'recommended' | 'strict' | 'all';
|
|
163
|
+
source: ConfigSource;
|
|
164
|
+
configFile?: string;
|
|
165
|
+
fixExcludes?: string[];
|
|
145
166
|
}
|
|
146
167
|
/**
|
|
147
168
|
* Resolved + normalized config that audit consumes.
|
|
@@ -182,6 +203,9 @@ interface CheckDeadCodeOptions {
|
|
|
182
203
|
}
|
|
183
204
|
declare function checkDeadCode(options: CheckDeadCodeOptions): Promise<Diagnostic[]>;
|
|
184
205
|
//#endregion
|
|
206
|
+
//#region src/check-nuxt-project.d.ts
|
|
207
|
+
declare function checkNuxtProject(projectInfo: ProjectInfo): Promise<Diagnostic[]>;
|
|
208
|
+
//#endregion
|
|
185
209
|
//#region src/config/built-in.d.ts
|
|
186
210
|
declare const BUILT_IN_RECOMMENDED: Omit<ResolvedDoctorConfig, 'rootDir' | 'source' | 'configFile'>;
|
|
187
211
|
//#endregion
|
|
@@ -192,6 +216,7 @@ interface CliOverrides {
|
|
|
192
216
|
failOn?: 'error' | 'warn' | 'none';
|
|
193
217
|
threshold?: number;
|
|
194
218
|
rules?: Record<string, Severity | 'off'>;
|
|
219
|
+
fixExcludes?: string[];
|
|
195
220
|
}
|
|
196
221
|
declare function mergeCliOverrides(resolved: ResolvedDoctorConfig, cli: CliOverrides): ResolvedDoctorConfig;
|
|
197
222
|
//#endregion
|
|
@@ -224,6 +249,51 @@ declare function validateConfig(raw: unknown): void;
|
|
|
224
249
|
//#region src/detect-project.d.ts
|
|
225
250
|
declare function detectProject(rootDirectory: string): Promise<ProjectInfo>;
|
|
226
251
|
//#endregion
|
|
252
|
+
//#region src/init/index.d.ts
|
|
253
|
+
type InitConfigFormat = 'ts' | 'json' | 'package-json';
|
|
254
|
+
type InitPreset = 'recommended' | 'strict' | 'minimal';
|
|
255
|
+
interface RawInitAnswers {
|
|
256
|
+
target?: InitConfigFormat;
|
|
257
|
+
preset?: InitPreset;
|
|
258
|
+
threshold?: number;
|
|
259
|
+
exclude?: string;
|
|
260
|
+
}
|
|
261
|
+
interface ResolvedInitAnswers {
|
|
262
|
+
configFormat: InitConfigFormat;
|
|
263
|
+
preset: InitPreset;
|
|
264
|
+
threshold: number | undefined;
|
|
265
|
+
exclude: string[] | undefined;
|
|
266
|
+
}
|
|
267
|
+
interface InitOptions {
|
|
268
|
+
dir: string;
|
|
269
|
+
configFormat: InitConfigFormat;
|
|
270
|
+
preset: InitPreset;
|
|
271
|
+
threshold: number | undefined;
|
|
272
|
+
exclude: string[] | undefined;
|
|
273
|
+
binName: string;
|
|
274
|
+
}
|
|
275
|
+
interface InitFileWrite {
|
|
276
|
+
path: string;
|
|
277
|
+
content: string;
|
|
278
|
+
}
|
|
279
|
+
interface InitPlan {
|
|
280
|
+
writes: InitFileWrite[];
|
|
281
|
+
conflict: boolean;
|
|
282
|
+
conflictPath: string | null;
|
|
283
|
+
}
|
|
284
|
+
declare function parseExcludeList(raw: string | undefined): string[] | undefined;
|
|
285
|
+
declare function normalizeInitAnswers(raw: RawInitAnswers): ResolvedInitAnswers;
|
|
286
|
+
declare function renderDetectSummary(project: ProjectInfo, sfcCount: number): string;
|
|
287
|
+
declare function detectSummary(dir: string): Promise<string>;
|
|
288
|
+
declare function planInit(options: InitOptions): Promise<InitPlan>;
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/project-info/find-monorepo-root.d.ts
|
|
291
|
+
interface MonorepoResult {
|
|
292
|
+
root: string;
|
|
293
|
+
kind: MonorepoKind;
|
|
294
|
+
}
|
|
295
|
+
declare function findMonorepoRoot(rootDirectory: string): Promise<MonorepoResult>;
|
|
296
|
+
//#endregion
|
|
227
297
|
//#region src/disables/apply.d.ts
|
|
228
298
|
interface ApplyInlineDisablesOptions {
|
|
229
299
|
respect: boolean;
|
|
@@ -255,6 +325,38 @@ interface GitScopeOptions {
|
|
|
255
325
|
}
|
|
256
326
|
declare function listChangedFiles(options: GitScopeOptions): Promise<string[]>;
|
|
257
327
|
//#endregion
|
|
328
|
+
//#region src/project-info/list-workspace-packages.d.ts
|
|
329
|
+
interface WorkspacePackage {
|
|
330
|
+
name: string;
|
|
331
|
+
dir: string;
|
|
332
|
+
}
|
|
333
|
+
declare function listWorkspacePackages(rootDir: string): Promise<WorkspacePackage[]>;
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/nuxt/file-role.d.ts
|
|
336
|
+
/**
|
|
337
|
+
* True when the path points at a Nuxt page component — a `.vue` file under
|
|
338
|
+
* `pages/` (Nuxt 3 layout) or `app/pages/` (Nuxt 4 layout) at the project root.
|
|
339
|
+
*/
|
|
340
|
+
declare function isNuxtPageFile(relPath: string): boolean;
|
|
341
|
+
/**
|
|
342
|
+
* True when the path points at a Nitro server file — a `.ts` file under the
|
|
343
|
+
* project-root `server/` directory.
|
|
344
|
+
*/
|
|
345
|
+
declare function isNuxtServerFile(relPath: string): boolean;
|
|
346
|
+
/**
|
|
347
|
+
* True when the path points at a Nuxt layout component — a `.vue` file under
|
|
348
|
+
* `layouts/` (Nuxt 3 layout) or `app/layouts/` (Nuxt 4 layout) at the project
|
|
349
|
+
* root.
|
|
350
|
+
*/
|
|
351
|
+
declare function isNuxtLayoutFile(relPath: string): boolean;
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/nuxt/cross-file/run.d.ts
|
|
354
|
+
interface CrossFilePassOptions {
|
|
355
|
+
files: string[];
|
|
356
|
+
projectInfo: ProjectInfo;
|
|
357
|
+
}
|
|
358
|
+
declare function runCrossFilePass(opts: CrossFilePassOptions): Promise<Diagnostic[]>;
|
|
359
|
+
//#endregion
|
|
258
360
|
//#region src/oxlint/errors.d.ts
|
|
259
361
|
declare class OxlintSpawnFailed extends Error {
|
|
260
362
|
name: "OxlintSpawnFailed";
|
|
@@ -281,7 +383,7 @@ interface VerboseTraceOptions {
|
|
|
281
383
|
declare function renderVerboseTrace(report: AuditReport, options: VerboseTraceOptions): string;
|
|
282
384
|
//#endregion
|
|
283
385
|
//#region src/reporters/index.d.ts
|
|
284
|
-
type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html';
|
|
386
|
+
type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html' | 'pr-comment';
|
|
285
387
|
declare function format(input: ReporterInput, kind?: ReporterFormat, options?: ReporterOptions): string;
|
|
286
388
|
//#endregion
|
|
287
389
|
//#region src/reporters/json.d.ts
|
|
@@ -340,6 +442,9 @@ declare function jsonReport(input: ReporterInput): string;
|
|
|
340
442
|
//#region src/reporters/json-compact.d.ts
|
|
341
443
|
declare function jsonCompactReport(input: ReporterInput): string;
|
|
342
444
|
//#endregion
|
|
445
|
+
//#region src/reporters/pr-comment.d.ts
|
|
446
|
+
declare function prCommentReport(input: ReporterInput): string;
|
|
447
|
+
//#endregion
|
|
343
448
|
//#region src/reporters/pretty.d.ts
|
|
344
449
|
declare function prettyReport(input: ReporterInput, options?: ReporterOptions): string;
|
|
345
450
|
//#endregion
|
|
@@ -362,7 +467,7 @@ declare function loadRuleDoc(ruleId: string, options?: LoadRuleDocOptions): Rule
|
|
|
362
467
|
declare function loadAllRuleDocs(options?: LoadRuleDocOptions): RuleDoc[];
|
|
363
468
|
//#endregion
|
|
364
469
|
//#region src/rule-registry.d.ts
|
|
365
|
-
type RuleCategory = 'ai-slop' | 'reactivity' | 'composition' | 'performance' | 'template' | 'template-perf' | 'build-quality' | 'deps' | 'dead-code' | 'sfc' | 'vue-builtin';
|
|
470
|
+
type RuleCategory = 'ai-slop' | 'reactivity' | 'composition' | 'performance' | 'template' | 'template-perf' | 'build-quality' | 'deps' | 'dead-code' | 'sfc' | 'vue-builtin' | 'structure' | 'modules-deps' | 'nitro' | 'seo' | 'cloudflare' | 'server-routes' | 'hydration' | 'data-fetching';
|
|
366
471
|
type RuleSource = 'doctor' | 'oxlint-builtin' | 'eslint-plugin-vue';
|
|
367
472
|
interface RegisteredRule {
|
|
368
473
|
readonly id: string;
|
|
@@ -380,5 +485,5 @@ interface ListRulesFilter {
|
|
|
380
485
|
}
|
|
381
486
|
declare function listRules(filter?: ListRulesFilter): RegisteredRule[];
|
|
382
487
|
//#endregion
|
|
383
|
-
export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, RULE_REGISTRY, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, dedupeDeadCodeAgainstLint, defineConfig, detectProject, docsUrl, encodeAnnotation, encodeAnnotations, format, jsonCompactReport, jsonReport, listChangedFiles, listRules, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, parseDirectives, prettyReport, renderVerboseTrace, sarifReport, scoreDiagnostics, validateConfig };
|
|
488
|
+
export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, type WorkspacePackage, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
|
|
384
489
|
//# sourceMappingURL=index.d.ts.map
|