@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.
Files changed (2) hide show
  1. package/dist/cli.js +211 -2
  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.0";
2025
+ var version = "0.15.1";
1817
2026
 
1818
2027
  // src/utils/filter-options.ts
1819
2028
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",