@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.
- package/dist/cli.js +417 -129
- 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,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
|
-
|
|
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
|
-
|
|
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.
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
877
|
-
log(chalk2.
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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 (
|
|
910
|
-
log(
|
|
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.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
977
|
+
const parts = [];
|
|
921
978
|
if (diff.driftIntroduced > 0) {
|
|
922
|
-
|
|
979
|
+
parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
|
|
923
980
|
}
|
|
924
981
|
if (diff.driftResolved > 0) {
|
|
925
|
-
|
|
982
|
+
parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
|
|
926
983
|
}
|
|
984
|
+
log(` Drift: ${parts.join(", ")}`);
|
|
927
985
|
}
|
|
928
|
-
|
|
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.
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
957
|
-
|
|
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
|
-
|
|
961
|
-
log(
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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 (
|
|
967
|
-
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|