@doccov/cli 0.5.6 → 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 +417 -129
  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,142 +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
- if (diff.memberChanges && diff.memberChanges.length > 0) {
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) {
868
945
  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);
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})`));
875
950
  }
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
- }
951
+ if (functionChanges.length > 3) {
952
+ log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
896
953
  }
897
954
  }
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
- }
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 ? ", ..." : ""}`));
908
960
  }
909
- if (diff.improvedExports.length > 0) {
910
- log(chalk2.green(` ${diff.improvedExports.length} export(s) improved docs`));
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 ? ", ..." : ""}`));
911
966
  }
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
- }
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 ? ", ..." : ""}`));
917
974
  }
918
975
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
919
976
  log("");
920
- log(chalk2.bold("Drift"));
977
+ const parts = [];
921
978
  if (diff.driftIntroduced > 0) {
922
- log(chalk2.red(` +${diff.driftIntroduced} new drift issue(s)`));
979
+ parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
923
980
  }
924
981
  if (diff.driftResolved > 0) {
925
- log(chalk2.green(` -${diff.driftResolved} drift issue(s) resolved`));
982
+ parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
926
983
  }
984
+ log(` Drift: ${parts.join(", ")}`);
927
985
  }
928
- 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;
929
1012
  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
- }
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}`));
955
1024
  }
956
- if (impactedFiles.length > 10) {
957
- 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}`);
958
1091
  }
959
1092
  }
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}`));
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>`;
965
1213
  }
966
- if (missingDocs.length > 5) {
967
- log(chalk2.gray(` ... and ${missingDocs.length - 5} more`));
1214
+ if (file.references.length > 5) {
1215
+ html += `
1216
+ <div class="ref-item neutral">... and ${file.references.length - 5} more</div>`;
968
1217
  }
1218
+ html += `
1219
+ </div>
1220
+ </div>`;
969
1221
  }
970
- if (impactedFiles.length === 0 && missingDocs.length === 0) {
971
- log(chalk2.green(" ✓ No docs impact detected"));
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>`;
1242
+ }
1243
+ html += `
1244
+ </ul>`;
1245
+ }
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>`;
972
1254
  }
1255
+ html += `
1256
+ </div>`;
973
1257
  }
974
- log("");
1258
+ html += `
1259
+ </div>
1260
+ </body>
1261
+ </html>`;
1262
+ return html;
975
1263
  }
976
1264
 
977
1265
  // src/commands/generate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.5.6",
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.5.6",
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",