@doccov/cli 0.15.1 → 0.16.0
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 +211 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -557,6 +557,205 @@ function renderHtml(stats, options = {}) {
|
|
|
557
557
|
</body>
|
|
558
558
|
</html>`;
|
|
559
559
|
}
|
|
560
|
+
// src/reports/pr-comment.ts
|
|
561
|
+
function renderPRComment(data, opts = {}) {
|
|
562
|
+
const { diff, headSpec } = data;
|
|
563
|
+
const limit = opts.limit ?? 10;
|
|
564
|
+
const lines = [];
|
|
565
|
+
const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
|
|
566
|
+
const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
|
|
567
|
+
lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
|
|
568
|
+
lines.push("");
|
|
569
|
+
const targetStr = opts.minCoverage !== undefined ? ` (target: ${opts.minCoverage}%) ${diff.newCoverage >= opts.minCoverage ? "✅" : "❌"}` : "";
|
|
570
|
+
lines.push(`**Patch coverage:** ${diff.newCoverage}%${targetStr}`);
|
|
571
|
+
if (diff.newUndocumented.length > 0) {
|
|
572
|
+
lines.push(`**New undocumented exports:** ${diff.newUndocumented.length}`);
|
|
573
|
+
}
|
|
574
|
+
if (diff.driftIntroduced > 0) {
|
|
575
|
+
lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
|
|
576
|
+
}
|
|
577
|
+
if (diff.newUndocumented.length > 0) {
|
|
578
|
+
lines.push("");
|
|
579
|
+
lines.push("### Undocumented exports in this PR");
|
|
580
|
+
lines.push("");
|
|
581
|
+
renderUndocumentedExports(lines, diff.newUndocumented, headSpec, opts, limit);
|
|
582
|
+
}
|
|
583
|
+
if (diff.driftIntroduced > 0 && headSpec) {
|
|
584
|
+
lines.push("");
|
|
585
|
+
lines.push("### Doc drift detected");
|
|
586
|
+
lines.push("");
|
|
587
|
+
renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
|
|
588
|
+
}
|
|
589
|
+
const fixGuidance = renderFixGuidance(diff);
|
|
590
|
+
if (fixGuidance) {
|
|
591
|
+
lines.push("");
|
|
592
|
+
lines.push("### How to fix");
|
|
593
|
+
lines.push("");
|
|
594
|
+
lines.push(fixGuidance);
|
|
595
|
+
}
|
|
596
|
+
lines.push("");
|
|
597
|
+
lines.push("<details>");
|
|
598
|
+
lines.push("<summary>View full report</summary>");
|
|
599
|
+
lines.push("");
|
|
600
|
+
renderDetailsTable(lines, diff);
|
|
601
|
+
lines.push("");
|
|
602
|
+
lines.push("</details>");
|
|
603
|
+
return lines.join(`
|
|
604
|
+
`);
|
|
605
|
+
}
|
|
606
|
+
function renderUndocumentedExports(lines, undocumented, headSpec, opts, limit) {
|
|
607
|
+
if (!headSpec) {
|
|
608
|
+
for (const name of undocumented.slice(0, limit)) {
|
|
609
|
+
lines.push(`- \`${name}\``);
|
|
610
|
+
}
|
|
611
|
+
if (undocumented.length > limit) {
|
|
612
|
+
lines.push(`- _...and ${undocumented.length - limit} more_`);
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const byFile = new Map;
|
|
617
|
+
const undocSet = new Set(undocumented);
|
|
618
|
+
for (const exp of headSpec.exports) {
|
|
619
|
+
if (undocSet.has(exp.name)) {
|
|
620
|
+
const file = exp.source?.file ?? "unknown";
|
|
621
|
+
const list = byFile.get(file) ?? [];
|
|
622
|
+
list.push(exp);
|
|
623
|
+
byFile.set(file, list);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
let count = 0;
|
|
627
|
+
for (const [file, exports] of byFile) {
|
|
628
|
+
if (count >= limit)
|
|
629
|
+
break;
|
|
630
|
+
const fileLink = buildFileLink(file, opts);
|
|
631
|
+
lines.push(`\uD83D\uDCC1 ${fileLink}`);
|
|
632
|
+
for (const exp of exports) {
|
|
633
|
+
if (count >= limit) {
|
|
634
|
+
lines.push(`- _...and more_`);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
const sig = formatExportSignature(exp);
|
|
638
|
+
lines.push(`- \`${sig}\``);
|
|
639
|
+
const missing = getMissingSignals(exp);
|
|
640
|
+
if (missing.length > 0) {
|
|
641
|
+
lines.push(` - Missing: ${missing.join(", ")}`);
|
|
642
|
+
}
|
|
643
|
+
count++;
|
|
644
|
+
}
|
|
645
|
+
lines.push("");
|
|
646
|
+
}
|
|
647
|
+
if (undocumented.length > count) {
|
|
648
|
+
lines.push(`_...and ${undocumented.length - count} more undocumented exports_`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function renderDriftIssues(lines, _undocumented, headSpec, opts, limit) {
|
|
652
|
+
const driftIssues = [];
|
|
653
|
+
for (const exp of headSpec.exports) {
|
|
654
|
+
const drifts = exp.docs?.drift;
|
|
655
|
+
if (drifts) {
|
|
656
|
+
for (const d of drifts) {
|
|
657
|
+
driftIssues.push({
|
|
658
|
+
exportName: exp.name,
|
|
659
|
+
file: exp.source?.file,
|
|
660
|
+
drift: d
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (driftIssues.length === 0) {
|
|
666
|
+
lines.push("_No specific drift details available_");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
for (const issue of driftIssues.slice(0, limit)) {
|
|
670
|
+
const fileRef = issue.file ? `\`${issue.file}\`` : "unknown file";
|
|
671
|
+
const fileLink = issue.file ? buildFileLink(issue.file, opts) : fileRef;
|
|
672
|
+
lines.push(`⚠️ ${fileLink}: \`${issue.exportName}\``);
|
|
673
|
+
lines.push(`- ${issue.drift.issue}`);
|
|
674
|
+
if (issue.drift.suggestion) {
|
|
675
|
+
lines.push(`- Fix: ${issue.drift.suggestion}`);
|
|
676
|
+
}
|
|
677
|
+
lines.push("");
|
|
678
|
+
}
|
|
679
|
+
if (driftIssues.length > limit) {
|
|
680
|
+
lines.push(`_...and ${driftIssues.length - limit} more drift issues_`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function buildFileLink(file, opts) {
|
|
684
|
+
if (opts.repoUrl && opts.sha) {
|
|
685
|
+
const url = `${opts.repoUrl}/blob/${opts.sha}/${file}`;
|
|
686
|
+
return `[\`${file}\`](${url})`;
|
|
687
|
+
}
|
|
688
|
+
return `\`${file}\``;
|
|
689
|
+
}
|
|
690
|
+
function formatExportSignature(exp) {
|
|
691
|
+
const prefix = `export ${exp.kind === "type" ? "type" : exp.kind === "interface" ? "interface" : exp.kind === "class" ? "class" : "function"}`;
|
|
692
|
+
if (exp.kind === "function" && exp.signatures?.[0]) {
|
|
693
|
+
const sig = exp.signatures[0];
|
|
694
|
+
const params = sig.parameters?.map((p) => `${p.name}${p.required === false ? "?" : ""}`).join(", ") ?? "";
|
|
695
|
+
const ret = sig.returns?.tsType ?? "void";
|
|
696
|
+
return `${prefix} ${exp.name}(${params}): ${ret}`;
|
|
697
|
+
}
|
|
698
|
+
if (exp.kind === "type" || exp.kind === "interface") {
|
|
699
|
+
return `${prefix} ${exp.name}`;
|
|
700
|
+
}
|
|
701
|
+
if (exp.kind === "class") {
|
|
702
|
+
return `${prefix} ${exp.name}`;
|
|
703
|
+
}
|
|
704
|
+
return `export ${exp.kind} ${exp.name}`;
|
|
705
|
+
}
|
|
706
|
+
function getMissingSignals(exp) {
|
|
707
|
+
const missing = [];
|
|
708
|
+
if (!exp.description) {
|
|
709
|
+
missing.push("description");
|
|
710
|
+
}
|
|
711
|
+
if (exp.kind === "function" && exp.signatures?.[0]) {
|
|
712
|
+
const sig = exp.signatures[0];
|
|
713
|
+
const undocParams = sig.parameters?.filter((p) => !p.description) ?? [];
|
|
714
|
+
if (undocParams.length > 0) {
|
|
715
|
+
missing.push(`\`@param ${undocParams.map((p) => p.name).join(", ")}\``);
|
|
716
|
+
}
|
|
717
|
+
if (!sig.returns?.description && sig.returns?.tsType !== "void") {
|
|
718
|
+
missing.push("`@returns`");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return missing;
|
|
722
|
+
}
|
|
723
|
+
function renderFixGuidance(diff) {
|
|
724
|
+
const sections = [];
|
|
725
|
+
if (diff.newUndocumented.length > 0) {
|
|
726
|
+
sections.push(`**For undocumented exports:**
|
|
727
|
+
` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
|
|
728
|
+
}
|
|
729
|
+
if (diff.driftIntroduced > 0) {
|
|
730
|
+
sections.push(`**For doc drift:**
|
|
731
|
+
` + "Update the code examples in your markdown files to match current signatures.");
|
|
732
|
+
}
|
|
733
|
+
if (diff.breaking.length > 0) {
|
|
734
|
+
sections.push(`**For breaking changes:**
|
|
735
|
+
` + "Consider adding a migration guide or updating changelog.");
|
|
736
|
+
}
|
|
737
|
+
if (sections.length === 0) {
|
|
738
|
+
return "";
|
|
739
|
+
}
|
|
740
|
+
sections.push(`
|
|
741
|
+
Push your changes — DocCov re-checks automatically.`);
|
|
742
|
+
return sections.join(`
|
|
743
|
+
|
|
744
|
+
`);
|
|
745
|
+
}
|
|
746
|
+
function renderDetailsTable(lines, diff) {
|
|
747
|
+
const delta = (n) => n > 0 ? `+${n}` : n === 0 ? "0" : String(n);
|
|
748
|
+
lines.push("| Metric | Before | After | Delta |");
|
|
749
|
+
lines.push("|--------|--------|-------|-------|");
|
|
750
|
+
lines.push(`| Coverage | ${diff.oldCoverage}% | ${diff.newCoverage}% | ${delta(diff.coverageDelta)}% |`);
|
|
751
|
+
lines.push(`| Breaking changes | - | ${diff.breaking.length} | - |`);
|
|
752
|
+
lines.push(`| New exports | - | ${diff.nonBreaking.length} | - |`);
|
|
753
|
+
lines.push(`| Undocumented | - | ${diff.newUndocumented.length} | - |`);
|
|
754
|
+
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
755
|
+
const driftDelta = diff.driftIntroduced - diff.driftResolved;
|
|
756
|
+
lines.push(`| Drift | - | - | ${delta(driftDelta)} |`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
560
759
|
// src/reports/stats.ts
|
|
561
760
|
import { isFixableDrift } from "@doccov/sdk";
|
|
562
761
|
import {
|
|
@@ -1308,7 +1507,7 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1308
1507
|
...defaultDependencies2,
|
|
1309
1508
|
...dependencies
|
|
1310
1509
|
};
|
|
1311
|
-
program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
|
|
1510
|
+
program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
|
|
1312
1511
|
try {
|
|
1313
1512
|
const baseFile = options.base ?? baseArg;
|
|
1314
1513
|
const headFile = options.head ?? headArg;
|
|
@@ -1418,6 +1617,16 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1418
1617
|
case "github":
|
|
1419
1618
|
printGitHubAnnotations(diff, log);
|
|
1420
1619
|
break;
|
|
1620
|
+
case "pr-comment": {
|
|
1621
|
+
const content = renderPRComment({ diff, baseName, headName, headSpec }, {
|
|
1622
|
+
repoUrl: options.repoUrl,
|
|
1623
|
+
sha: options.sha,
|
|
1624
|
+
minCoverage,
|
|
1625
|
+
limit
|
|
1626
|
+
});
|
|
1627
|
+
log(content);
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1421
1630
|
}
|
|
1422
1631
|
const failures = validateDiff(diff, headSpec, {
|
|
1423
1632
|
minCoverage,
|
|
@@ -1813,7 +2022,7 @@ import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as
|
|
|
1813
2022
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
1814
2023
|
import chalk8 from "chalk";
|
|
1815
2024
|
// package.json
|
|
1816
|
-
var version = "0.15.
|
|
2025
|
+
var version = "0.15.1";
|
|
1817
2026
|
|
|
1818
2027
|
// src/utils/filter-options.ts
|
|
1819
2028
|
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|