@doccov/cli 0.5.6 → 0.5.8

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/dist/cli.js CHANGED
@@ -33,11 +33,22 @@ var docsConfigSchema = z.object({
33
33
  include: stringList.optional(),
34
34
  exclude: stringList.optional()
35
35
  });
36
+ var lintSeveritySchema = z.enum(["error", "warn", "off"]);
37
+ var checkConfigSchema = z.object({
38
+ lint: z.boolean().optional(),
39
+ typecheck: z.boolean().optional(),
40
+ exec: z.boolean().optional()
41
+ });
42
+ var lintConfigSchema = z.object({
43
+ rules: z.record(lintSeveritySchema).optional()
44
+ });
36
45
  var docCovConfigSchema = z.object({
37
46
  include: stringList.optional(),
38
47
  exclude: stringList.optional(),
39
48
  plugins: z.array(z.unknown()).optional(),
40
- docs: docsConfigSchema.optional()
49
+ docs: docsConfigSchema.optional(),
50
+ check: checkConfigSchema.optional(),
51
+ lint: lintConfigSchema.optional()
41
52
  });
42
53
  var normalizeList = (value) => {
43
54
  if (!value) {
@@ -61,11 +72,27 @@ var normalizeConfig = (input) => {
61
72
  };
62
73
  }
63
74
  }
75
+ let check;
76
+ if (input.check) {
77
+ check = {
78
+ lint: input.check.lint,
79
+ typecheck: input.check.typecheck,
80
+ exec: input.check.exec
81
+ };
82
+ }
83
+ let lint;
84
+ if (input.lint) {
85
+ lint = {
86
+ rules: input.lint.rules
87
+ };
88
+ }
64
89
  return {
65
90
  include,
66
91
  exclude,
67
92
  plugins: input.plugins,
68
- docs
93
+ docs,
94
+ check,
95
+ lint
69
96
  };
70
97
  };
71
98
 
@@ -142,7 +169,7 @@ ${formatIssues(issues)}`);
142
169
  var defineConfig = (config) => config;
143
170
  // src/cli.ts
144
171
  import { readFileSync as readFileSync5 } from "node:fs";
145
- import * as path9 from "node:path";
172
+ import * as path11 from "node:path";
146
173
  import { fileURLToPath } from "node:url";
147
174
  import { Command } from "commander";
148
175
 
@@ -161,13 +188,16 @@ import {
161
188
  findPackageByName,
162
189
  findJSDocLocation,
163
190
  generateFixesForExport,
191
+ getDefaultConfig as getLintDefaultConfig,
164
192
  hasNonAssertionComments,
193
+ lintExport,
165
194
  mergeFixes,
166
195
  NodeFileSystem,
167
196
  parseAssertions,
168
197
  parseJSDocToPatch,
169
198
  runExamplesWithPackage,
170
- serializeJSDoc
199
+ serializeJSDoc,
200
+ typecheckExamples
171
201
  } from "@doccov/sdk";
172
202
  import chalk from "chalk";
173
203
 
@@ -274,7 +304,7 @@ function registerCheckCommand(program, dependencies = {}) {
274
304
  ...defaultDependencies,
275
305
  ...dependencies
276
306
  };
277
- program.command("check [entry]").description("Fail if documentation coverage falls below a threshold").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--require-examples", "Require at least one @example for every export").option("--run-examples", "Execute @example blocks and fail on runtime errors").option("--ignore-drift", "Do not fail on documentation drift").option("--skip-resolve", "Skip external type resolution from node_modules").option("--write", "Auto-fix drift issues").option("--only <types>", "Only fix specific drift types (comma-separated)").option("--dry-run", "Preview fixes without writing (requires --write)").action(async (entry, options) => {
307
+ program.command("check [entry]").description("Fail if documentation coverage falls below a threshold").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--require-examples", "Require at least one @example for every export").option("--exec", "Execute @example blocks at runtime").option("--no-lint", "Skip lint checks").option("--no-typecheck", "Skip example type checking").option("--ignore-drift", "Do not fail on documentation drift").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift and lint issues").option("--write", "Alias for --fix").option("--only <types>", "Only fix specific drift types (comma-separated)").option("--dry-run", "Preview fixes without writing (requires --fix)").action(async (entry, options) => {
278
308
  try {
279
309
  let targetDir = options.cwd;
280
310
  let entryFile = entry;
@@ -344,8 +374,56 @@ function registerCheckCommand(program, dependencies = {}) {
344
374
  }
345
375
  log("");
346
376
  }
377
+ const shouldFix = options.fix || options.write;
378
+ const lintViolations = [];
379
+ if (options.lint !== false) {
380
+ process.stdout.write(chalk.cyan(`> Running lint checks...
381
+ `));
382
+ const lintConfig = getLintDefaultConfig();
383
+ for (const exp of spec.exports ?? []) {
384
+ const violations = lintExport(exp, undefined, lintConfig);
385
+ for (const violation of violations) {
386
+ lintViolations.push({ exportName: exp.name, violation });
387
+ }
388
+ }
389
+ if (lintViolations.length === 0) {
390
+ process.stdout.write(chalk.green(`✓ No lint issues
391
+ `));
392
+ } else {
393
+ const errors = lintViolations.filter((v) => v.violation.severity === "error").length;
394
+ const warns = lintViolations.filter((v) => v.violation.severity === "warn").length;
395
+ process.stdout.write(chalk.yellow(`⚠ ${lintViolations.length} lint issue(s) (${errors} error, ${warns} warn)
396
+ `));
397
+ }
398
+ }
399
+ const typecheckErrors = [];
400
+ if (options.typecheck !== false) {
401
+ const allExamplesForTypecheck = [];
402
+ for (const exp of spec.exports ?? []) {
403
+ if (exp.examples && exp.examples.length > 0) {
404
+ allExamplesForTypecheck.push({ exportName: exp.name, examples: exp.examples });
405
+ }
406
+ }
407
+ if (allExamplesForTypecheck.length > 0) {
408
+ process.stdout.write(chalk.cyan(`> Type-checking examples...
409
+ `));
410
+ for (const { exportName, examples } of allExamplesForTypecheck) {
411
+ const result = typecheckExamples(examples, targetDir);
412
+ for (const err of result.errors) {
413
+ typecheckErrors.push({ exportName, error: err });
414
+ }
415
+ }
416
+ if (typecheckErrors.length === 0) {
417
+ process.stdout.write(chalk.green(`✓ All examples type-check
418
+ `));
419
+ } else {
420
+ process.stdout.write(chalk.red(`✗ ${typecheckErrors.length} type error(s)
421
+ `));
422
+ }
423
+ }
424
+ }
347
425
  const runtimeDrifts = [];
348
- if (options.runExamples) {
426
+ if (options.exec) {
349
427
  const allExamples = [];
350
428
  for (const entry2 of spec.exports ?? []) {
351
429
  if (entry2.examples && entry2.examples.length > 0) {
@@ -454,7 +532,7 @@ function registerCheckCommand(program, dependencies = {}) {
454
532
  const missingExamples = options.requireExamples ? failingExports.filter((item) => item.missing?.includes("examples")) : [];
455
533
  let driftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
456
534
  const fixedDriftKeys = new Set;
457
- if (options.write && driftExports.length > 0) {
535
+ if (shouldFix && driftExports.length > 0) {
458
536
  const allDrifts = collectDriftsFromExports(spec.exports ?? []);
459
537
  const filteredDrifts = filterDriftsByType(allDrifts, options.only);
460
538
  if (filteredDrifts.length === 0 && options.only) {
@@ -567,7 +645,9 @@ function registerCheckCommand(program, dependencies = {}) {
567
645
  const coverageFailed = coverageScore < minCoverage;
568
646
  const hasMissingExamples = missingExamples.length > 0;
569
647
  const hasDrift = !options.ignoreDrift && driftExports.length > 0;
570
- if (!coverageFailed && !hasMissingExamples && !hasDrift) {
648
+ const hasLintErrors = lintViolations.filter((v) => v.violation.severity === "error").length > 0;
649
+ const hasTypecheckErrors = typecheckErrors.length > 0;
650
+ if (!coverageFailed && !hasMissingExamples && !hasDrift && !hasLintErrors && !hasTypecheckErrors) {
571
651
  log(chalk.green(`✓ Docs coverage ${coverageScore}% (min ${minCoverage}%)`));
572
652
  if (failingExports.length > 0) {
573
653
  log(chalk.gray("Some exports have partial docs:"));
@@ -594,6 +674,20 @@ function registerCheckCommand(program, dependencies = {}) {
594
674
  if (hasMissingExamples) {
595
675
  error(chalk.red(`${missingExamples.length} export(s) missing examples (required via --require-examples)`));
596
676
  }
677
+ if (hasLintErrors) {
678
+ error("");
679
+ error(chalk.bold("Lint errors:"));
680
+ for (const { exportName, violation } of lintViolations.filter((v) => v.violation.severity === "error").slice(0, 10)) {
681
+ error(chalk.red(` • ${exportName}: ${violation.message}`));
682
+ }
683
+ }
684
+ if (hasTypecheckErrors) {
685
+ error("");
686
+ error(chalk.bold("Type errors in examples:"));
687
+ for (const { exportName, error: err } of typecheckErrors.slice(0, 10)) {
688
+ error(chalk.red(` • ${exportName} @example ${err.exampleIndex + 1}, line ${err.line}: ${err.message}`));
689
+ }
690
+ }
597
691
  if (failingExports.length > 0 || driftExports.length > 0) {
598
692
  error("");
599
693
  error(chalk.bold("Missing documentation details:"));
@@ -730,12 +824,37 @@ var defaultDependencies2 = {
730
824
  log: console.log,
731
825
  error: console.error
732
826
  };
827
+ var VALID_STRICT_OPTIONS = [
828
+ "regression",
829
+ "drift",
830
+ "docs-impact",
831
+ "breaking",
832
+ "undocumented",
833
+ "all"
834
+ ];
835
+ function parseStrictOptions(value) {
836
+ if (!value)
837
+ return new Set;
838
+ const options = value.split(",").map((s) => s.trim().toLowerCase());
839
+ const result = new Set;
840
+ for (const opt of options) {
841
+ if (opt === "all") {
842
+ for (const o of VALID_STRICT_OPTIONS) {
843
+ if (o !== "all")
844
+ result.add(o);
845
+ }
846
+ } else if (VALID_STRICT_OPTIONS.includes(opt)) {
847
+ result.add(opt);
848
+ }
849
+ }
850
+ return result;
851
+ }
733
852
  function registerDiffCommand(program, dependencies = {}) {
734
853
  const { readFileSync: readFileSync2, log, error } = {
735
854
  ...defaultDependencies2,
736
855
  ...dependencies
737
856
  };
738
- program.command("diff <base> <head>").description("Compare two OpenPkg specs and report coverage delta").option("--output <format>", "Output format: json or text", "text").option("--fail-on-regression", "Exit with error if coverage regressed").option("--fail-on-drift", "Exit with error if new drift was introduced").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--fail-on-docs-impact", "Exit with error if docs need updates").option("--ai", "Use AI for deeper analysis and fix suggestions").action(async (base, head, options) => {
857
+ program.command("diff <base> <head>").description("Compare two OpenPkg specs and report coverage delta").option("--format <format>", "Output format: text, json, github, report", "text").option("--strict <options>", "Fail on conditions (comma-separated): regression, drift, docs-impact, breaking, undocumented, all").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--output <format>", "DEPRECATED: Use --format instead").option("--fail-on-regression", "DEPRECATED: Use --strict regression").option("--fail-on-drift", "DEPRECATED: Use --strict drift").option("--fail-on-docs-impact", "DEPRECATED: Use --strict docs-impact").action(async (base, head, options) => {
739
858
  try {
740
859
  const baseSpec = loadSpec(base, readFileSync2);
741
860
  const headSpec = loadSpec(head, readFileSync2);
@@ -752,52 +871,81 @@ function registerDiffCommand(program, dependencies = {}) {
752
871
  markdownFiles = await loadMarkdownFiles(docsPatterns);
753
872
  }
754
873
  const diff = diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
755
- const format = options.output ?? "text";
756
- if (format === "json") {
757
- log(JSON.stringify(diff, null, 2));
758
- } else {
759
- printTextDiff(diff, log, error);
760
- if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
761
- if (!isAIDocsAnalysisAvailable()) {
762
- log(chalk2.yellow(`
874
+ const format = options.format ?? options.output ?? "text";
875
+ const strictOptions = parseStrictOptions(options.strict);
876
+ if (options.failOnRegression)
877
+ strictOptions.add("regression");
878
+ if (options.failOnDrift)
879
+ strictOptions.add("drift");
880
+ if (options.failOnDocsImpact)
881
+ strictOptions.add("docs-impact");
882
+ switch (format) {
883
+ case "json":
884
+ log(JSON.stringify(diff, null, 2));
885
+ break;
886
+ case "github":
887
+ printGitHubAnnotations(diff, log);
888
+ break;
889
+ case "report":
890
+ log(generateHTMLReport(diff));
891
+ break;
892
+ case "text":
893
+ default:
894
+ printTextDiff(diff, log, error);
895
+ if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
896
+ if (!isAIDocsAnalysisAvailable()) {
897
+ log(chalk2.yellow(`
763
898
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
764
- } else {
765
- log(chalk2.gray(`
899
+ } else {
900
+ log(chalk2.gray(`
766
901
  Generating AI summary...`));
767
- const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
768
- file: f.file,
769
- exportName: r.exportName,
770
- changeType: r.changeType,
771
- context: r.context
772
- })));
773
- const summary = await generateImpactSummary(impacts);
774
- if (summary) {
775
- log("");
776
- log(chalk2.bold("AI Summary"));
777
- log(chalk2.cyan(` ${summary}`));
902
+ const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
903
+ file: f.file,
904
+ exportName: r.exportName,
905
+ changeType: r.changeType,
906
+ context: r.context
907
+ })));
908
+ const summary = await generateImpactSummary(impacts);
909
+ if (summary) {
910
+ log("");
911
+ log(chalk2.bold("AI Summary"));
912
+ log(chalk2.cyan(` ${summary}`));
913
+ }
778
914
  }
