@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 CHANGED
@@ -1,34 +1,94 @@
1
1
  # `@geoql/doctor-core`
2
2
 
3
- > Audit engine shared by [`@geoql/vue-doctor`](../vue-doctor) and (future) `@geoql/nuxt-doctor`.
3
+ > The audit engine behind [`@geoql/vue-doctor`](../vue-doctor) and [`@geoql/nuxt-doctor`](../nuxt-doctor).
4
4
 
5
- Implements the **hybrid two-pass design** locked in [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md):
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
- 1. **Template pass** — parses `.vue` SFCs with `@vue/compiler-sfc` and walks the template AST for rules that need template-level context (e.g. `v-for` missing `:key`).
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
- Diagnostics from both passes are merged, deduped, and fed into a deterministic 0–100 score.
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
- ## Usage (programmatic)
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 diag of report.diagnostics) {
40
+ for (const d of report.diagnostics) {
24
41
  console.log(
25
- `${diag.file}:${diag.line}:${diag.column} ${diag.severity} ${diag.ruleId}: ${diag.message}`,
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
- Most users should depend on `@geoql/vue-doctor` (the CLI) rather than this package directly.
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