@geoql/doctor-core 0.1.1 → 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
@@ -6,7 +6,7 @@ This is the library that does the work. The two CLIs are thin `cac` wrappers; ev
6
6
 
7
7
  ## What it does
8
8
 
9
- `doctor-core` runs a **hybrid two-pass scan** over a Vue 3 or Nuxt 4 project, as locked in [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md):
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:
10
10
 
11
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
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.
@@ -88,7 +88,7 @@ Vue 3 and Nuxt 4 only. Nuxt detection expects the `app/` directory layout with `
88
88
 
89
89
  ## Architecture
90
90
 
91
- See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md) for the full two-pass design, the empirical oxlint findings that shaped it, and the canonical `Diagnostic` shape.
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.
92
92
 
93
93
  ## License
94
94
 
package/dist/index.d.ts CHANGED
@@ -100,6 +100,8 @@ interface AuditConfig {
100
100
  threshold?: number;
101
101
  scopeFiles?: string[];
102
102
  respectInlineDisables?: boolean;
103
+ fix?: boolean;
104
+ fixExcludes?: string[];
103
105
  }
104
106
  interface AuditTimings {
105
107
  template: number;
@@ -148,6 +150,19 @@ interface DoctorUserConfig {
148
150
  preset?: string;
149
151
  rules?: Record<string, Severity | 'off'>;
150
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[];
151
166
  }
152
167
  /**
153
168
  * Resolved + normalized config that audit consumes.
@@ -201,6 +216,7 @@ interface CliOverrides {
201
216
  failOn?: 'error' | 'warn' | 'none';
202
217
  threshold?: number;
203
218
  rules?: Record<string, Severity | 'off'>;
219
+ fixExcludes?: string[];
204
220
  }
205
221
  declare function mergeCliOverrides(resolved: ResolvedDoctorConfig, cli: CliOverrides): ResolvedDoctorConfig;
206
222
  //#endregion
@@ -233,6 +249,51 @@ declare function validateConfig(raw: unknown): void;
233
249
  //#region src/detect-project.d.ts
234
250
  declare function detectProject(rootDirectory: string): Promise<ProjectInfo>;
235
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
236
297
  //#region src/disables/apply.d.ts
237
298
  interface ApplyInlineDisablesOptions {
238
299
  respect: boolean;
@@ -264,6 +325,13 @@ interface GitScopeOptions {
264
325
  }
265
326
  declare function listChangedFiles(options: GitScopeOptions): Promise<string[]>;
266
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
267
335
  //#region src/nuxt/file-role.d.ts
268
336
  /**
269
337
  * True when the path points at a Nuxt page component — a `.vue` file under
@@ -315,7 +383,7 @@ interface VerboseTraceOptions {
315
383
  declare function renderVerboseTrace(report: AuditReport, options: VerboseTraceOptions): string;
316
384
  //#endregion
317
385
  //#region src/reporters/index.d.ts
318
- type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html';
386
+ type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html' | 'pr-comment';
319
387
  declare function format(input: ReporterInput, kind?: ReporterFormat, options?: ReporterOptions): string;
320
388
  //#endregion
321
389
  //#region src/reporters/json.d.ts
@@ -374,6 +442,9 @@ declare function jsonReport(input: ReporterInput): string;
374
442
  //#region src/reporters/json-compact.d.ts
375
443
  declare function jsonCompactReport(input: ReporterInput): string;
376
444
  //#endregion
445
+ //#region src/reporters/pr-comment.d.ts
446
+ declare function prCommentReport(input: ReporterInput): string;
447
+ //#endregion
377
448
  //#region src/reporters/pretty.d.ts
378
449
  declare function prettyReport(input: ReporterInput, options?: ReporterOptions): string;
379
450
  //#endregion
@@ -414,5 +485,5 @@ interface ListRulesFilter {
414
485
  }
415
486
  declare function listRules(filter?: ListRulesFilter): RegisteredRule[];
416
487
  //#endregion
417
- 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, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, docsUrl, encodeAnnotation, encodeAnnotations, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, parseDirectives, prettyReport, renderVerboseTrace, runCrossFilePass, 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 };
418
489
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-BGjurxpY.js";
1
+ import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-yXDi-5ti.js";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
3
  import { performance } from "node:perf_hooks";
4
4
  import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
@@ -215,6 +215,7 @@ async function checkBuildQuality(projectInfo) {
215
215
  //#region src/dead-code/build-knip-config.ts
216
216
  function buildKnipConfig(projectInfo, doctorConfig) {
217
217
  const isNuxt = projectInfo.framework === "nuxt";
218
+ const usesConventionResolution = projectInfo.hasAutoImports || projectInfo.hasComponentsAutoImport || projectInfo.hasVueRouter;
218
219
  const entry = isNuxt ? [
219
220
  "app/**/*.vue",
220
221
  "app/**/*.ts",
@@ -224,7 +225,14 @@ function buildKnipConfig(projectInfo, doctorConfig) {
224
225
  "src/main.{ts,js}",
225
226
  "src/App.vue",
226
227
  "index.html",
227
- "vite.config.{ts,js}"
228
+ "vite.config.{ts,js}",
229
+ ...usesConventionResolution ? [
230
+ "src/pages/**/*.vue",
231
+ "src/layouts/**/*.vue",
232
+ "src/components/**/*.vue",
233
+ "src/composables/**/*.ts",
234
+ "src/stores/**/*.ts"
235
+ ] : []
228
236
  ];
229
237
  const config = {
230
238
  cwd: projectInfo.rootDirectory,
@@ -381,6 +389,20 @@ const _knipLoader = { load: async () => {
381
389
  run: runModule.run
382
390
  };
383
391
  } };
392
+ async function writeKnipConfig(rootDir, config) {
393
+ const cacheDir = join(rootDir, "node_modules", ".cache", "doctor");
394
+ await mkdir(cacheDir, { recursive: true });
395
+ const configPath = join(cacheDir, "knip.json");
396
+ const knipJson = {
397
+ entry: config.entry,
398
+ project: config.project,
399
+ ignore: config.ignoreFiles,
400
+ ignoreDependencies: config.ignoreDependencies
401
+ };
402
+ if (config.compilers) knipJson.compilers = config.compilers;
403
+ await writeFile(configPath, JSON.stringify(knipJson, null, 2), "utf8");
404
+ return configPath;
405
+ }
384
406
  async function checkDeadCode(options) {
385
407
  if (!options.enabled) return [];
386
408
  const config = buildKnipConfig(options.projectInfo, options.doctorConfig);
@@ -394,13 +416,10 @@ async function checkDeadCode(options) {
394
416
  } catch (err) {
395
417
  throw new DeadCodeImportFailed(err);
396
418
  }
419
+ const configPath = await writeKnipConfig(options.projectInfo.rootDirectory, config);
397
420
  const knipOptions = await createOptions({
398
421
  cwd: options.projectInfo.rootDirectory,
399
- entry: config.entry,
400
- project: config.project,
401
- ignoreFiles: config.ignoreFiles,
402
- ignoreDependencies: config.ignoreDependencies,
403
- ...config.compilers ? { compilers: config.compilers } : {}
422
+ args: { config: configPath }
404
423
  });
405
424
  const diagnostics = flattenKnipIssues((await Promise.race([run(knipOptions), new Promise((_, reject) => setTimeout(() => reject(new DeadCodeTimeoutError(timeoutMs)), timeoutMs))])).results.issues).map((issue) => mapKnipDiagnostic(options.projectInfo.rootDirectory, issue)).filter((d) => d !== null);
406
425
  diagnostics.sort((a, b) => {
@@ -1347,6 +1366,23 @@ function toCanonicalDiagnostics(raws, rootDir) {
1347
1366
  const VUE_DEFAULT_RULES = {
1348
1367
  "vue/no-export-in-script-setup": "error",
1349
1368
  "vue/require-typed-ref": "warn",
1369
+ "vue/no-arrow-functions-in-watch": "error",
1370
+ "vue/no-deprecated-data-object-declaration": "error",
1371
+ "vue/no-deprecated-events-api": "error",
1372
+ "vue/no-deprecated-destroyed-lifecycle": "error",
1373
+ "vue/no-deprecated-model-definition": "error",
1374
+ "vue/no-deprecated-delete-set": "error",
1375
+ "vue/no-deprecated-vue-config-keycodes": "error",
1376
+ "vue/no-lifecycle-after-await": "error",
1377
+ "vue/no-this-in-before-route-enter": "error",
1378
+ "vue/return-in-computed-property": "error",
1379
+ "vue/valid-define-emits": "error",
1380
+ "vue/valid-define-props": "error",
1381
+ "vue/no-required-prop-with-default": "warn",
1382
+ "vue/prefer-import-from-vue": "warn",
1383
+ "vue/no-import-compiler-macros": "warn",
1384
+ "vue/no-multiple-slot-args": "warn",
1385
+ "vue/require-default-export": "warn",
1350
1386
  "vue-doctor/no-em-dash-in-string": "warn",
1351
1387
  "vue-doctor/no-non-null-assertion-on-ref-value": "warn",
1352
1388
  "vue-doctor/no-imports-from-vue-when-auto-imported": "warn",
@@ -1369,6 +1405,27 @@ const NUXT_PLUGIN_RULES = {
1369
1405
  const VUE_OXLINT_RULE_IDS = new Set([
1370
1406
  "vue/no-export-in-script-setup",
1371
1407
  "vue/require-typed-ref",
1408
+ "vue/no-arrow-functions-in-watch",
1409
+ "vue/no-deprecated-data-object-declaration",
1410
+ "vue/no-deprecated-events-api",
1411
+ "vue/no-deprecated-destroyed-lifecycle",
1412
+ "vue/no-deprecated-model-definition",
1413
+ "vue/no-deprecated-delete-set",
1414
+ "vue/no-deprecated-vue-config-keycodes",
1415
+ "vue/no-lifecycle-after-await",
1416
+ "vue/no-this-in-before-route-enter",
1417
+ "vue/return-in-computed-property",
1418
+ "vue/valid-define-emits",
1419
+ "vue/valid-define-props",
1420
+ "vue/no-required-prop-with-default",
1421
+ "vue/prefer-import-from-vue",
1422
+ "vue/no-import-compiler-macros",
1423
+ "vue/no-multiple-slot-args",
1424
+ "vue/require-default-export",
1425
+ "vue/define-emits-declaration",
1426
+ "vue/define-props-declaration",
1427
+ "vue/define-props-destructuring",
1428
+ "vue/max-props",
1372
1429
  "vue-doctor/no-em-dash-in-string",
1373
1430
  "vue-doctor/no-destructure-props-without-to-refs",
1374
1431
  "vue-doctor/no-destructure-reactive-without-to-refs",
@@ -1543,9 +1600,10 @@ async function runOxlint(opts) {
1543
1600
  "-c",
1544
1601
  opts.configPath,
1545
1602
  "--format",
1546
- "json",
1547
- opts.targetPath
1603
+ "json"
1548
1604
  ];
1605
+ if (opts.fix) args.push("--fix");
1606
+ args.push(opts.targetPath);
1549
1607
  const child = spawn(opts.oxlintBin, args, {
1550
1608
  cwd: opts.rootDir,
1551
1609
  stdio: [
@@ -1647,7 +1705,8 @@ async function runScriptPass(opts) {
1647
1705
  targetPath: opts.targetPath,
1648
1706
  configPath,
1649
1707
  oxlintBin,
1650
- timeoutMs: opts.timeoutMs
1708
+ timeoutMs: opts.timeoutMs,
1709
+ fix: opts.fix
1651
1710
  });
1652
1711
  return {
1653
1712
  diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),
@@ -2618,7 +2677,9 @@ async function audit(config = {}) {
2618
2677
  rootDir,
2619
2678
  targetPath: rootDir,
2620
2679
  ruleOverrides: config.rules,
2621
- framework: project.framework === "nuxt" ? "nuxt" : "vue"
2680
+ framework: project.framework === "nuxt" ? "nuxt" : "vue",
2681
+ fix: config.fix === true && config.scopeFiles === void 0,
2682
+ fixExcludes: config.fixExcludes
2622
2683
  });
2623
2684
  scriptDiagnostics = result.diagnostics;
2624
2685
  oxlintStderr = result.stderr;
@@ -2632,7 +2693,7 @@ async function audit(config = {}) {
2632
2693
  if (config.deadCode !== false) {
2633
2694
  const deadCodeStart = performance.now();
2634
2695
  try {
2635
- const { loadDoctorConfig } = await import("./load-BGjurxpY.js").then((n) => n.n);
2696
+ const { loadDoctorConfig } = await import("./load-yXDi-5ti.js").then((n) => n.n);
2636
2697
  deadCodeDiagnostics = dedupeDeadCodeAgainstLint(await checkDeadCode({
2637
2698
  projectInfo: project,
2638
2699
  doctorConfig: await loadDoctorConfig(rootDir),
@@ -2751,7 +2812,111 @@ function mergeCliOverrides(resolved, cli) {
2751
2812
  threshold: cli.threshold ?? resolved.threshold,
2752
2813
  rules,
2753
2814
  source: resolved.source,
2754
- configFile: resolved.configFile
2815
+ configFile: resolved.configFile,
2816
+ ...cli.fixExcludes ?? resolved.fixExcludes ? { fixExcludes: cli.fixExcludes ?? resolved.fixExcludes } : {}
2817
+ };
2818
+ }
2819
+ //#endregion
2820
+ //#region src/init/index.ts
2821
+ function parseExcludeList(raw) {
2822
+ if (!raw) return void 0;
2823
+ const parts = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
2824
+ return parts.length > 0 ? parts : void 0;
2825
+ }
2826
+ function normalizeInitAnswers(raw) {
2827
+ return {
2828
+ configFormat: raw.target ?? "ts",
2829
+ preset: raw.preset ?? "recommended",
2830
+ threshold: typeof raw.threshold === "number" ? raw.threshold : void 0,
2831
+ exclude: parseExcludeList(raw.exclude)
2832
+ };
2833
+ }
2834
+ function frameworkLabel(project) {
2835
+ if (project.framework === "nuxt") return `Nuxt ${project.nuxtVersion ?? "unknown"}`;
2836
+ if (project.framework === "vue") return `Vue ${project.vueVersion ?? "unknown"}`;
2837
+ return "unknown project";
2838
+ }
2839
+ function renderDetectSummary(project, sfcCount) {
2840
+ const ts = project.capabilities.has("typescript") ? " · TS" : "";
2841
+ const noun = sfcCount === 1 ? "SFC" : "SFCs";
2842
+ return `detected: ${frameworkLabel(project)}${ts} · ${sfcCount} ${noun}`;
2843
+ }
2844
+ async function detectSummary(dir) {
2845
+ return renderDetectSummary(await detectProject(dir), (await listSourceFiles({
2846
+ rootDir: dir,
2847
+ include: ["**/*.vue"],
2848
+ exclude: ["**/node_modules/**"]
2849
+ })).length);
2850
+ }
2851
+ function buildUserConfig(options) {
2852
+ const config = { preset: options.preset };
2853
+ if (options.threshold !== void 0) config.threshold = options.threshold;
2854
+ if (options.exclude && options.exclude.length > 0) config.exclude = options.exclude;
2855
+ return config;
2856
+ }
2857
+ function renderTsConfig(options) {
2858
+ const config = buildUserConfig(options);
2859
+ const lines = [
2860
+ "import { defineConfig } from '@geoql/doctor-core';",
2861
+ "",
2862
+ "export default defineConfig({",
2863
+ ` preset: '${config.preset}',`
2864
+ ];
2865
+ if (config.threshold !== void 0) lines.push(` threshold: ${config.threshold},`);
2866
+ if (config.exclude) {
2867
+ const globs = config.exclude.map((glob) => `'${glob}'`).join(", ");
2868
+ lines.push(` exclude: [${globs}],`);
2869
+ }
2870
+ lines.push("});");
2871
+ return `${lines.join("\n")}\n`;
2872
+ }
2873
+ function renderJsonConfig(options) {
2874
+ return `${JSON.stringify(buildUserConfig(options), null, 2)}\n`;
2875
+ }
2876
+ async function readPackageJsonRaw(path) {
2877
+ try {
2878
+ return JSON.parse(await readFile(path, "utf8"));
2879
+ } catch {
2880
+ return null;
2881
+ }
2882
+ }
2883
+ async function planInit(options) {
2884
+ const writes = [];
2885
+ const packageJsonPath = join(options.dir, "package.json");
2886
+ const pkg = { ...await readPackageJsonRaw(packageJsonPath) ?? {} };
2887
+ let conflict = false;
2888
+ let conflictPath = null;
2889
+ if (options.configFormat === "package-json") {
2890
+ if (pkg.doctor !== void 0) {
2891
+ conflict = true;
2892
+ conflictPath = packageJsonPath;
2893
+ }
2894
+ pkg.doctor = buildUserConfig(options);
2895
+ } else {
2896
+ const fileName = options.configFormat === "ts" ? "doctor.config.ts" : "doctor.config.json";
2897
+ const configPath = join(options.dir, fileName);
2898
+ if (await pathExists(configPath)) {
2899
+ conflict = true;
2900
+ conflictPath = configPath;
2901
+ }
2902
+ const content = options.configFormat === "ts" ? renderTsConfig(options) : renderJsonConfig(options);
2903
+ writes.push({
2904
+ path: configPath,
2905
+ content
2906
+ });
2907
+ }
2908
+ pkg.scripts = {
2909
+ ...pkg.scripts ?? {},
2910
+ "doctor:check": options.binName
2911
+ };
2912
+ writes.push({
2913
+ path: packageJsonPath,
2914
+ content: `${JSON.stringify(pkg, null, 2)}\n`
2915
+ });
2916
+ return {
2917
+ writes,
2918
+ conflict,
2919
+ conflictPath
2755
2920
  };
2756
2921
  }
2757
2922
  //#endregion
@@ -2798,6 +2963,47 @@ async function listChangedFiles(options) {
2798
2963
  return [...new Set(absolute)].sort();
2799
2964
  }
2800
2965
  //#endregion
2966
+ //#region src/project-info/list-workspace-packages.ts
2967
+ function parsePackageGlobs(yaml) {
2968
+ const globs = [];
2969
+ let inPackages = false;
2970
+ for (const raw of yaml.split("\n")) {
2971
+ if (!inPackages) {
2972
+ if (/^packages:/.test(raw)) inPackages = true;
2973
+ continue;
2974
+ }
2975
+ const item = raw.match(/^\s*-\s*(.+?)\s*$/);
2976
+ if (item) {
2977
+ globs.push(item[1].replace(/^['"]|['"]$/g, ""));
2978
+ continue;
2979
+ }
2980
+ if (raw.trim() === "") continue;
2981
+ break;
2982
+ }
2983
+ return globs;
2984
+ }
2985
+ async function listWorkspacePackages(rootDir) {
2986
+ const { root, kind } = await findMonorepoRoot(rootDir);
2987
+ if (kind !== "pnpm") return [];
2988
+ const manifests = await glob(parsePackageGlobs(await readFile(join(root, "pnpm-workspace.yaml"), "utf8")).map((g) => `${g}/package.json`), {
2989
+ cwd: root,
2990
+ absolute: true,
2991
+ onlyFiles: true,
2992
+ dot: false
2993
+ });
2994
+ const packages = [];
2995
+ for (const manifest of manifests) {
2996
+ const dir = dirname(manifest);
2997
+ const pkg = await readPackageJson(dir);
2998
+ if (pkg?.name) packages.push({
2999
+ name: pkg.name,
3000
+ dir
3001
+ });
3002
+ }
3003
+ packages.sort((a, b) => a.name.localeCompare(b.name));
3004
+ return packages;
3005
+ }
3006
+ //#endregion
2801
3007
  //#region src/reporters/docs-url.ts
2802
3008
  function docsUrl(ruleId) {
2803
3009
  return `https://docs.doctor.geoql.in/rules/${ruleId}`;
