@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.
- package/dist/cli.js +419 -90
- 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("--
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
|
|
805
|
+
} else {
|
|
806
|
+
log(chalk2.gray(`
|
|
766
807
|
Generating AI summary...`));
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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 (
|
|
875
|
-
log(chalk2.gray(` ... and ${
|
|
951
|
+
if (functionChanges.length > 3) {
|
|
952
|
+
log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
|
|
876
953
|
}
|
|
877
954
|
}
|
|
878
|
-
if (
|
|
879
|
-
log(
|
|
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 (
|
|
882
|
-
log(
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
977
|
+
const parts = [];
|
|
890
978
|
if (diff.driftIntroduced > 0) {
|
|
891
|
-
|
|
979
|
+
parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
|
|
892
980
|
}
|
|
893
981
|
if (diff.driftResolved > 0) {
|
|
894
|
-
|
|
982
|
+
parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
|
|
895
983
|
}
|
|
984
|
+
log(` Drift: ${parts.join(", ")}`);
|
|
896
985
|
}
|
|
897
|
-
|
|
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.
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
916
|
-
|
|
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
|
-
|
|
920
|
-
log(
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
926
|
-
|
|
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 (
|
|
930
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
53
|
-
"@openpkg-ts/spec": "^0.
|
|
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",
|