779
915
  }
780
- }
916
+ break;
781
917
  }
782
- if (options.failOnRegression && diff.coverageDelta < 0) {
918
+ if (strictOptions.has("regression") && diff.coverageDelta < 0) {
783
919
  error(chalk2.red(`
784
920
  Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
785
921
  process.exitCode = 1;
786
922
  return;
787
923
  }
788
- if (options.failOnDrift && diff.driftIntroduced > 0) {
924
+ if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
789
925
  error(chalk2.red(`
790
926
  ${diff.driftIntroduced} new drift issue(s) introduced`));
791
927
  process.exitCode = 1;
792
928
  return;
793
929
  }
794
- if (options.failOnDocsImpact && hasDocsImpact(diff)) {
930
+ if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
795
931
  const summary = getDocsImpactSummary(diff);
796
932
  error(chalk2.red(`
797
933
  ${summary.totalIssues} docs issue(s) require attention`));
798
934
  process.exitCode = 1;
799
935
  return;
800
936
  }
937
+ if (strictOptions.has("breaking") && diff.breaking.length > 0) {
938
+ error(chalk2.red(`
939
+ ${diff.breaking.length} breaking change(s) detected`));
940
+ process.exitCode = 1;
941
+ return;
942
+ }
943
+ if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
944
+ error(chalk2.red(`
945
+ ${diff.newUndocumented.length} new undocumented export(s)`));
946
+ process.exitCode = 1;
947
+ return;
948
+ }
801
949
  } catch (commandError) {
802
950
  error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
803
951
  process.exitCode = 1;
@@ -836,142 +984,376 @@ function printTextDiff(diff, log, _error) {
836
984
  log("");
837
985
  log(chalk2.bold("DocCov Diff Report"));
838
986
  log("─".repeat(40));
987
+ printCoverage(diff, log);
988
+ printAPIChanges(diff, log);
989
+ if (diff.docsImpact) {
990
+ printDocsRequiringUpdates(diff, log);
991
+ }
992
+ log("");
993
+ }
994
+ function printCoverage(diff, log) {
839
995
  const coverageColor = diff.coverageDelta > 0 ? chalk2.green : diff.coverageDelta < 0 ? chalk2.red : chalk2.gray;
840
996
  const coverageSymbol = diff.coverageDelta > 0 ? "↑" : diff.coverageDelta < 0 ? "↓" : "→";
841
997
  const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
842
998
  log("");
843
999
  log(chalk2.bold("Coverage"));
844
1000
  log(` ${diff.oldCoverage}% ${coverageSymbol} ${diff.newCoverage}% ${coverageColor(`(${deltaStr}%)`)}`);
845
- if (diff.breaking.length > 0 || diff.nonBreaking.length > 0) {
846
- log("");
847
- log(chalk2.bold("API Changes"));
848
- if (diff.breaking.length > 0) {
849
- log(chalk2.red(` ${diff.breaking.length} breaking change(s)`));
850
- for (const id of diff.breaking.slice(0, 5)) {
851
- log(chalk2.red(` - ${id}`));
852
- }
853
- if (diff.breaking.length > 5) {
854
- log(chalk2.gray(` ... and ${diff.breaking.length - 5} more`));
855
- }
1001
+ }
1002
+ function printAPIChanges(diff, log) {
1003
+ const hasChanges = diff.breaking.length > 0 || diff.nonBreaking.length > 0 || diff.memberChanges && diff.memberChanges.length > 0;
1004
+ if (!hasChanges)
1005
+ return;
1006
+ log("");
1007
+ log(chalk2.bold("API Changes"));
1008
+ const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
1009
+ const classesWithMembers = new Set(membersByClass.keys());
1010
+ for (const [className, changes] of membersByClass) {
1011
+ const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
1012
+ const isHighSeverity = categorized?.severity === "high";
1013
+ const label = isHighSeverity ? chalk2.red(" [BREAKING]") : chalk2.yellow(" [CHANGED]");
1014
+ log(chalk2.cyan(` ${className}`) + label);
1015
+ const removed = changes.filter((c) => c.changeType === "removed");
1016
+ for (const mc of removed) {
1017
+ const suggestion = mc.suggestion ? chalk2.gray(` → ${mc.suggestion}`) : "";
1018
+ log(chalk2.red(` ✖ ${mc.memberName}()`) + suggestion);
856
1019
  }
857
- if (diff.nonBreaking.length > 0) {
858
- log(chalk2.green(` ${diff.nonBreaking.length} new export(s)`));
859
- for (const id of diff.nonBreaking.slice(0, 5)) {
860
- log(chalk2.green(` + ${id}`));
861
- }
862
- if (diff.nonBreaking.length > 5) {
863
- log(chalk2.gray(` ... and ${diff.nonBreaking.length - 5} more`));
1020
+ const changed = changes.filter((c) => c.changeType === "signature-changed");
1021
+ for (const mc of changed) {
1022
+ log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
1023
+ if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
1024
+ log(chalk2.gray(` was: ${mc.oldSignature}`));
1025
+ log(chalk2.gray(` now: ${mc.newSignature}`));
864
1026
  }
865
1027
  }
1028
+ const added = changes.filter((c) => c.changeType === "added");
1029
+ if (added.length > 0) {
1030
+ const addedNames = added.map((a) => a.memberName + "()").join(", ");
1031
+ log(chalk2.green(` + ${addedNames}`));
1032
+ }
866
1033
  }
867
- if (diff.memberChanges && diff.memberChanges.length > 0) {
1034
+ const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
1035
+ const typeChanges = nonClassBreaking.filter((c) => c.kind === "interface" || c.kind === "type" || c.kind === "enum");
1036
+ const functionChanges = nonClassBreaking.filter((c) => c.kind === "function");
1037
+ const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
1038
+ if (functionChanges.length > 0) {
868
1039
  log("");
869
- log(chalk2.bold("Member Changes"));
870
- const byClass = new Map;
871
- for (const mc of diff.memberChanges) {
872
- const list = byClass.get(mc.className) ?? [];
873
- list.push(mc);
874
- byClass.set(mc.className, list);
1040
+ log(chalk2.red(` Function Changes (${functionChanges.length}):`));
1041
+ for (const fc of functionChanges.slice(0, 3)) {
1042
+ const reason = fc.reason === "removed" ? "removed" : "signature changed";
1043
+ log(chalk2.red(` ✖ ${fc.name} (${reason})`));
875
1044
  }
876
- for (const [className, changes] of byClass) {
877
- log(chalk2.cyan(` ${className}:`));
878
- const removed = changes.filter((c) => c.changeType === "removed");
879
- const added = changes.filter((c) => c.changeType === "added");
880
- const changed = changes.filter((c) => c.changeType === "signature-changed");
881
- for (const mc of removed.slice(0, 3)) {
882
- const suggestion = mc.suggestion ? ` (${mc.suggestion})` : "";
883
- log(chalk2.red(` - ${mc.memberName}()${suggestion}`));
884
- }
885
- for (const mc of added.slice(0, 3)) {
886
- log(chalk2.green(` + ${mc.memberName}()`));
887
- }
888
- for (const mc of changed.slice(0, 3)) {
889
- log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
890
- }
891
- const total = removed.length + added.length + changed.length;
892
- const shown = Math.min(removed.length, 3) + Math.min(added.length, 3) + Math.min(changed.length, 3);
893
- if (total > shown) {
894
- log(chalk2.gray(` ... and ${total - shown} more member change(s)`));
895
- }
1045
+ if (functionChanges.length > 3) {
1046
+ log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
896
1047
  }
897
1048
  }
898
- log("");
899
- log(chalk2.bold("Docs Health"));
900
- if (diff.newUndocumented.length > 0) {
901
- log(chalk2.yellow(` ${diff.newUndocumented.length} new undocumented export(s)`));
902
- for (const id of diff.newUndocumented.slice(0, 5)) {
903
- log(chalk2.yellow(` ! ${id}`));
904
- }
905
- if (diff.newUndocumented.length > 5) {
906
- log(chalk2.gray(` ... and ${diff.newUndocumented.length - 5} more`));
907
- }
1049
+ if (typeChanges.length > 0) {
1050
+ log("");
1051
+ log(chalk2.yellow(` Type/Interface Changes (${typeChanges.length}):`));
1052
+ const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
1053
+ log(chalk2.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
908
1054
  }
909
- if (diff.improvedExports.length > 0) {
910
- log(chalk2.green(` ${diff.improvedExports.length} export(s) improved docs`));
1055
+ if (otherChanges.length > 0) {
1056
+ log("");
1057
+ log(chalk2.gray(` Other Changes (${otherChanges.length}):`));
1058
+ const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
1059
+ log(chalk2.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
911
1060
  }
912
- if (diff.regressedExports.length > 0) {
913
- log(chalk2.red(` ${diff.regressedExports.length} export(s) regressed docs`));
914
- for (const id of diff.regressedExports.slice(0, 5)) {
915
- log(chalk2.red(` ↓ ${id}`));
916
- }
1061
+ if (diff.nonBreaking.length > 0) {
1062
+ const undocCount = diff.newUndocumented.length;
1063
+ const undocSuffix = undocCount > 0 ? chalk2.yellow(` (${undocCount} undocumented)`) : "";
1064
+ log("");
1065
+ log(chalk2.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
1066
+ const exportNames = diff.nonBreaking.slice(0, 3);
1067
+ log(chalk2.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
917
1068
  }
918
1069
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
919
1070
  log("");
920
- log(chalk2.bold("Drift"));
1071
+ const parts = [];
921
1072
  if (diff.driftIntroduced > 0) {
922
- log(chalk2.red(` +${diff.driftIntroduced} new drift issue(s)`));
1073
+ parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
923
1074
  }
924
1075
  if (diff.driftResolved > 0) {
925
- log(chalk2.green(` -${diff.driftResolved} drift issue(s) resolved`));
1076
+ parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
926
1077
  }
1078
+ log(` Drift: ${parts.join(", ")}`);
927
1079
  }
928
- if (diff.docsImpact) {
1080
+ }
1081
+ function printDocsRequiringUpdates(diff, log) {
1082
+ if (!diff.docsImpact)
1083
+ return;
1084
+ const { impactedFiles, missingDocs, stats } = diff.docsImpact;
1085
+ log("");
1086
+ log(chalk2.bold("Docs Requiring Updates"));
1087
+ log(chalk2.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
1088
+ if (impactedFiles.length === 0 && missingDocs.length === 0) {
1089
+ log(chalk2.green(" ✓ No updates needed"));
1090
+ return;
1091
+ }
1092
+ const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
1093
+ const actionableFiles = [];
1094
+ const instantiationOnlyFiles = [];
1095
+ for (const file of sortedFiles) {
1096
+ const hasActionableRefs = file.references.some((r) => r.memberName && !r.isInstantiation || !r.memberName && !r.isInstantiation);
1097
+ if (hasActionableRefs) {
1098
+ actionableFiles.push(file);
1099
+ } else {
1100
+ instantiationOnlyFiles.push(file);
1101
+ }
1102
+ }
1103
+ for (const file of actionableFiles.slice(0, 6)) {
1104
+ const filename = path3.basename(file.file);
1105
+ const issueCount = file.references.length;
929
1106
  log("");
930
- log(chalk2.bold("Docs Impact"));
931
- const { impactedFiles, missingDocs, stats } = diff.docsImpact;
932
- log(chalk2.gray(` Scanned ${stats.filesScanned} file(s), ${stats.codeBlocksFound} code block(s)`));
933
- if (impactedFiles.length > 0) {
934
- log("");
935
- log(chalk2.yellow(` ${impactedFiles.length} file(s) need updates:`));
936
- for (const file of impactedFiles.slice(0, 10)) {
937
- log(chalk2.yellow(` \uD83D\uDCC4 ${file.file}`));
938
- for (const ref of file.references.slice(0, 5)) {
939
- if (ref.memberName) {
940
- const changeLabel = ref.changeType === "method-removed" ? "removed" : ref.changeType === "method-changed" ? "signature changed" : ref.changeType === "method-deprecated" ? "deprecated" : "changed";
941
- log(chalk2.gray(` Line ${ref.line}: ${ref.memberName}() ${changeLabel}`));
942
- if (ref.replacementSuggestion) {
943
- log(chalk2.cyan(` → ${ref.replacementSuggestion}`));
944
- }
945
- } else if (ref.isInstantiation) {
946
- log(chalk2.gray(` Line ${ref.line}: new ${ref.exportName}() (class changed)`));
947
- } else {
948
- const changeLabel = ref.changeType === "signature-changed" ? "signature changed" : ref.changeType === "removed" ? "removed" : "deprecated";
949
- log(chalk2.gray(` Line ${ref.line}: ${ref.exportName} (${changeLabel})`));
950
- }
951
- }
952
- if (file.references.length > 5) {
953
- log(chalk2.gray(` ... and ${file.references.length - 5} more reference(s)`));
954
- }
1107
+ log(chalk2.yellow(` ${filename}`) + chalk2.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
1108
+ const actionableRefs = file.references.filter((r) => !r.isInstantiation);
1109
+ for (const ref of actionableRefs.slice(0, 4)) {
1110
+ if (ref.memberName) {
1111
+ const action = ref.changeType === "method-removed" ? "→" : "~";
1112
+ const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
1113
+ log(chalk2.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
1114
+ } else {
1115
+ const action = ref.changeType === "removed" ? "→" : "~";
1116
+ const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
1117
+ log(chalk2.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
1118
+ }
1119
+ }
1120
+ if (actionableRefs.length > 4) {
1121
+ log(chalk2.gray(` ... and ${actionableRefs.length - 4} more`));
1122
+ }
1123
+ }
1124
+ if (actionableFiles.length > 6) {
1125
+ log(chalk2.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
1126
+ }
1127
+ if (instantiationOnlyFiles.length > 0) {
1128
+ log("");
1129
+ const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path3.basename(f.file));
1130
+ const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
1131
+ log(chalk2.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
1132
+ log(chalk2.gray(` ${fileNames.join(", ")}${suffix}`));
1133
+ }
1134
+ const { allUndocumented } = diff.docsImpact;
1135
+ if (missingDocs.length > 0) {
1136
+ log("");
1137
+ log(chalk2.yellow(` New exports missing docs (${missingDocs.length}):`));
1138
+ const names = missingDocs.slice(0, 4);
1139
+ log(chalk2.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
1140
+ }
1141
+ if (allUndocumented && allUndocumented.length > 0) {
1142
+ const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
1143
+ log("");
1144
+ log(chalk2.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
1145
+ if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
1146
+ log(chalk2.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
1147
+ }
1148
+ }
1149
+ }
1150
+ function groupMemberChangesByClass(memberChanges) {
1151
+ const byClass = new Map;
1152
+ for (const mc of memberChanges) {
1153
+ const list = byClass.get(mc.className) ?? [];
1154
+ list.push(mc);
1155
+ byClass.set(mc.className, list);
1156
+ }
1157
+ return byClass;
1158
+ }
1159
+ function printGitHubAnnotations(diff, log) {
1160
+ if (diff.coverageDelta !== 0) {
1161
+ const level = diff.coverageDelta < 0 ? "warning" : "notice";
1162
+ const sign = diff.coverageDelta > 0 ? "+" : "";
1163
+ log(`::${level} title=Coverage Change::Coverage ${diff.oldCoverage}% → ${diff.newCoverage}% (${sign}${diff.coverageDelta}%)`);
1164
+ }
1165
+ for (const breaking of diff.categorizedBreaking ?? []) {
1166
+ const level = breaking.severity === "high" ? "error" : "warning";
1167
+ log(`::${level} title=Breaking Change::${breaking.name} - ${breaking.reason}`);
1168
+ }
1169
+ for (const mc of diff.memberChanges ?? []) {
1170
+ if (mc.changeType === "removed") {
1171
+ const suggestion = mc.suggestion ? ` ${mc.suggestion}` : "";
1172
+ log(`::warning title=Method Removed::${mc.className}.${mc.memberName}() removed.${suggestion}`);
1173
+ } else if (mc.changeType === "signature-changed") {
1174
+ log(`::warning title=Signature Changed::${mc.className}.${mc.memberName}() signature changed`);
1175
+ }
1176
+ }
1177
+ if (diff.docsImpact) {
1178
+ for (const file of diff.docsImpact.impactedFiles) {
1179
+ for (const ref of file.references) {
1180
+ const level = ref.changeType === "removed" || ref.changeType === "method-removed" ? "error" : "warning";
1181
+ const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
1182
+ const changeDesc = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : ref.changeType === "signature-changed" || ref.changeType === "method-changed" ? "signature changed" : "changed";
1183
+ const suggestion = ref.replacementSuggestion ? ` → ${ref.replacementSuggestion}` : "";
1184
+ log(`::${level} file=${file.file},line=${ref.line},title=API Change::${name} ${changeDesc}${suggestion}`);
955
1185
  }
956
- if (impactedFiles.length > 10) {
957
- log(chalk2.gray(` ... and ${impactedFiles.length - 10} more file(s)`));
1186
+ }
1187
+ for (const name of diff.docsImpact.missingDocs) {
1188
+ log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
1189
+ }
1190
+ const { stats, allUndocumented } = diff.docsImpact;
1191
+ if (allUndocumented && allUndocumented.length > 0) {
1192
+ const docPercent = Math.round((1 - allUndocumented.length / stats.totalExports) * 100);
1193
+ log(`::notice title=Documentation Coverage::${stats.documentedExports}/${stats.totalExports} exports documented (${docPercent}%)`);
1194
+ }
1195
+ }
1196
+ if (!diff.docsImpact && diff.newUndocumented.length > 0) {
1197
+ for (const name of diff.newUndocumented) {
1198
+ log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
1199
+ }
1200
+ }
1201
+ if (diff.driftIntroduced > 0) {
1202
+ log(`::warning title=Drift Detected::${diff.driftIntroduced} new drift issue(s) introduced`);
1203
+ }
1204
+ }
1205
+ function generateHTMLReport(diff) {
1206
+ const coverageClass = diff.coverageDelta > 0 ? "positive" : diff.coverageDelta < 0 ? "negative" : "neutral";
1207
+ const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1208
+ let html = `<!DOCTYPE html>
1209
+ <html lang="en">
1210
+ <head>
1211
+ <meta charset="UTF-8">
1212
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1213
+ <title>DocCov Diff Report</title>
1214
+ <style>
1215
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1216
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; line-height: 1.5; }
1217
+ .container { max-width: 900px; margin: 0 auto; }
1218
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #f0f6fc; }
1219
+ h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
1220
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
1221
+ .metric { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
1222
+ .metric-label { color: #8b949e; }
1223
+ .metric-value { font-weight: 600; }
1224
+ .positive { color: #3fb950; }
1225
+ .negative { color: #f85149; }
1226
+ .neutral { color: #8b949e; }
1227
+ .warning { color: #d29922; }
1228
+ .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
1229
+ .badge-breaking { background: #f8514933; color: #f85149; }
1230
+ .badge-changed { background: #d2992233; color: #d29922; }
1231
+ .badge-added { background: #3fb95033; color: #3fb950; }
1232
+ .file-item { padding: 0.5rem; margin: 0.25rem 0; background: #0d1117; border-radius: 4px; }
1233
+ .file-name { font-family: monospace; font-size: 0.9rem; }
1234
+ .ref-list { margin-top: 0.5rem; padding-left: 1rem; font-size: 0.85rem; color: #8b949e; }
1235
+ .ref-item { margin: 0.25rem 0; }
1236
+ ul { list-style: none; }
1237
+ li { padding: 0.25rem 0; }
1238
+ code { font-family: monospace; background: #0d1117; padding: 0.125rem 0.375rem; border-radius: 3px; font-size: 0.9rem; }
1239
+ </style>
1240
+ </head>
1241
+ <body>
1242
+ <div class="container">
1243
+ <h1>\uD83D\uDCCA DocCov Diff Report</h1>
1244
+
1245
+ <div class="card">
1246
+ <div class="metric">
1247
+ <span class="metric-label">Coverage</span>
1248
+ <span class="metric-value ${coverageClass}">${diff.oldCoverage}% → ${diff.newCoverage}% (${coverageSign}${diff.coverageDelta}%)</span>
1249
+ </div>
1250
+ <div class="metric">
1251
+ <span class="metric-label">Breaking Changes</span>
1252
+ <span class="metric-value ${diff.breaking.length > 0 ? "negative" : "neutral"}">${diff.breaking.length}</span>
1253
+ </div>
1254
+ <div class="metric">
1255
+ <span class="metric-label">New Exports</span>
1256
+ <span class="metric-value positive">${diff.nonBreaking.length}</span>
1257
+ </div>
1258
+ <div class="metric">
1259
+ <span class="metric-label">Undocumented</span>
1260
+ <span class="metric-value ${diff.newUndocumented.length > 0 ? "warning" : "neutral"}">${diff.newUndocumented.length}</span>
1261
+ </div>
1262
+ </div>`;
1263
+ if (diff.breaking.length > 0) {
1264
+ html += `
1265
+ <h2>Breaking Changes</h2>
1266
+ <div class="card">
1267
+ <ul>`;
1268
+ for (const item of diff.categorizedBreaking ?? []) {
1269
+ const badgeClass = item.severity === "high" ? "badge-breaking" : "badge-changed";
1270
+ html += `
1271
+ <li><code>${item.name}</code> <span class="badge ${badgeClass}">${item.reason}</span></li>`;
1272
+ }
1273
+ html += `
1274
+ </ul>
1275
+ </div>`;
1276
+ }
1277
+ if (diff.memberChanges && diff.memberChanges.length > 0) {
1278
+ html += `
1279
+ <h2>Member Changes</h2>
1280
+ <div class="card">
1281
+ <ul>`;
1282
+ for (const mc of diff.memberChanges) {
1283
+ const badgeClass = mc.changeType === "removed" ? "badge-breaking" : mc.changeType === "added" ? "badge-added" : "badge-changed";
1284
+ const suggestion = mc.suggestion ? ` → ${mc.suggestion}` : "";
1285
+ html += `
1286
+ <li><code>${mc.className}.${mc.memberName}()</code> <span class="badge ${badgeClass}">${mc.changeType}</span>${suggestion}</li>`;
1287
+ }
1288
+ html += `
1289
+ </ul>
1290
+ </div>`;
1291
+ }
1292
+ if (diff.docsImpact && diff.docsImpact.impactedFiles.length > 0) {
1293
+ html += `
1294
+ <h2>Documentation Impact</h2>
1295
+ <div class="card">`;
1296
+ for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
1297
+ const filename = path3.basename(file.file);
1298
+ html += `
1299
+ <div class="file-item">
1300
+ <div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
1301
+ <div class="ref-list">`;
1302
+ for (const ref of file.references.slice(0, 5)) {
1303
+ const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
1304
+ const change = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : "signature changed";
1305
+ html += `
1306
+ <div class="ref-item">Line ${ref.line}: <code>${name}</code> ${change}</div>`;
1307
+ }
1308
+ if (file.references.length > 5) {
1309
+ html += `
1310
+ <div class="ref-item neutral">... and ${file.references.length - 5} more</div>`;
958
1311
  }
1312
+ html += `
1313
+ </div>
1314
+ </div>`;
959
1315
  }
960
- if (missingDocs.length > 0) {
961
- log("");
962
- log(chalk2.yellow(` ${missingDocs.length} new export(s) missing docs:`));
963
- for (const name of missingDocs.slice(0, 5)) {
964
- log(chalk2.yellow(` • ${name}`));
1316
+ html += `
1317
+ </div>`;
1318
+ }
1319
+ const hasNewUndocumented = diff.newUndocumented.length > 0;
1320
+ const hasAllUndocumented = diff.docsImpact?.allUndocumented && diff.docsImpact.allUndocumented.length > 0;
1321
+ if (hasNewUndocumented || hasAllUndocumented) {
1322
+ html += `
1323
+ <h2>Missing Documentation</h2>
1324
+ <div class="card">`;
1325
+ if (hasNewUndocumented) {
1326
+ html += `
1327
+ <p class="warning">New exports missing docs (${diff.newUndocumented.length}):</p>
1328
+ <ul>`;
1329
+ for (const name of diff.newUndocumented.slice(0, 10)) {
1330
+ html += `
1331
+ <li><code>${name}</code></li>`;
965
1332
  }
966
- if (missingDocs.length > 5) {
967
- log(chalk2.gray(` ... and ${missingDocs.length - 5} more`));
1333
+ if (diff.newUndocumented.length > 10) {
1334
+ html += `
1335
+ <li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
968
1336
  }
1337
+ html += `
1338
+ </ul>`;
969
1339
  }
970
- if (impactedFiles.length === 0 && missingDocs.length === 0) {
971
- log(chalk2.green(" ✓ No docs impact detected"));
1340
+ if (diff.docsImpact?.stats) {
1341
+ const { stats, allUndocumented } = diff.docsImpact;
1342
+ const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
1343
+ html += `
1344
+ <div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
1345
+ <span class="metric-label">Total Documentation Coverage</span>
1346
+ <span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
1347
+ </div>`;
972
1348
  }
1349
+ html += `
1350
+ </div>`;
973
1351
  }
974
- log("");
1352
+ html += `
1353
+ </div>
1354
+ </body>
1355
+ </html>`;
1356
+ return html;
975
1357
  }
976
1358
 
977
1359
  // src/commands/generate.ts
@@ -1306,17 +1688,225 @@ var buildTemplate = (format) => {
1306
1688
  `);
1307
1689
  };
1308
1690
 
1309
- // src/commands/report.ts
1691
+ // src/commands/lint.ts
1310
1692
  import * as fs5 from "node:fs";
1311
1693
  import * as path6 from "node:path";
1312
1694
  import {
1313
- DocCov as DocCov3,
1695
+ applyEdits as applyEdits2,
1696
+ createSourceFile as createSourceFile2,
1314
1697
  detectEntryPoint as detectEntryPoint3,
1315
1698
  detectMonorepo as detectMonorepo3,
1699
+ DocCov as DocCov3,
1700
+ findJSDocLocation as findJSDocLocation2,
1316
1701
  findPackageByName as findPackageByName3,
1317
- NodeFileSystem as NodeFileSystem3
1702
+ getDefaultConfig,
1703
+ getRule,
1704
+ lintExport as lintExport2,
1705
+ NodeFileSystem as NodeFileSystem3,
1706
+ serializeJSDoc as serializeJSDoc2
1318
1707
  } from "@doccov/sdk";
1319
1708
  import chalk6 from "chalk";
1709
+ var defaultDependencies5 = {
1710
+ createDocCov: (options) => new DocCov3(options),
1711
+ log: console.log,
1712
+ error: console.error
1713
+ };
1714
+ function getRawJSDoc(exp, targetDir) {
1715
+ if (!exp.source?.file)
1716
+ return;
1717
+ const filePath = path6.resolve(targetDir, exp.source.file);
1718
+ if (!fs5.existsSync(filePath))
1719
+ return;
1720
+ try {
1721
+ const sourceFile = createSourceFile2(filePath);
1722
+ const location = findJSDocLocation2(sourceFile, exp.name, exp.source.line);
1723
+ return location?.existingJSDoc;
1724
+ } catch {
1725
+ return;
1726
+ }
1727
+ }
1728
+ function registerLintCommand(program, dependencies = {}) {
1729
+ const { createDocCov, log, error } = {
1730
+ ...defaultDependencies5,
1731
+ ...dependencies
1732
+ };
1733
+ program.command("lint [entry]").description("Lint documentation for style and quality issues").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--fix", "Auto-fix fixable issues").option("--write", "Alias for --fix").option("--rule <name>", "Run only a specific rule").option("--skip-resolve", "Skip external type resolution").action(async (entry, options) => {
1734
+ try {
1735
+ let targetDir = options.cwd;
1736
+ let entryFile = entry;
1737
+ const fileSystem = new NodeFileSystem3(options.cwd);
1738
+ if (options.package) {
1739
+ const mono = await detectMonorepo3(fileSystem);
1740
+ if (!mono.isMonorepo) {
1741
+ throw new Error("Not a monorepo. Remove --package flag.");
1742
+ }
1743
+ const pkg = findPackageByName3(mono.packages, options.package);
1744
+ if (!pkg) {
1745
+ const available = mono.packages.map((p) => p.name).join(", ");
1746
+ throw new Error(`Package "${options.package}" not found. Available: ${available}`);
1747
+ }
1748
+ targetDir = path6.join(options.cwd, pkg.path);
1749
+ log(chalk6.gray(`Found package at ${pkg.path}`));
1750
+ }
1751
+ if (!entryFile) {
1752
+ const targetFs = new NodeFileSystem3(targetDir);
1753
+ const detected = await detectEntryPoint3(targetFs);
1754
+ entryFile = path6.join(targetDir, detected.path);
1755
+ log(chalk6.gray(`Auto-detected entry point: ${detected.path}`));
1756
+ } else {
1757
+ entryFile = path6.resolve(targetDir, entryFile);
1758
+ if (fs5.existsSync(entryFile) && fs5.statSync(entryFile).isDirectory()) {
1759
+ targetDir = entryFile;
1760
+ const dirFs = new NodeFileSystem3(entryFile);
1761
+ const detected = await detectEntryPoint3(dirFs);
1762
+ entryFile = path6.join(entryFile, detected.path);
1763
+ log(chalk6.gray(`Auto-detected entry point: ${detected.path}`));
1764
+ }
1765
+ }
1766
+ const resolveExternalTypes = !options.skipResolve;
1767
+ process.stdout.write(chalk6.cyan(`> Analyzing documentation...
1768
+ `));
1769
+ const doccov = createDocCov({ resolveExternalTypes });
1770
+ const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
1771
+ if (!specResult) {
1772
+ throw new Error("Failed to analyze documentation.");
1773
+ }
1774
+ process.stdout.write(chalk6.cyan(`> Running lint rules...
1775
+ `));
1776
+ let config = getDefaultConfig();
1777
+ if (options.rule) {
1778
+ const rule = getRule(options.rule);
1779
+ if (!rule) {
1780
+ throw new Error(`Unknown rule: ${options.rule}`);
1781
+ }
1782
+ const rules = {};
1783
+ for (const key of Object.keys(config.rules)) {
1784
+ rules[key] = "off";
1785
+ }
1786
+ rules[options.rule] = rule.defaultSeverity === "off" ? "warn" : rule.defaultSeverity;
1787
+ config = { rules };
1788
+ }
1789
+ const exportsWithJSDoc = [];
1790
+ for (const exp of specResult.spec.exports ?? []) {
1791
+ const rawJSDoc = getRawJSDoc(exp, targetDir);
1792
+ exportsWithJSDoc.push({
1793
+ export: exp,
1794
+ rawJSDoc,
1795
+ filePath: exp.source?.file ? path6.resolve(targetDir, exp.source.file) : undefined
1796
+ });
1797
+ }
1798
+ const allViolations = [];
1799
+ for (const { export: exp, rawJSDoc, filePath } of exportsWithJSDoc) {
1800
+ const violations = lintExport2(exp, rawJSDoc, config);
1801
+ for (const violation of violations) {
1802
+ allViolations.push({ export: exp, violation, filePath, rawJSDoc });
1803
+ }
1804
+ }
1805
+ const shouldFix = options.fix || options.write;
1806
+ const fixableViolations = allViolations.filter((v) => v.violation.fixable);
1807
+ if (shouldFix && fixableViolations.length > 0) {
1808
+ process.stdout.write(chalk6.cyan(`> Applying fixes...
1809
+ `));
1810
+ const edits = [];
1811
+ for (const { export: exp, rawJSDoc, filePath } of fixableViolations) {
1812
+ if (!filePath || !rawJSDoc)
1813
+ continue;
1814
+ if (filePath.endsWith(".d.ts"))
1815
+ continue;
1816
+ const sourceFile = createSourceFile2(filePath);
1817
+ const location = findJSDocLocation2(sourceFile, exp.name, exp.source?.line);
1818
+ if (!location)
1819
+ continue;
1820
+ const rule = getRule("consistent-param-style");
1821
+ if (!rule?.fix)
1822
+ continue;
1823
+ const patch = rule.fix(exp, rawJSDoc);
1824
+ if (!patch)
1825
+ continue;
1826
+ const newJSDoc = serializeJSDoc2(patch, location.indent);
1827
+ edits.push({
1828
+ filePath,
1829
+ symbolName: exp.name,
1830
+ startLine: location.startLine,
1831
+ endLine: location.endLine,
1832
+ hasExisting: location.hasExisting,
1833
+ existingJSDoc: location.existingJSDoc,
1834
+ newJSDoc,
1835
+ indent: location.indent
1836
+ });
1837
+ }
1838
+ if (edits.length > 0) {
1839
+ const result = await applyEdits2(edits);
1840
+ if (result.errors.length > 0) {
1841
+ for (const err of result.errors) {
1842
+ error(chalk6.red(` ${err.file}: ${err.error}`));
1843
+ }
1844
+ } else {
1845
+ process.stdout.write(chalk6.green(`✓ Fixed ${result.editsApplied} issue(s) in ${result.filesModified} file(s)
1846
+ `));
1847
+ }
1848
+ const fixedExports = new Set(edits.map((e) => e.symbolName));
1849
+ const remaining = allViolations.filter((v) => !v.violation.fixable || !fixedExports.has(v.export.name));
1850
+ allViolations.length = 0;
1851
+ allViolations.push(...remaining);
1852
+ }
1853
+ }
1854
+ if (allViolations.length === 0) {
1855
+ log(chalk6.green("✓ No lint issues found"));
1856
+ return;
1857
+ }
1858
+ const byFile = new Map;
1859
+ for (const v of allViolations) {
1860
+ const file = v.filePath ?? "unknown";
1861
+ const existing = byFile.get(file) ?? [];
1862
+ existing.push(v);
1863
+ byFile.set(file, existing);
1864
+ }
1865
+ log("");
1866
+ for (const [filePath, violations] of byFile) {
1867
+ const relativePath = path6.relative(targetDir, filePath);
1868
+ log(chalk6.underline(relativePath));
1869
+ for (const { export: exp, violation } of violations) {
1870
+ const line = exp.source?.line ?? 0;
1871
+ const severity = violation.severity === "error" ? chalk6.red("error") : chalk6.yellow("warning");
1872
+ const fixable = violation.fixable ? chalk6.gray(" (fixable)") : "";
1873
+ log(` ${line}:1 ${severity} ${violation.message} ${chalk6.gray(violation.rule)}${fixable}`);
1874
+ }
1875
+ log("");
1876
+ }
1877
+ const errorCount = allViolations.filter((v) => v.violation.severity === "error").length;
1878
+ const warnCount = allViolations.filter((v) => v.violation.severity === "warn").length;
1879
+ const fixableCount = allViolations.filter((v) => v.violation.fixable).length;
1880
+ const summary = [];
1881
+ if (errorCount > 0)
1882
+ summary.push(chalk6.red(`${errorCount} error(s)`));
1883
+ if (warnCount > 0)
1884
+ summary.push(chalk6.yellow(`${warnCount} warning(s)`));
1885
+ if (fixableCount > 0 && !shouldFix) {
1886
+ summary.push(chalk6.gray(`${fixableCount} fixable with --fix`));
1887
+ }
1888
+ log(summary.join(", "));
1889
+ if (errorCount > 0) {
1890
+ process.exit(1);
1891
+ }
1892
+ } catch (commandError) {
1893
+ error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1894
+ process.exit(1);
1895
+ }
1896
+ });
1897
+ }
1898
+
1899
+ // src/commands/report.ts
1900
+ import * as fs6 from "node:fs";
1901
+ import * as path7 from "node:path";
1902
+ import {
1903
+ DocCov as DocCov4,
1904
+ detectEntryPoint as detectEntryPoint4,
1905
+ detectMonorepo as detectMonorepo4,
1906
+ findPackageByName as findPackageByName4,
1907
+ NodeFileSystem as NodeFileSystem4
1908
+ } from "@doccov/sdk";
1909
+ import chalk7 from "chalk";
1320
1910
 
1321
1911
  // src/reports/markdown.ts
1322
1912
  function bar(pct, width = 10) {
@@ -1493,42 +2083,42 @@ function registerReportCommand(program) {
1493
2083
  try {
1494
2084
  let spec;
1495
2085
  if (options.spec) {
1496
- const specPath = path6.resolve(options.cwd, options.spec);
1497
- spec = JSON.parse(fs5.readFileSync(specPath, "utf-8"));
2086
+ const specPath = path7.resolve(options.cwd, options.spec);
2087
+ spec = JSON.parse(fs6.readFileSync(specPath, "utf-8"));
1498
2088
  } else {
1499
2089
  let targetDir = options.cwd;
1500
2090
  let entryFile = entry;
1501
- const fileSystem = new NodeFileSystem3(options.cwd);
2091
+ const fileSystem = new NodeFileSystem4(options.cwd);
1502
2092
  if (options.package) {
1503
- const mono = await detectMonorepo3(fileSystem);
2093
+ const mono = await detectMonorepo4(fileSystem);
1504
2094
  if (!mono.isMonorepo) {
1505
2095
  throw new Error(`Not a monorepo. Remove --package flag for single-package repos.`);
1506
2096
  }
1507
- const pkg = findPackageByName3(mono.packages, options.package);
2097
+ const pkg = findPackageByName4(mono.packages, options.package);
1508
2098
  if (!pkg) {
1509
2099
  const available = mono.packages.map((p) => p.name).join(", ");
1510
2100
  throw new Error(`Package "${options.package}" not found. Available: ${available}`);
1511
2101
  }
1512
- targetDir = path6.join(options.cwd, pkg.path);
2102
+ targetDir = path7.join(options.cwd, pkg.path);
1513
2103
  }
1514
2104
  if (!entryFile) {
1515
- const targetFs = new NodeFileSystem3(targetDir);
1516
- const detected = await detectEntryPoint3(targetFs);
1517
- entryFile = path6.join(targetDir, detected.path);
2105
+ const targetFs = new NodeFileSystem4(targetDir);
2106
+ const detected = await detectEntryPoint4(targetFs);
2107
+ entryFile = path7.join(targetDir, detected.path);
1518
2108
  } else {
1519
- entryFile = path6.resolve(targetDir, entryFile);
2109
+ entryFile = path7.resolve(targetDir, entryFile);
1520
2110
  }
1521
- process.stdout.write(chalk6.cyan(`> Analyzing...
2111
+ process.stdout.write(chalk7.cyan(`> Analyzing...
1522
2112
  `));
1523
2113
  try {
1524
2114
  const resolveExternalTypes = !options.skipResolve;
1525
- const doccov = new DocCov3({ resolveExternalTypes });
2115
+ const doccov = new DocCov4({ resolveExternalTypes });
1526
2116
  const result = await doccov.analyzeFileWithDiagnostics(entryFile);
1527
- process.stdout.write(chalk6.green(`✓ Analysis complete
2117
+ process.stdout.write(chalk7.green(`✓ Analysis complete
1528
2118
  `));
1529
2119
  spec = result.spec;
1530
2120
  } catch (analysisError) {
1531
- process.stdout.write(chalk6.red(`✗ Analysis failed
2121
+ process.stdout.write(chalk7.red(`✗ Analysis failed
1532
2122
  `));
1533
2123
  throw analysisError;
1534
2124
  }
@@ -1545,35 +2135,35 @@ function registerReportCommand(program) {
1545
2135
  output = renderMarkdown(stats, { limit });
1546
2136
  }
1547
2137
  if (options.out) {
1548
- const outPath = path6.resolve(options.cwd, options.out);
1549
- fs5.writeFileSync(outPath, output);
1550
- console.log(chalk6.green(`Report written to ${outPath}`));
2138
+ const outPath = path7.resolve(options.cwd, options.out);
2139
+ fs6.writeFileSync(outPath, output);
2140
+ console.log(chalk7.green(`Report written to ${outPath}`));
1551
2141
  } else {
1552
2142
  console.log(output);
1553
2143
  }
1554
2144
  } catch (err) {
1555
- console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
2145
+ console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
1556
2146
  process.exitCode = 1;
1557
2147
  }
1558
2148
  });
1559
2149
  }
1560
2150
 
1561
2151
  // src/commands/scan.ts
1562
- import * as fs7 from "node:fs";
2152
+ import * as fs8 from "node:fs";
1563
2153
  import * as os from "node:os";
1564
- import * as path8 from "node:path";
2154
+ import * as path9 from "node:path";
1565
2155
  import {
1566
- DocCov as DocCov4,
2156
+ DocCov as DocCov5,
1567
2157
  detectBuildInfo,
1568
- detectEntryPoint as detectEntryPoint4,
1569
- detectMonorepo as detectMonorepo4,
2158
+ detectEntryPoint as detectEntryPoint5,
2159
+ detectMonorepo as detectMonorepo5,
1570
2160
  detectPackageManager,
1571
- findPackageByName as findPackageByName4,
2161
+ findPackageByName as findPackageByName5,
1572
2162
  formatPackageList,
1573
2163
  getInstallCommand,
1574
- NodeFileSystem as NodeFileSystem4
2164
+ NodeFileSystem as NodeFileSystem5
1575
2165
  } from "@doccov/sdk";
1576
- import chalk7 from "chalk";
2166
+ import chalk8 from "chalk";
1577
2167
  import { simpleGit } from "simple-git";
1578
2168
 
1579
2169
  // src/utils/github-url.ts
@@ -1607,8 +2197,8 @@ function buildDisplayUrl(parsed) {
1607
2197
  }
1608
2198
 
1609
2199
  // src/utils/llm-build-plan.ts
1610
- import * as fs6 from "node:fs";
1611
- import * as path7 from "node:path";
2200
+ import * as fs7 from "node:fs";
2201
+ import * as path8 from "node:path";
1612
2202
  import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
1613
2203
  import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
1614
2204
  import { generateObject as generateObject3 } from "ai";
@@ -1644,10 +2234,10 @@ function getModel3() {
1644
2234
  async function gatherContextFiles(repoDir) {
1645
2235
  const sections = [];
1646
2236
  for (const fileName of CONTEXT_FILES) {
1647
- const filePath = path7.join(repoDir, fileName);
1648
- if (fs6.existsSync(filePath)) {
2237
+ const filePath = path8.join(repoDir, fileName);
2238
+ if (fs7.existsSync(filePath)) {
1649
2239
  try {
1650
- let content = fs6.readFileSync(filePath, "utf-8");
2240
+ let content = fs7.readFileSync(filePath, "utf-8");
1651
2241
  if (content.length > MAX_FILE_CHARS) {
1652
2242
  content = `${content.slice(0, MAX_FILE_CHARS)}
1653
2243
  ... (truncated)`;
@@ -1699,14 +2289,14 @@ async function generateBuildPlan(repoDir) {
1699
2289
  }
1700
2290
 
1701
2291
  // src/commands/scan.ts
1702
- var defaultDependencies5 = {
1703
- createDocCov: (options) => new DocCov4(options),
2292
+ var defaultDependencies6 = {
2293
+ createDocCov: (options) => new DocCov5(options),
1704
2294
  log: console.log,
1705
2295
  error: console.error
1706
2296
  };
1707
2297
  function registerScanCommand(program, dependencies = {}) {
1708
2298
  const { createDocCov, log, error } = {
1709
- ...defaultDependencies5,
2299
+ ...defaultDependencies6,
1710
2300
  ...dependencies
1711
2301
  };
1712
2302
  program.command("scan <url>").description("Analyze docs coverage for any public GitHub repository").option("--ref <branch>", "Branch or tag to analyze").option("--package <name>", "Target package in monorepo").option("--output <format>", "Output format: text or json", "text").option("--no-cleanup", "Keep cloned repo (for debugging)").option("--skip-install", "Skip dependency installation (faster, but may limit type resolution)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--save-spec <path>", "Save full OpenPkg spec to file").action(async (url, options) => {
@@ -1716,12 +2306,12 @@ function registerScanCommand(program, dependencies = {}) {
1716
2306
  const cloneUrl = buildCloneUrl(parsed);
1717
2307
  const displayUrl = buildDisplayUrl(parsed);
1718
2308
  log("");
1719
- log(chalk7.bold(`Scanning ${displayUrl}`));
1720
- log(chalk7.gray(`Branch/tag: ${parsed.ref}`));
2309
+ log(chalk8.bold(`Scanning ${displayUrl}`));
2310
+ log(chalk8.gray(`Branch/tag: ${parsed.ref}`));
1721
2311
  log("");
1722
- tempDir = path8.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
1723
- fs7.mkdirSync(tempDir, { recursive: true });
1724
- process.stdout.write(chalk7.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
2312
+ tempDir = path9.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
2313
+ fs8.mkdirSync(tempDir, { recursive: true });
2314
+ process.stdout.write(chalk8.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
1725
2315
  `));
1726
2316
  try {
1727
2317
  const git = simpleGit({
@@ -1743,10 +2333,10 @@ function registerScanCommand(program, dependencies = {}) {
1743
2333
  } finally {
1744
2334
  process.env = originalEnv;
1745
2335
  }
1746
- process.stdout.write(chalk7.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
2336
+ process.stdout.write(chalk8.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
1747
2337
  `));
1748
2338
  } catch (cloneError) {
1749
- process.stdout.write(chalk7.red(`✗ Failed to clone repository
2339
+ process.stdout.write(chalk8.red(`✗ Failed to clone repository
1750
2340
  `));
1751
2341
  const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
1752
2342
  if (message.includes("Authentication failed") || message.includes("could not read Username") || message.includes("terminal prompts disabled") || message.includes("Invalid username or password") || message.includes("Permission denied")) {
@@ -1762,11 +2352,11 @@ function registerScanCommand(program, dependencies = {}) {
1762
2352
  }
1763
2353
  throw new Error(`Clone failed: ${message}`);
1764
2354
  }
1765
- const fileSystem = new NodeFileSystem4(tempDir);
2355
+ const fileSystem = new NodeFileSystem5(tempDir);
1766
2356
  if (options.skipInstall) {
1767
- log(chalk7.gray("Skipping dependency installation (--skip-install)"));
2357
+ log(chalk8.gray("Skipping dependency installation (--skip-install)"));
1768
2358
  } else {
1769
- process.stdout.write(chalk7.cyan(`> Installing dependencies...
2359
+ process.stdout.write(chalk8.cyan(`> Installing dependencies...
1770
2360
  `));
1771
2361
  const installErrors = [];
1772
2362
  try {
@@ -1816,56 +2406,56 @@ function registerScanCommand(program, dependencies = {}) {
1816
2406
  }
1817
2407
  }
1818
2408
  if (installed) {
1819
- process.stdout.write(chalk7.green(`✓ Dependencies installed
2409
+ process.stdout.write(chalk8.green(`✓ Dependencies installed
1820
2410
  `));
1821
2411
  } else {
1822
- process.stdout.write(chalk7.yellow(`⚠ Could not install dependencies (analysis may be limited)
2412
+ process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies (analysis may be limited)
1823
2413
  `));
1824
2414
  for (const err of installErrors) {
1825
- log(chalk7.gray(` ${err}`));
2415
+ log(chalk8.gray(` ${err}`));
1826
2416
  }
1827
2417
  }
1828
2418
  } catch (outerError) {
1829
2419
  const msg = outerError instanceof Error ? outerError.message : String(outerError);
1830
- process.stdout.write(chalk7.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
2420
+ process.stdout.write(chalk8.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
1831
2421
  `));
1832
2422
  for (const err of installErrors) {
1833
- log(chalk7.gray(` ${err}`));
2423
+ log(chalk8.gray(` ${err}`));
1834
2424
  }
1835
2425
  }
1836
2426
  }
1837
2427
  let targetDir = tempDir;
1838
2428
  let packageName;
1839
- const mono = await detectMonorepo4(fileSystem);
2429
+ const mono = await detectMonorepo5(fileSystem);
1840
2430
  if (mono.isMonorepo) {
1841
2431
  if (!options.package) {
1842
2432
  error("");
1843
- error(chalk7.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
2433
+ error(chalk8.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
1844
2434
  error("");
1845
2435
  error(formatPackageList(mono.packages));
1846
2436
  error("");
1847
2437
  throw new Error("Monorepo requires --package flag");
1848
2438
  }
1849
- const pkg = findPackageByName4(mono.packages, options.package);
2439
+ const pkg = findPackageByName5(mono.packages, options.package);
1850
2440
  if (!pkg) {
1851
2441
  error("");
1852
- error(chalk7.red(`Package "${options.package}" not found. Available packages:`));
2442
+ error(chalk8.red(`Package "${options.package}" not found. Available packages:`));
1853
2443
  error("");
1854
2444
  error(formatPackageList(mono.packages));
1855
2445
  error("");
1856
2446
  throw new Error(`Package not found: ${options.package}`);
1857
2447
  }
1858
- targetDir = path8.join(tempDir, pkg.path);
2448
+ targetDir = path9.join(tempDir, pkg.path);
1859
2449
  packageName = pkg.name;
1860
- log(chalk7.gray(`Analyzing package: ${packageName}`));
2450
+ log(chalk8.gray(`Analyzing package: ${packageName}`));
1861
2451
  }
1862
- process.stdout.write(chalk7.cyan(`> Detecting entry point...
2452
+ process.stdout.write(chalk8.cyan(`> Detecting entry point...
1863
2453
  `));
1864
2454
  let entryPath;
1865
- const targetFs = mono.isMonorepo ? new NodeFileSystem4(targetDir) : fileSystem;
2455
+ const targetFs = mono.isMonorepo ? new NodeFileSystem5(targetDir) : fileSystem;
1866
2456
  let buildFailed = false;
1867
2457
  const runLlmFallback = async (reason) => {
1868
- process.stdout.write(chalk7.cyan(`> ${reason}, trying LLM fallback...
2458
+ process.stdout.write(chalk8.cyan(`> ${reason}, trying LLM fallback...
1869
2459
  `));
1870
2460
  const plan = await generateBuildPlan(targetDir);
1871
2461
  if (!plan) {
@@ -1874,88 +2464,88 @@ function registerScanCommand(program, dependencies = {}) {
1874
2464
  if (plan.buildCommands.length > 0) {
1875
2465
  const { execSync } = await import("node:child_process");
1876
2466
  for (const cmd of plan.buildCommands) {
1877
- log(chalk7.gray(` Running: ${cmd}`));
2467
+ log(chalk8.gray(` Running: ${cmd}`));
1878
2468
  try {
1879
2469
  execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
1880
2470
  } catch (buildError) {
1881
2471
  buildFailed = true;
1882
2472
  const msg = buildError instanceof Error ? buildError.message : String(buildError);
1883
2473
  if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
1884
- log(chalk7.yellow(` ⚠ Build requires Rust toolchain (not available)`));
2474
+ log(chalk8.yellow(` ⚠ Build requires Rust toolchain (not available)`));
1885
2475
  } else if (msg.includes("rimraf") || msg.includes("command not found")) {
1886
- log(chalk7.yellow(` ⚠ Build failed: missing dependencies`));
2476
+ log(chalk8.yellow(` ⚠ Build failed: missing dependencies`));
1887
2477
  } else {
1888
- log(chalk7.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
2478
+ log(chalk8.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
1889
2479
  }
1890
2480
  }
1891
2481
  }
1892
2482
  }
1893
2483
  if (plan.notes) {
1894
- log(chalk7.gray(` Note: ${plan.notes}`));
2484
+ log(chalk8.gray(` Note: ${plan.notes}`));
1895
2485
  }
1896
2486
  return plan.entryPoint;
1897
2487
  };
1898
2488
  try {
1899
- const entry = await detectEntryPoint4(targetFs);
2489
+ const entry = await detectEntryPoint5(targetFs);
1900
2490
  const buildInfo = await detectBuildInfo(targetFs);
1901
2491
  const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
1902
2492
  if (needsBuildStep) {
1903
- process.stdout.write(chalk7.cyan(`> Detected .d.ts entry with WASM indicators...
2493
+ process.stdout.write(chalk8.cyan(`> Detected .d.ts entry with WASM indicators...
1904
2494
  `));
1905
2495
  const llmEntry = await runLlmFallback("WASM project detected");
1906
2496
  if (llmEntry) {
1907
- entryPath = path8.join(targetDir, llmEntry);
2497
+ entryPath = path9.join(targetDir, llmEntry);
1908
2498
  if (buildFailed) {
1909
- process.stdout.write(chalk7.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
2499
+ process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
1910
2500
  `));
1911
- log(chalk7.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
2501
+ log(chalk8.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
1912
2502
  } else {
1913
- process.stdout.write(chalk7.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
2503
+ process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
1914
2504
  `));
1915
2505
  }
1916
2506
  } else {
1917
- entryPath = path8.join(targetDir, entry.path);
1918
- process.stdout.write(chalk7.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2507
+ entryPath = path9.join(targetDir, entry.path);
2508
+ process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
1919
2509
  `));
1920
- log(chalk7.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
2510
+ log(chalk8.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
1921
2511
  }
1922
2512
  } else {
1923
- entryPath = path8.join(targetDir, entry.path);
1924
- process.stdout.write(chalk7.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2513
+ entryPath = path9.join(targetDir, entry.path);
2514
+ process.stdout.write(chalk8.green(`✓ Entry point: ${entry.path} (from ${entry.source})
1925
2515
  `));
1926
2516
  }
1927
2517
  } catch (entryError) {
1928
2518
  const llmEntry = await runLlmFallback("Heuristics failed");
1929
2519
  if (llmEntry) {
1930
- entryPath = path8.join(targetDir, llmEntry);
1931
- process.stdout.write(chalk7.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
2520
+ entryPath = path9.join(targetDir, llmEntry);
2521
+ process.stdout.write(chalk8.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
1932
2522
  `));
1933
2523
  } else {
1934
- process.stdout.write(chalk7.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
2524
+ process.stdout.write(chalk8.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
1935
2525
  `));
1936
2526
  throw entryError;
1937
2527
  }
1938
2528
  }
1939
- process.stdout.write(chalk7.cyan(`> Analyzing documentation coverage...
2529
+ process.stdout.write(chalk8.cyan(`> Analyzing documentation coverage...
1940
2530
  `));
1941
2531
  let result;
1942
2532
  try {
1943
2533
  const resolveExternalTypes = !options.skipResolve;
1944
2534
  const doccov = createDocCov({ resolveExternalTypes });
1945
2535
  result = await doccov.analyzeFileWithDiagnostics(entryPath);
1946
- process.stdout.write(chalk7.green(`✓ Analysis complete
2536
+ process.stdout.write(chalk8.green(`✓ Analysis complete
1947
2537
  `));
1948
2538
  } catch (analysisError) {
1949
- process.stdout.write(chalk7.red(`✗ Analysis failed
2539
+ process.stdout.write(chalk8.red(`✗ Analysis failed
1950
2540
  `));
1951
2541
  throw analysisError;
1952
2542
  }
1953
2543
  const spec = result.spec;
1954
2544
  const coverageScore = spec.docs?.coverageScore ?? 0;
1955
2545
  if (options.saveSpec) {
1956
- const specPath = path8.resolve(process.cwd(), options.saveSpec);
1957
- fs7.writeFileSync(specPath, JSON.stringify(spec, null, 2));
1958
- log(chalk7.green(`✓ Saved spec to ${options.saveSpec}`));
2546
+ const specPath = path9.resolve(process.cwd(), options.saveSpec);
2547
+ fs8.writeFileSync(specPath, JSON.stringify(spec, null, 2));
2548
+ log(chalk8.green(`✓ Saved spec to ${options.saveSpec}`));
1959
2549
  }
1960
2550
  const undocumented = [];
1961
2551
  const driftIssues = [];
@@ -1992,7 +2582,7 @@ function registerScanCommand(program, dependencies = {}) {
1992
2582
  printTextResult(scanResult, log);
1993
2583
  }
1994
2584
  } catch (commandError) {
1995
- error(chalk7.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2585
+ error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1996
2586
  process.exitCode = 1;
1997
2587
  } finally {
1998
2588
  if (tempDir && options.cleanup !== false) {
@@ -2002,63 +2592,180 @@ function registerScanCommand(program, dependencies = {}) {
2002
2592
  stdio: "ignore"
2003
2593
  }).unref();
2004
2594
  } else if (tempDir) {
2005
- log(chalk7.gray(`Repo preserved at: ${tempDir}`));
2595
+ log(chalk8.gray(`Repo preserved at: ${tempDir}`));
2006
2596
  }
2007
2597
  }
2008
2598
  });
2009
2599
  }
2010
2600
  function printTextResult(result, log) {
2011
2601
  log("");
2012
- log(chalk7.bold("DocCov Scan Results"));
2602
+ log(chalk8.bold("DocCov Scan Results"));
2013
2603
  log("─".repeat(40));
2014
2604
  const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
2015
- log(`Repository: ${chalk7.cyan(repoName)}`);
2016
- log(`Branch: ${chalk7.gray(result.ref)}`);
2605
+ log(`Repository: ${chalk8.cyan(repoName)}`);
2606
+ log(`Branch: ${chalk8.gray(result.ref)}`);
2017
2607
  log("");
2018
- const coverageColor = result.coverage >= 80 ? chalk7.green : result.coverage >= 50 ? chalk7.yellow : chalk7.red;
2019
- log(chalk7.bold("Coverage"));
2608
+ const coverageColor = result.coverage >= 80 ? chalk8.green : result.coverage >= 50 ? chalk8.yellow : chalk8.red;
2609
+ log(chalk8.bold("Coverage"));
2020
2610
  log(` ${coverageColor(`${result.coverage}%`)}`);
2021
2611
  log("");
2022
- log(chalk7.bold("Stats"));
2612
+ log(chalk8.bold("Stats"));
2023
2613
  log(` ${result.exportCount} exports`);
2024
2614
  log(` ${result.typeCount} types`);
2025
2615
  log(` ${result.undocumented.length} undocumented`);
2026
2616
  log(` ${result.driftCount} drift issues`);
2027
2617
  if (result.undocumented.length > 0) {
2028
2618
  log("");
2029
- log(chalk7.bold("Undocumented Exports"));
2619
+ log(chalk8.bold("Undocumented Exports"));
2030
2620
  for (const name of result.undocumented.slice(0, 10)) {
2031
- log(chalk7.yellow(` ! ${name}`));
2621
+ log(chalk8.yellow(` ! ${name}`));
2032
2622
  }
2033
2623
  if (result.undocumented.length > 10) {
2034
- log(chalk7.gray(` ... and ${result.undocumented.length - 10} more`));
2624
+ log(chalk8.gray(` ... and ${result.undocumented.length - 10} more`));
2035
2625
  }
2036
2626
  }
2037
2627
  if (result.drift.length > 0) {
2038
2628
  log("");
2039
- log(chalk7.bold("Drift Issues"));
2629
+ log(chalk8.bold("Drift Issues"));
2040
2630
  for (const d of result.drift.slice(0, 5)) {
2041
- log(chalk7.red(` • ${d.export}: ${d.issue}`));
2631
+ log(chalk8.red(` • ${d.export}: ${d.issue}`));
2042
2632
  }
2043
2633
  if (result.drift.length > 5) {
2044
- log(chalk7.gray(` ... and ${result.drift.length - 5} more`));
2634
+ log(chalk8.gray(` ... and ${result.drift.length - 5} more`));
2045
2635
  }
2046
2636
  }
2047
2637
  log("");
2048
2638
  }
2049
2639
 
2640
+ // src/commands/typecheck.ts
2641
+ import * as fs9 from "node:fs";
2642
+ import * as path10 from "node:path";
2643
+ import {
2644
+ detectEntryPoint as detectEntryPoint6,
2645
+ detectMonorepo as detectMonorepo6,
2646
+ DocCov as DocCov6,
2647
+ findPackageByName as findPackageByName6,
2648
+ NodeFileSystem as NodeFileSystem6,
2649
+ typecheckExamples as typecheckExamples2
2650
+ } from "@doccov/sdk";
2651
+ import chalk9 from "chalk";
2652
+ var defaultDependencies7 = {
2653
+ createDocCov: (options) => new DocCov6(options),
2654
+ log: console.log,
2655
+ error: console.error
2656
+ };
2657
+ function registerTypecheckCommand(program, dependencies = {}) {
2658
+ const { createDocCov, log, error } = {
2659
+ ...defaultDependencies7,
2660
+ ...dependencies
2661
+ };
2662
+ program.command("typecheck [entry]").description("Type-check @example blocks without executing them").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution").action(async (entry, options) => {
2663
+ try {
2664
+ let targetDir = options.cwd;
2665
+ let entryFile = entry;
2666
+ const fileSystem = new NodeFileSystem6(options.cwd);
2667
+ if (options.package) {
2668
+ const mono = await detectMonorepo6(fileSystem);
2669
+ if (!mono.isMonorepo) {
2670
+ throw new Error("Not a monorepo. Remove --package flag.");
2671
+ }
2672
+ const pkg = findPackageByName6(mono.packages, options.package);
2673
+ if (!pkg) {
2674
+ const available = mono.packages.map((p) => p.name).join(", ");
2675
+ throw new Error(`Package "${options.package}" not found. Available: ${available}`);
2676
+ }
2677
+ targetDir = path10.join(options.cwd, pkg.path);
2678
+ log(chalk9.gray(`Found package at ${pkg.path}`));
2679
+ }
2680
+ if (!entryFile) {
2681
+ const targetFs = new NodeFileSystem6(targetDir);
2682
+ const detected = await detectEntryPoint6(targetFs);
2683
+ entryFile = path10.join(targetDir, detected.path);
2684
+ log(chalk9.gray(`Auto-detected entry point: ${detected.path}`));
2685
+ } else {
2686
+ entryFile = path10.resolve(targetDir, entryFile);
2687
+ if (fs9.existsSync(entryFile) && fs9.statSync(entryFile).isDirectory()) {
2688
+ targetDir = entryFile;
2689
+ const dirFs = new NodeFileSystem6(entryFile);
2690
+ const detected = await detectEntryPoint6(dirFs);
2691
+ entryFile = path10.join(entryFile, detected.path);
2692
+ log(chalk9.gray(`Auto-detected entry point: ${detected.path}`));
2693
+ }
2694
+ }
2695
+ const resolveExternalTypes = !options.skipResolve;
2696
+ process.stdout.write(chalk9.cyan(`> Analyzing documentation...
2697
+ `));
2698
+ const doccov = createDocCov({ resolveExternalTypes });
2699
+ const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
2700
+ if (!specResult) {
2701
+ throw new Error("Failed to analyze documentation.");
2702
+ }
2703
+ const allExamples = [];
2704
+ for (const exp of specResult.spec.exports ?? []) {
2705
+ if (exp.examples && exp.examples.length > 0) {
2706
+ allExamples.push({ exportName: exp.name, examples: exp.examples });
2707
+ }
2708
+ }
2709
+ if (allExamples.length === 0) {
2710
+ log(chalk9.gray("No @example blocks found"));
2711
+ return;
2712
+ }
2713
+ const totalExamples = allExamples.reduce((sum, e) => sum + e.examples.length, 0);
2714
+ process.stdout.write(chalk9.cyan(`> Type-checking ${totalExamples} example(s)...
2715
+ `));
2716
+ const allErrors = [];
2717
+ let passed = 0;
2718
+ let failed = 0;
2719
+ for (const { exportName, examples } of allExamples) {
2720
+ const result = typecheckExamples2(examples, targetDir);
2721
+ for (const err of result.errors) {
2722
+ allErrors.push({ exportName, error: err });
2723
+ }
2724
+ passed += result.passed;
2725
+ failed += result.failed;
2726
+ }
2727
+ if (allErrors.length === 0) {
2728
+ log(chalk9.green(`✓ All ${totalExamples} example(s) passed type checking`));
2729
+ return;
2730
+ }
2731
+ log("");
2732
+ const byExport = new Map;
2733
+ for (const { exportName, error: err } of allErrors) {
2734
+ const existing = byExport.get(exportName) ?? [];
2735
+ existing.push(err);
2736
+ byExport.set(exportName, existing);
2737
+ }
2738
+ for (const [exportName, errors] of byExport) {
2739
+ log(chalk9.red(`✗ ${exportName}`));
2740
+ for (const err of errors) {
2741
+ log(chalk9.gray(` @example block ${err.exampleIndex + 1}, line ${err.line}:`));
2742
+ log(chalk9.red(` ${err.message}`));
2743
+ }
2744
+ log("");
2745
+ }
2746
+ log(chalk9.red(`${failed} example(s) failed`) + chalk9.gray(`, ${passed} passed`));
2747
+ process.exit(1);
2748
+ } catch (commandError) {
2749
+ error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2750
+ process.exit(1);
2751
+ }
2752
+ });
2753
+ }
2754
+
2050
2755
  // src/cli.ts
2051
2756
  var __filename2 = fileURLToPath(import.meta.url);
2052
- var __dirname2 = path9.dirname(__filename2);
2053
- var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
2757
+ var __dirname2 = path11.dirname(__filename2);
2758
+ var packageJson = JSON.parse(readFileSync5(path11.join(__dirname2, "../package.json"), "utf-8"));
2054
2759
  var program = new Command;
2055
2760
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2056
2761
  registerGenerateCommand(program);
2057
2762
  registerCheckCommand(program);
2058
2763
  registerDiffCommand(program);
2059
2764
  registerInitCommand(program);
2765
+ registerLintCommand(program);
2060
2766
  registerReportCommand(program);
2061
2767
  registerScanCommand(program);
2768
+ registerTypecheckCommand(program);
2062
2769
  program.command("*", { hidden: true }).action(() => {
2063
2770
  program.outputHelp();
2064
2771
  });