@doccov/cli 0.5.5 → 0.5.7

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.
Files changed (2) hide show
  1. package/dist/cli.js +419 -90
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -730,12 +730,37 @@ var defaultDependencies2 = {
730
730
  log: console.log,
731
731
  error: console.error
732
732
  };
733
+ var VALID_STRICT_OPTIONS = [
734
+ "regression",
735
+ "drift",
736
+ "docs-impact",
737
+ "breaking",
738
+ "undocumented",
739
+ "all"
740
+ ];
741
+ function parseStrictOptions(value) {
742
+ if (!value)
743
+ return new Set;
744
+ const options = value.split(",").map((s) => s.trim().toLowerCase());
745
+ const result = new Set;
746
+ for (const opt of options) {
747
+ if (opt === "all") {
748
+ for (const o of VALID_STRICT_OPTIONS) {
749
+ if (o !== "all")
750
+ result.add(o);
751
+ }
752
+ } else if (VALID_STRICT_OPTIONS.includes(opt)) {
753
+ result.add(opt);
754
+ }
755
+ }
756
+ return result;
757
+ }
733
758
  function registerDiffCommand(program, dependencies = {}) {
734
759
  const { readFileSync: readFileSync2, log, error } = {
735
760
  ...defaultDependencies2,
736
761
  ...dependencies
737
762
  };
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) => {
763
+ 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
764
  try {
740
765
  const baseSpec = loadSpec(base, readFileSync2);
741
766
  const headSpec = loadSpec(head, readFileSync2);
@@ -752,52 +777,81 @@ function registerDiffCommand(program, dependencies = {}) {
752
777
  markdownFiles = await loadMarkdownFiles(docsPatterns);
753
778
  }
754
779
  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(`
780
+ const format = options.format ?? options.output ?? "text";
781
+ const strictOptions = parseStrictOptions(options.strict);
782
+ if (options.failOnRegression)
783
+ strictOptions.add("regression");
784
+ if (options.failOnDrift)
785
+ strictOptions.add("drift");
786
+ if (options.failOnDocsImpact)
787
+ strictOptions.add("docs-impact");
788
+ switch (format) {
789
+ case "json":
790
+ log(JSON.stringify(diff, null, 2));
791
+ break;
792
+ case "github":
793
+ printGitHubAnnotations(diff, log);
794
+ break;
795
+ case "report":
796
+ log(generateHTMLReport(diff));
797
+ break;
798
+ case "text":
799
+ default:
800
+ printTextDiff(diff, log, error);
801
+ if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
802
+ if (!isAIDocsAnalysisAvailable()) {
803
+ log(chalk2.yellow(`
763
804
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
764
- } else {
765
- log(chalk2.gray(`
805
+ } else {
806
+ log(chalk2.gray(`
766
807
  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}`));
808
+ const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
809
+ file: f.file,
810
+ exportName: r.exportName,
811
+ changeType: r.changeType,
812
+ context: r.context
813
+ })));
814
+ const summary = await generateImpactSummary(impacts);
815
+ if (summary) {
816
+ log("");
817
+ log(chalk2.bold("AI Summary"));
818
+ log(chalk2.cyan(` ${summary}`));
819
+ }
778
820
  }
779
821
  }
780
- }
822
+ break;
781
823
  }
782
- if (options.failOnRegression && diff.coverageDelta < 0) {
824
+ if (strictOptions.has("regression") && diff.coverageDelta < 0) {
783
825
  error(chalk2.red(`
784
826
  Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
785
827
  process.exitCode = 1;
786
828
  return;
787
829
  }
788
- if (options.failOnDrift && diff.driftIntroduced > 0) {
830
+ if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
789
831
  error(chalk2.red(`
790
832
  ${diff.driftIntroduced} new drift issue(s) introduced`));
791
833
  process.exitCode = 1;
792
834
  return;
793
835
  }
794
- if (options.failOnDocsImpact && hasDocsImpact(diff)) {
836
+ if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
795
837
  const summary = getDocsImpactSummary(diff);
796
838
  error(chalk2.red(`
797
839
  ${summary.totalIssues} docs issue(s) require attention`));
798
840
  process.exitCode = 1;
799
841
  return;
800
842
  }
843
+ if (strictOptions.has("breaking") && diff.breaking.length > 0) {
844
+ error(chalk2.red(`
845
+ ${diff.breaking.length} breaking change(s) detected`));
846
+ process.exitCode = 1;
847
+ return;
848
+ }
849
+ if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
850
+ error(chalk2.red(`
851
+ ${diff.newUndocumented.length} new undocumented export(s)`));
852
+ process.exitCode = 1;
853
+ return;
854
+ }
801
855
  } catch (commandError) {
802
856
  error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
803
857
  process.exitCode = 1;
@@ -836,101 +890,376 @@ function printTextDiff(diff, log, _error) {
836
890
  log("");
837
891
  log(chalk2.bold("DocCov Diff Report"));
838
892
  log("─".repeat(40));
893
+ printCoverage(diff, log);
894
+ printAPIChanges(diff, log);
895
+ if (diff.docsImpact) {
896
+ printDocsRequiringUpdates(diff, log);
897
+ }
898
+ log("");
899
+ }
900
+ function printCoverage(diff, log) {
839
901
  const coverageColor = diff.coverageDelta > 0 ? chalk2.green : diff.coverageDelta < 0 ? chalk2.red : chalk2.gray;
840
902
  const coverageSymbol = diff.coverageDelta > 0 ? "↑" : diff.coverageDelta < 0 ? "↓" : "→";
841
903
  const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
842
904
  log("");
843
905
  log(chalk2.bold("Coverage"));
844
906
  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
- }
907
+ }
908
+ function printAPIChanges(diff, log) {
909
+ const hasChanges = diff.breaking.length > 0 || diff.nonBreaking.length > 0 || diff.memberChanges && diff.memberChanges.length > 0;
910
+ if (!hasChanges)
911
+ return;
912
+ log("");
913
+ log(chalk2.bold("API Changes"));
914
+ const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
915
+ const classesWithMembers = new Set(membersByClass.keys());
916
+ for (const [className, changes] of membersByClass) {
917
+ const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
918
+ const isHighSeverity = categorized?.severity === "high";
919
+ const label = isHighSeverity ? chalk2.red(" [BREAKING]") : chalk2.yellow(" [CHANGED]");
920
+ log(chalk2.cyan(` ${className}`) + label);
921
+ const removed = changes.filter((c) => c.changeType === "removed");
922
+ for (const mc of removed) {
923
+ const suggestion = mc.suggestion ? chalk2.gray(` → ${mc.suggestion}`) : "";
924
+ log(chalk2.red(` ✖ ${mc.memberName}()`) + suggestion);
856
925
  }
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`));
926
+ const changed = changes.filter((c) => c.changeType === "signature-changed");
927
+ for (const mc of changed) {
928
+ log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
929
+ if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
930
+ log(chalk2.gray(` was: ${mc.oldSignature}`));
931
+ log(chalk2.gray(` now: ${mc.newSignature}`));
864
932
  }
865
933
  }
934
+ const added = changes.filter((c) => c.changeType === "added");
935
+ if (added.length > 0) {
936
+ const addedNames = added.map((a) => a.memberName + "()").join(", ");
937
+ log(chalk2.green(` + ${addedNames}`));
938
+ }
866
939
  }
867
- log("");
868
- log(chalk2.bold("Docs Health"));
869
- if (diff.newUndocumented.length > 0) {
870
- log(chalk2.yellow(` ${diff.newUndocumented.length} new undocumented export(s)`));
871
- for (const id of diff.newUndocumented.slice(0, 5)) {
872
- log(chalk2.yellow(` ! ${id}`));
940
+ const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
941
+ const typeChanges = nonClassBreaking.filter((c) => c.kind === "interface" || c.kind === "type" || c.kind === "enum");
942
+ const functionChanges = nonClassBreaking.filter((c) => c.kind === "function");
943
+ const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
944
+ if (functionChanges.length > 0) {
945
+ log("");
946
+ log(chalk2.red(` Function Changes (${functionChanges.length}):`));
947
+ for (const fc of functionChanges.slice(0, 3)) {
948
+ const reason = fc.reason === "removed" ? "removed" : "signature changed";
949
+ log(chalk2.red(` ✖ ${fc.name} (${reason})`));
873
950
  }
874
- if (diff.newUndocumented.length > 5) {
875
- log(chalk2.gray(` ... and ${diff.newUndocumented.length - 5} more`));
951
+ if (functionChanges.length > 3) {
952
+ log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
876
953
  }
877
954
  }
878
- if (diff.improvedExports.length > 0) {
879
- log(chalk2.green(` ${diff.improvedExports.length} export(s) improved docs`));
955
+ if (typeChanges.length > 0) {
956
+ log("");
957
+ log(chalk2.yellow(` Type/Interface Changes (${typeChanges.length}):`));
958
+ const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
959
+ log(chalk2.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
880
960
  }
881
- if (diff.regressedExports.length > 0) {
882
- log(chalk2.red(` ${diff.regressedExports.length} export(s) regressed docs`));
883
- for (const id of diff.regressedExports.slice(0, 5)) {
884
- log(chalk2.red(` ↓ ${id}`));
885
- }
961
+ if (otherChanges.length > 0) {
962
+ log("");
963
+ log(chalk2.gray(` Other Changes (${otherChanges.length}):`));
964
+ const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
965
+ log(chalk2.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
966
+ }
967
+ if (diff.nonBreaking.length > 0) {
968
+ const undocCount = diff.newUndocumented.length;
969
+ const undocSuffix = undocCount > 0 ? chalk2.yellow(` (${undocCount} undocumented)`) : "";
970
+ log("");
971
+ log(chalk2.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
972
+ const exportNames = diff.nonBreaking.slice(0, 3);
973
+ log(chalk2.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
886
974
  }
887
975
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
888
976
  log("");
889
- log(chalk2.bold("Drift"));
977
+ const parts = [];
890
978
  if (diff.driftIntroduced > 0) {
891
- log(chalk2.red(` +${diff.driftIntroduced} new drift issue(s)`));
979
+ parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
892
980
  }
893
981
  if (diff.driftResolved > 0) {
894
- log(chalk2.green(` -${diff.driftResolved} drift issue(s) resolved`));
982
+ parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
895
983
  }
984
+ log(` Drift: ${parts.join(", ")}`);
896
985
  }
897
- if (diff.docsImpact) {
986
+ }
987
+ function printDocsRequiringUpdates(diff, log) {
988
+ if (!diff.docsImpact)
989
+ return;
990
+ const { impactedFiles, missingDocs, stats } = diff.docsImpact;
991
+ log("");
992
+ log(chalk2.bold("Docs Requiring Updates"));
993
+ log(chalk2.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
994
+ if (impactedFiles.length === 0 && missingDocs.length === 0) {
995
+ log(chalk2.green(" ✓ No updates needed"));
996
+ return;
997
+ }
998
+ const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
999
+ const actionableFiles = [];
1000
+ const instantiationOnlyFiles = [];
1001
+ for (const file of sortedFiles) {
1002
+ const hasActionableRefs = file.references.some((r) => r.memberName && !r.isInstantiation || !r.memberName && !r.isInstantiation);
1003
+ if (hasActionableRefs) {
1004
+ actionableFiles.push(file);
1005
+ } else {
1006
+ instantiationOnlyFiles.push(file);
1007
+ }
1008
+ }
1009
+ for (const file of actionableFiles.slice(0, 6)) {
1010
+ const filename = path3.basename(file.file);
1011
+ const issueCount = file.references.length;
898
1012
  log("");
899
- log(chalk2.bold("Docs Impact"));
900
- const { impactedFiles, missingDocs, stats } = diff.docsImpact;
901
- log(chalk2.gray(` Scanned ${stats.filesScanned} file(s), ${stats.codeBlocksFound} code block(s)`));
902
- if (impactedFiles.length > 0) {
903
- log("");
904
- log(chalk2.yellow(` ${impactedFiles.length} file(s) need updates:`));
905
- for (const file of impactedFiles.slice(0, 10)) {
906
- log(chalk2.yellow(` \uD83D\uDCC4 ${file.file}`));
907
- for (const ref of file.references.slice(0, 3)) {
908
- const changeLabel = ref.changeType === "signature-changed" ? "signature changed" : ref.changeType === "removed" ? "removed" : "deprecated";
909
- log(chalk2.gray(` Line ${ref.line}: ${ref.exportName} (${changeLabel})`));
910
- }
911
- if (file.references.length > 3) {
912
- log(chalk2.gray(` ... and ${file.references.length - 3} more reference(s)`));
913
- }
1013
+ log(chalk2.yellow(` ${filename}`) + chalk2.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
1014
+ const actionableRefs = file.references.filter((r) => !r.isInstantiation);
1015
+ for (const ref of actionableRefs.slice(0, 4)) {
1016
+ if (ref.memberName) {
1017
+ const action = ref.changeType === "method-removed" ? "→" : "~";
1018
+ const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
1019
+ log(chalk2.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
1020
+ } else {
1021
+ const action = ref.changeType === "removed" ? "→" : "~";
1022
+ const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
1023
+ log(chalk2.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
914
1024
  }
915
- if (impactedFiles.length > 10) {
916
- log(chalk2.gray(` ... and ${impactedFiles.length - 10} more file(s)`));
1025
+ }
1026
+ if (actionableRefs.length > 4) {
1027
+ log(chalk2.gray(` ... and ${actionableRefs.length - 4} more`));
1028
+ }
1029
+ }
1030
+ if (actionableFiles.length > 6) {
1031
+ log(chalk2.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
1032
+ }
1033
+ if (instantiationOnlyFiles.length > 0) {
1034
+ log("");
1035
+ const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path3.basename(f.file));
1036
+ const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
1037
+ log(chalk2.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
1038
+ log(chalk2.gray(` ${fileNames.join(", ")}${suffix}`));
1039
+ }
1040
+ const { allUndocumented } = diff.docsImpact;
1041
+ if (missingDocs.length > 0) {
1042
+ log("");
1043
+ log(chalk2.yellow(` New exports missing docs (${missingDocs.length}):`));
1044
+ const names = missingDocs.slice(0, 4);
1045
+ log(chalk2.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
1046
+ }
1047
+ if (allUndocumented && allUndocumented.length > 0) {
1048
+ const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
1049
+ log("");
1050
+ log(chalk2.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
1051
+ if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
1052
+ log(chalk2.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
1053
+ }
1054
+ }
1055
+ }
1056
+ function groupMemberChangesByClass(memberChanges) {
1057
+ const byClass = new Map;
1058
+ for (const mc of memberChanges) {
1059
+ const list = byClass.get(mc.className) ?? [];
1060
+ list.push(mc);
1061
+ byClass.set(mc.className, list);
1062
+ }
1063
+ return byClass;
1064
+ }
1065
+ function printGitHubAnnotations(diff, log) {
1066
+ if (diff.coverageDelta !== 0) {
1067
+ const level = diff.coverageDelta < 0 ? "warning" : "notice";
1068
+ const sign = diff.coverageDelta > 0 ? "+" : "";
1069
+ log(`::${level} title=Coverage Change::Coverage ${diff.oldCoverage}% → ${diff.newCoverage}% (${sign}${diff.coverageDelta}%)`);
1070
+ }
1071
+ for (const breaking of diff.categorizedBreaking ?? []) {
1072
+ const level = breaking.severity === "high" ? "error" : "warning";
1073
+ log(`::${level} title=Breaking Change::${breaking.name} - ${breaking.reason}`);
1074
+ }
1075
+ for (const mc of diff.memberChanges ?? []) {
1076
+ if (mc.changeType === "removed") {
1077
+ const suggestion = mc.suggestion ? ` ${mc.suggestion}` : "";
1078
+ log(`::warning title=Method Removed::${mc.className}.${mc.memberName}() removed.${suggestion}`);
1079
+ } else if (mc.changeType === "signature-changed") {
1080
+ log(`::warning title=Signature Changed::${mc.className}.${mc.memberName}() signature changed`);
1081
+ }
1082
+ }
1083
+ if (diff.docsImpact) {
1084
+ for (const file of diff.docsImpact.impactedFiles) {
1085
+ for (const ref of file.references) {
1086
+ const level = ref.changeType === "removed" || ref.changeType === "method-removed" ? "error" : "warning";
1087
+ const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
1088
+ const changeDesc = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : ref.changeType === "signature-changed" || ref.changeType === "method-changed" ? "signature changed" : "changed";
1089
+ const suggestion = ref.replacementSuggestion ? ` → ${ref.replacementSuggestion}` : "";
1090
+ log(`::${level} file=${file.file},line=${ref.line},title=API Change::${name} ${changeDesc}${suggestion}`);
917
1091
  }
918
1092
  }
919
- if (missingDocs.length > 0) {
920
- log("");
921
- log(chalk2.yellow(` ${missingDocs.length} new export(s) missing docs:`));
922
- for (const name of missingDocs.slice(0, 5)) {
923
- log(chalk2.yellow(` • ${name}`));
1093
+ for (const name of diff.docsImpact.missingDocs) {
1094
+ log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
1095
+ }
1096
+ const { stats, allUndocumented } = diff.docsImpact;
1097
+ if (allUndocumented && allUndocumented.length > 0) {
1098
+ const docPercent = Math.round((1 - allUndocumented.length / stats.totalExports) * 100);
1099
+ log(`::notice title=Documentation Coverage::${stats.documentedExports}/${stats.totalExports} exports documented (${docPercent}%)`);
1100
+ }
1101
+ }
1102
+ if (!diff.docsImpact && diff.newUndocumented.length > 0) {
1103
+ for (const name of diff.newUndocumented) {
1104
+ log(`::notice title=Missing Documentation::New export ${name} needs documentation`);
1105
+ }
1106
+ }
1107
+ if (diff.driftIntroduced > 0) {
1108
+ log(`::warning title=Drift Detected::${diff.driftIntroduced} new drift issue(s) introduced`);
1109
+ }
1110
+ }
1111
+ function generateHTMLReport(diff) {
1112
+ const coverageClass = diff.coverageDelta > 0 ? "positive" : diff.coverageDelta < 0 ? "negative" : "neutral";
1113
+ const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1114
+ let html = `<!DOCTYPE html>
1115
+ <html lang="en">
1116
+ <head>
1117
+ <meta charset="UTF-8">
1118
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1119
+ <title>DocCov Diff Report</title>
1120
+ <style>
1121
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1122
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; line-height: 1.5; }
1123
+ .container { max-width: 900px; margin: 0 auto; }
1124
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #f0f6fc; }
1125
+ h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
1126
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
1127
+ .metric { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
1128
+ .metric-label { color: #8b949e; }
1129
+ .metric-value { font-weight: 600; }
1130
+ .positive { color: #3fb950; }
1131
+ .negative { color: #f85149; }
1132
+ .neutral { color: #8b949e; }
1133
+ .warning { color: #d29922; }
1134
+ .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
1135
+ .badge-breaking { background: #f8514933; color: #f85149; }
1136
+ .badge-changed { background: #d2992233; color: #d29922; }
1137
+ .badge-added { background: #3fb95033; color: #3fb950; }
1138
+ .file-item { padding: 0.5rem; margin: 0.25rem 0; background: #0d1117; border-radius: 4px; }
1139
+ .file-name { font-family: monospace; font-size: 0.9rem; }
1140
+ .ref-list { margin-top: 0.5rem; padding-left: 1rem; font-size: 0.85rem; color: #8b949e; }
1141
+ .ref-item { margin: 0.25rem 0; }
1142
+ ul { list-style: none; }
1143
+ li { padding: 0.25rem 0; }
1144
+ code { font-family: monospace; background: #0d1117; padding: 0.125rem 0.375rem; border-radius: 3px; font-size: 0.9rem; }
1145
+ </style>
1146
+ </head>
1147
+ <body>
1148
+ <div class="container">
1149
+ <h1>\uD83D\uDCCA DocCov Diff Report</h1>
1150
+
1151
+ <div class="card">
1152
+ <div class="metric">
1153
+ <span class="metric-label">Coverage</span>
1154
+ <span class="metric-value ${coverageClass}">${diff.oldCoverage}% → ${diff.newCoverage}% (${coverageSign}${diff.coverageDelta}%)</span>
1155
+ </div>
1156
+ <div class="metric">
1157
+ <span class="metric-label">Breaking Changes</span>
1158
+ <span class="metric-value ${diff.breaking.length > 0 ? "negative" : "neutral"}">${diff.breaking.length}</span>
1159
+ </div>
1160
+ <div class="metric">
1161
+ <span class="metric-label">New Exports</span>
1162
+ <span class="metric-value positive">${diff.nonBreaking.length}</span>
1163
+ </div>
1164
+ <div class="metric">
1165
+ <span class="metric-label">Undocumented</span>
1166
+ <span class="metric-value ${diff.newUndocumented.length > 0 ? "warning" : "neutral"}">${diff.newUndocumented.length}</span>
1167
+ </div>
1168
+ </div>`;
1169
+ if (diff.breaking.length > 0) {
1170
+ html += `
1171
+ <h2>Breaking Changes</h2>
1172
+ <div class="card">
1173
+ <ul>`;
1174
+ for (const item of diff.categorizedBreaking ?? []) {
1175
+ const badgeClass = item.severity === "high" ? "badge-breaking" : "badge-changed";
1176
+ html += `
1177
+ <li><code>${item.name}</code> <span class="badge ${badgeClass}">${item.reason}</span></li>`;
1178
+ }
1179
+ html += `
1180
+ </ul>
1181
+ </div>`;
1182
+ }
1183
+ if (diff.memberChanges && diff.memberChanges.length > 0) {
1184
+ html += `
1185
+ <h2>Member Changes</h2>
1186
+ <div class="card">
1187
+ <ul>`;
1188
+ for (const mc of diff.memberChanges) {
1189
+ const badgeClass = mc.changeType === "removed" ? "badge-breaking" : mc.changeType === "added" ? "badge-added" : "badge-changed";
1190
+ const suggestion = mc.suggestion ? ` → ${mc.suggestion}` : "";
1191
+ html += `
1192
+ <li><code>${mc.className}.${mc.memberName}()</code> <span class="badge ${badgeClass}">${mc.changeType}</span>${suggestion}</li>`;
1193
+ }
1194
+ html += `
1195
+ </ul>
1196
+ </div>`;
1197
+ }
1198
+ if (diff.docsImpact && diff.docsImpact.impactedFiles.length > 0) {
1199
+ html += `
1200
+ <h2>Documentation Impact</h2>
1201
+ <div class="card">`;
1202
+ for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
1203
+ const filename = path3.basename(file.file);
1204
+ html += `
1205
+ <div class="file-item">
1206
+ <div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
1207
+ <div class="ref-list">`;
1208
+ for (const ref of file.references.slice(0, 5)) {
1209
+ const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
1210
+ const change = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : "signature changed";
1211
+ html += `
1212
+ <div class="ref-item">Line ${ref.line}: <code>${name}</code> ${change}</div>`;
1213
+ }
1214
+ if (file.references.length > 5) {
1215
+ html += `
1216
+ <div class="ref-item neutral">... and ${file.references.length - 5} more</div>`;
924
1217
  }
925
- if (missingDocs.length > 5) {
926
- log(chalk2.gray(` ... and ${missingDocs.length - 5} more`));
1218
+ html += `
1219
+ </div>
1220
+ </div>`;
1221
+ }
1222
+ html += `
1223
+ </div>`;
1224
+ }
1225
+ const hasNewUndocumented = diff.newUndocumented.length > 0;
1226
+ const hasAllUndocumented = diff.docsImpact?.allUndocumented && diff.docsImpact.allUndocumented.length > 0;
1227
+ if (hasNewUndocumented || hasAllUndocumented) {
1228
+ html += `
1229
+ <h2>Missing Documentation</h2>
1230
+ <div class="card">`;
1231
+ if (hasNewUndocumented) {
1232
+ html += `
1233
+ <p class="warning">New exports missing docs (${diff.newUndocumented.length}):</p>
1234
+ <ul>`;
1235
+ for (const name of diff.newUndocumented.slice(0, 10)) {
1236
+ html += `
1237
+ <li><code>${name}</code></li>`;
1238
+ }
1239
+ if (diff.newUndocumented.length > 10) {
1240
+ html += `
1241
+ <li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
927
1242
  }
1243
+ html += `
1244
+ </ul>`;
928
1245
  }
929
- if (impactedFiles.length === 0 && missingDocs.length === 0) {
930
- log(chalk2.green(" ✓ No docs impact detected"));
1246
+ if (diff.docsImpact?.stats) {
1247
+ const { stats, allUndocumented } = diff.docsImpact;
1248
+ const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
1249
+ html += `
1250
+ <div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
1251
+ <span class="metric-label">Total Documentation Coverage</span>
1252
+ <span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
1253
+ </div>`;
931
1254
  }
1255
+ html += `
1256
+ </div>`;
932
1257
  }
933
- log("");
1258
+ html += `
1259
+ </div>
1260
+ </body>
1261
+ </html>`;
1262
+ return html;
934
1263
  }
935
1264
 
936
1265
  // src/commands/generate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",
@@ -49,8 +49,8 @@
49
49
  "@ai-sdk/anthropic": "^1.0.0",
50
50
  "@ai-sdk/openai": "^1.0.0",
51
51
  "@inquirer/prompts": "^7.8.0",
52
- "@doccov/sdk": "^0.3.7",
53
- "@openpkg-ts/spec": "^0.3.1",
52
+ "@doccov/sdk": "^0.5.7",
53
+ "@openpkg-ts/spec": "^0.4.0",
54
54
  "ai": "^4.0.0",
55
55
  "chalk": "^5.4.1",
56
56
  "commander": "^14.0.0",