@@ -3162,6 +3368,53 @@ function jsonCompactReport(input) {
3162
3368
  return `${JSON.stringify(buildDoctorReport(input))}\n`;
3163
3369
  }
3164
3370
  //#endregion
3371
+ //#region src/reporters/pr-comment.ts
3372
+ const DOCS_BASE = "https://docs.doctor.geoql.in/rules/";
3373
+ const MAX_PER_SECTION = 5;
3374
+ function binName(toolName) {
3375
+ return toolName.replace("@geoql/", "");
3376
+ }
3377
+ function relativeLocation(diag, rootDirectory) {
3378
+ return `${relative(rootDirectory, diag.file).replaceAll("\\", "/")}:${diag.line}`;
3379
+ }
3380
+ function findingLine(diag, rootDirectory) {
3381
+ return `- \`${relativeLocation(diag, rootDirectory)}\` ${diag.ruleId} — ${diag.message}`;
3382
+ }
3383
+ function sortBySeverityFilter(diagnostics, severity, rootDirectory) {
3384
+ return diagnostics.filter((d) => d.severity === severity).sort((a, b) => {
3385
+ const byFile = compareStrings(relative(rootDirectory, a.file).replaceAll("\\", "/"), relative(rootDirectory, b.file).replaceAll("\\", "/"));
3386
+ if (byFile !== 0) return byFile;
3387
+ if (a.line !== b.line) return a.line - b.line;
3388
+ if (a.column !== b.column) return a.column - b.column;
3389
+ return compareStrings(a.ruleId, b.ruleId);
3390
+ });
3391
+ }
3392
+ function section(title, diagnostics, rootDirectory) {
3393
+ if (diagnostics.length === 0) return [];
3394
+ const lines = ["", `### ${title}`];
3395
+ for (const d of diagnostics.slice(0, MAX_PER_SECTION)) lines.push(findingLine(d, rootDirectory));
3396
+ return lines;
3397
+ }
3398
+ function prCommentReport(input) {
3399
+ const score = input.score.score;
3400
+ const header = `## 🛡 ${input.toolName} — Score: ${score}`;
3401
+ const errors = sortBySeverityFilter(input.diagnostics, "error", input.rootDirectory);
3402
+ const warnings = sortBySeverityFilter(input.diagnostics, "warn", input.rootDirectory);
3403
+ const actionable = errors.length + warnings.length;
3404
+ if (actionable === 0) return `${header}\n\n✓ No actionable findings. Score: ${score}\n`;
3405
+ const topRule = (errors[0] ?? warnings[0]).ruleId;
3406
+ return `${[
3407
+ header,
3408
+ "",
3409
+ `**${actionable} findings** (${errors.length} errors, ${warnings.length} warnings)`,
3410
+ ...section("Errors", errors, input.rootDirectory),
3411
+ ...section("Warnings", warnings, input.rootDirectory),
3412
+ "",
3413
+ "---",
3414
+ `[View the docs](${DOCS_BASE}) · Run \`${binName(input.toolName)} explain ${topRule}\` for details.`
3415
+ ].join("\n")}\n`;
3416
+ }
3417
+ //#endregion
3165
3418
  //#region src/reporters/pretty.ts
3166
3419
  const colors = pc.createColors(true);
3167
3420
  const colorTheme = {
@@ -3212,9 +3465,10 @@ function format(input, kind = "agent", options) {
3212
3465
  if (kind === "json-compact") return jsonCompactReport(input);
3213
3466
  if (kind === "sarif") return sarifReport(input);
3214
3467
  if (kind === "html") return htmlReport(input);
3468
+ if (kind === "pr-comment") return prCommentReport(input);
3215
3469
  return agentReport(input, options);
3216
3470
  }
3217
3471
  //#endregion
3218
- export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, docsUrl, encodeAnnotation, encodeAnnotations, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, parseDirectives, prettyReport, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
3472
+ export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, 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 };
3219
3473
 
3220
3474
  //# sourceMappingURL=index.js.map