@doccov/cli 0.27.1 → 0.28.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 +99 -737
- package/dist/config/index.d.ts +4 -44
- package/dist/config/index.js +1 -84
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,90 +4,7 @@
|
|
|
4
4
|
import { access } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
|
|
8
|
-
// src/config/schema.ts
|
|
9
|
-
import { z } from "zod";
|
|
10
|
-
var stringList = z.union([
|
|
11
|
-
z.string(),
|
|
12
|
-
z.array(z.string())
|
|
13
|
-
]);
|
|
14
|
-
var docsConfigSchema = z.object({
|
|
15
|
-
include: stringList.optional(),
|
|
16
|
-
exclude: stringList.optional()
|
|
17
|
-
});
|
|
18
|
-
var exampleModeSchema = z.enum([
|
|
19
|
-
"presence",
|
|
20
|
-
"typecheck",
|
|
21
|
-
"run"
|
|
22
|
-
]);
|
|
23
|
-
var exampleModesSchema = z.union([
|
|
24
|
-
exampleModeSchema,
|
|
25
|
-
z.array(exampleModeSchema),
|
|
26
|
-
z.string()
|
|
27
|
-
]);
|
|
28
|
-
var apiSurfaceConfigSchema = z.object({
|
|
29
|
-
minCompleteness: z.number().min(0).max(100).optional(),
|
|
30
|
-
warnBelow: z.number().min(0).max(100).optional(),
|
|
31
|
-
ignore: z.array(z.string()).optional()
|
|
32
|
-
});
|
|
33
|
-
var checkConfigSchema = z.object({
|
|
34
|
-
examples: exampleModesSchema.optional(),
|
|
35
|
-
minHealth: z.number().min(0).max(100).optional(),
|
|
36
|
-
minCoverage: z.number().min(0).max(100).optional(),
|
|
37
|
-
maxDrift: z.number().min(0).max(100).optional(),
|
|
38
|
-
minApiSurface: z.number().min(0).max(100).optional(),
|
|
39
|
-
apiSurface: apiSurfaceConfigSchema.optional()
|
|
40
|
-
});
|
|
41
|
-
var docCovConfigSchema = z.object({
|
|
42
|
-
include: stringList.optional(),
|
|
43
|
-
exclude: stringList.optional(),
|
|
44
|
-
plugins: z.array(z.unknown()).optional(),
|
|
45
|
-
docs: docsConfigSchema.optional(),
|
|
46
|
-
check: checkConfigSchema.optional()
|
|
47
|
-
});
|
|
48
|
-
var normalizeList = (value) => {
|
|
49
|
-
if (!value) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const list = Array.isArray(value) ? value : [value];
|
|
53
|
-
const normalized = list.map((item) => item.trim()).filter(Boolean);
|
|
54
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
55
|
-
};
|
|
56
|
-
var normalizeConfig = (input) => {
|
|
57
|
-
const include = normalizeList(input.include);
|
|
58
|
-
const exclude = normalizeList(input.exclude);
|
|
59
|
-
let docs;
|
|
60
|
-
if (input.docs) {
|
|
61
|
-
const docsInclude = normalizeList(input.docs.include);
|
|
62
|
-
const docsExclude = normalizeList(input.docs.exclude);
|
|
63
|
-
if (docsInclude || docsExclude) {
|
|
64
|
-
docs = {
|
|
65
|
-
include: docsInclude,
|
|
66
|
-
exclude: docsExclude
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
let check;
|
|
71
|
-
if (input.check) {
|
|
72
|
-
check = {
|
|
73
|
-
examples: input.check.examples,
|
|
74
|
-
minHealth: input.check.minHealth,
|
|
75
|
-
minCoverage: input.check.minCoverage,
|
|
76
|
-
maxDrift: input.check.maxDrift,
|
|
77
|
-
minApiSurface: input.check.minApiSurface,
|
|
78
|
-
apiSurface: input.check.apiSurface
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
include,
|
|
83
|
-
exclude,
|
|
84
|
-
plugins: input.plugins,
|
|
85
|
-
docs,
|
|
86
|
-
check
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// src/config/doccov-config.ts
|
|
7
|
+
import { docCovConfigSchema, normalizeConfig } from "@doccov/sdk";
|
|
91
8
|
var DOCCOV_CONFIG_FILENAMES = [
|
|
92
9
|
"doccov.config.ts",
|
|
93
10
|
"doccov.config.mts",
|
|
@@ -1517,73 +1434,6 @@ function displayPreview(editsByFile, targetDir, log) {
|
|
|
1517
1434
|
// src/commands/check/output.ts
|
|
1518
1435
|
import { generateReportFromDocCov } from "@doccov/sdk";
|
|
1519
1436
|
|
|
1520
|
-
// src/reports/changelog-renderer.ts
|
|
1521
|
-
function renderChangelog(data, options = {}) {
|
|
1522
|
-
const { diff, categorizedBreaking } = data;
|
|
1523
|
-
const lines = [];
|
|
1524
|
-
const version = options.version ?? data.version ?? "Unreleased";
|
|
1525
|
-
const date = options.date instanceof Date ? options.date.toISOString().split("T")[0] : options.date ?? new Date().toISOString().split("T")[0];
|
|
1526
|
-
lines.push(`## [${version}] - ${date}`);
|
|
1527
|
-
lines.push("");
|
|
1528
|
-
if (diff.breaking.length > 0) {
|
|
1529
|
-
lines.push("### ⚠️ BREAKING CHANGES");
|
|
1530
|
-
lines.push("");
|
|
1531
|
-
if (categorizedBreaking && categorizedBreaking.length > 0) {
|
|
1532
|
-
for (const breaking of categorizedBreaking) {
|
|
1533
|
-
const severity = breaking.severity === "high" ? "**" : "";
|
|
1534
|
-
lines.push(`- ${severity}${breaking.name}${severity}: ${breaking.reason}`);
|
|
1535
|
-
}
|
|
1536
|
-
} else {
|
|
1537
|
-
for (const id of diff.breaking) {
|
|
1538
|
-
lines.push(`- \`${id}\` removed or changed`);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
lines.push("");
|
|
1542
|
-
}
|
|
1543
|
-
if (diff.nonBreaking.length > 0) {
|
|
1544
|
-
lines.push("### Added");
|
|
1545
|
-
lines.push("");
|
|
1546
|
-
for (const id of diff.nonBreaking) {
|
|
1547
|
-
lines.push(`- \`${id}\``);
|
|
1548
|
-
}
|
|
1549
|
-
lines.push("");
|
|
1550
|
-
}
|
|
1551
|
-
if (diff.docsOnly.length > 0) {
|
|
1552
|
-
lines.push("### Documentation");
|
|
1553
|
-
lines.push("");
|
|
1554
|
-
for (const id of diff.docsOnly) {
|
|
1555
|
-
lines.push(`- Updated documentation for \`${id}\``);
|
|
1556
|
-
}
|
|
1557
|
-
lines.push("");
|
|
1558
|
-
}
|
|
1559
|
-
if (diff.coverageDelta !== 0) {
|
|
1560
|
-
lines.push("### Coverage");
|
|
1561
|
-
lines.push("");
|
|
1562
|
-
const arrow = diff.coverageDelta > 0 ? "↑" : "↓";
|
|
1563
|
-
const sign = diff.coverageDelta > 0 ? "+" : "";
|
|
1564
|
-
lines.push(`- Documentation coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% (${arrow} ${sign}${diff.coverageDelta}%)`);
|
|
1565
|
-
lines.push("");
|
|
1566
|
-
}
|
|
1567
|
-
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1568
|
-
if (!lines.some((l) => l.startsWith("### Coverage"))) {
|
|
1569
|
-
lines.push("### Coverage");
|
|
1570
|
-
lines.push("");
|
|
1571
|
-
}
|
|
1572
|
-
if (diff.driftResolved > 0) {
|
|
1573
|
-
lines.push(`- Fixed ${diff.driftResolved} drift issue${diff.driftResolved === 1 ? "" : "s"}`);
|
|
1574
|
-
}
|
|
1575
|
-
if (diff.driftIntroduced > 0) {
|
|
1576
|
-
lines.push(`- ${diff.driftIntroduced} new drift issue${diff.driftIntroduced === 1 ? "" : "s"} detected`);
|
|
1577
|
-
}
|
|
1578
|
-
lines.push("");
|
|
1579
|
-
}
|
|
1580
|
-
if (options.compareUrl) {
|
|
1581
|
-
lines.push(`**Full Changelog**: ${options.compareUrl}`);
|
|
1582
|
-
lines.push("");
|
|
1583
|
-
}
|
|
1584
|
-
return lines.join(`
|
|
1585
|
-
`);
|
|
1586
|
-
}
|
|
1587
1437
|
// src/reports/diff-markdown.ts
|
|
1588
1438
|
import * as path4 from "node:path";
|
|
1589
1439
|
function bar(pct, width = 10) {
|
|
@@ -1801,55 +1651,6 @@ function renderDiffHtml(data, options = {}) {
|
|
|
1801
1651
|
</body>
|
|
1802
1652
|
</html>`;
|
|
1803
1653
|
}
|
|
1804
|
-
// src/reports/github.ts
|
|
1805
|
-
function renderGithubSummary(stats, options = {}) {
|
|
1806
|
-
const coverageScore = options.coverageScore ?? stats.coverageScore;
|
|
1807
|
-
const driftCount = options.driftCount ?? stats.driftCount;
|
|
1808
|
-
const qualityIssues = options.qualityIssues ?? 0;
|
|
1809
|
-
let output = "";
|
|
1810
|
-
if (stats.health) {
|
|
1811
|
-
const h = stats.health;
|
|
1812
|
-
const status = h.score >= 80 ? "✅" : h.score >= 50 ? "⚠️" : "❌";
|
|
1813
|
-
output += `## ${status} Documentation Health: ${h.score}%
|
|
1814
|
-
|
|
1815
|
-
`;
|
|
1816
|
-
output += `| Metric | Score | Details |
|
|
1817
|
-
|--------|-------|---------|
|
|
1818
|
-
`;
|
|
1819
|
-
output += `| Completeness | ${h.completeness.score}% | ${h.completeness.total - h.completeness.documented} missing docs |
|
|
1820
|
-
`;
|
|
1821
|
-
output += `| Accuracy | ${h.accuracy.score}% | ${h.accuracy.issues} drift issues |
|
|
1822
|
-
`;
|
|
1823
|
-
if (h.examples) {
|
|
1824
|
-
output += `| Examples | ${h.examples.score}% | ${h.examples.passed}/${h.examples.total} passed |
|
|
1825
|
-
`;
|
|
1826
|
-
}
|
|
1827
|
-
output += `
|
|
1828
|
-
`;
|
|
1829
|
-
} else {
|
|
1830
|
-
output += `## Documentation Coverage: ${coverageScore}%
|
|
1831
|
-
|
|
1832
|
-
`;
|
|
1833
|
-
}
|
|
1834
|
-
output += `| Metric | Value |
|
|
1835
|
-
|--------|-------|
|
|
1836
|
-
`;
|
|
1837
|
-
output += `| Coverage Score | ${coverageScore}% |
|
|
1838
|
-
`;
|
|
1839
|
-
output += `| Total Exports | ${stats.totalExports} |
|
|
1840
|
-
`;
|
|
1841
|
-
output += `| Drift Issues | ${driftCount} |
|
|
1842
|
-
`;
|
|
1843
|
-
output += `| Quality Issues | ${qualityIssues} |
|
|
1844
|
-
`;
|
|
1845
|
-
if (!stats.health) {
|
|
1846
|
-
const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
|
|
1847
|
-
output += `
|
|
1848
|
-
${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "needs improvement" : "failing"}
|
|
1849
|
-
`;
|
|
1850
|
-
}
|
|
1851
|
-
return output;
|
|
1852
|
-
}
|
|
1853
1654
|
// src/reports/markdown.ts
|
|
1854
1655
|
import { DRIFT_CATEGORY_LABELS } from "@doccov/sdk";
|
|
1855
1656
|
function bar2(pct, width = 10) {
|
|
@@ -1977,321 +1778,8 @@ function renderMarkdown(stats, options = {}) {
|
|
|
1977
1778
|
return lines.join(`
|
|
1978
1779
|
`);
|
|
1979
1780
|
}
|
|
1980
|
-
|
|
1981
|
-
// src/reports/html.ts
|
|
1982
|
-
function escapeHtml2(s) {
|
|
1983
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1984
|
-
}
|
|
1985
|
-
function renderHtml(stats, options = {}) {
|
|
1986
|
-
const md = renderMarkdown(stats, options);
|
|
1987
|
-
return `<!DOCTYPE html>
|
|
1988
|
-
<html lang="en">
|
|
1989
|
-
<head>
|
|
1990
|
-
<meta charset="UTF-8">
|
|
1991
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1992
|
-
<title>DocCov Report: ${escapeHtml2(stats.packageName)}</title>
|
|
1993
|
-
<style>
|
|
1994
|
-
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
1995
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
|
|
1996
|
-
h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
1997
|
-
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
1998
|
-
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
1999
|
-
th { background: #161b22; }
|
|
2000
|
-
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
2001
|
-
a { color: var(--accent); }
|
|
2002
|
-
</style>
|
|
2003
|
-
</head>
|
|
2004
|
-
<body>
|
|
2005
|
-
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml2(md)}</pre>
|
|
2006
|
-
</body>
|
|
2007
|
-
</html>`;
|
|
2008
|
-
}
|
|
2009
|
-
// src/reports/pr-comment.ts
|
|
2010
|
-
import { getExportDrift as getExportDrift2 } from "@doccov/sdk";
|
|
2011
|
-
function extractReturnType(schema) {
|
|
2012
|
-
if (!schema)
|
|
2013
|
-
return "void";
|
|
2014
|
-
if (typeof schema === "string")
|
|
2015
|
-
return schema;
|
|
2016
|
-
if (typeof schema === "object") {
|
|
2017
|
-
const s = schema;
|
|
2018
|
-
if (typeof s.type === "string")
|
|
2019
|
-
return s.type;
|
|
2020
|
-
if (typeof s.$ref === "string") {
|
|
2021
|
-
const ref = s.$ref;
|
|
2022
|
-
return ref.startsWith("#/types/") ? ref.slice("#/types/".length) : ref;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
return "unknown";
|
|
2026
|
-
}
|
|
2027
|
-
function renderPRComment(data, opts = {}) {
|
|
2028
|
-
const { diff, headSpec, doccov } = data;
|
|
2029
|
-
const limit = opts.limit ?? 10;
|
|
2030
|
-
const lines = [];
|
|
2031
|
-
const hasStaleRefs = (opts.staleDocsRefs?.length ?? 0) > 0;
|
|
2032
|
-
const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || hasStaleRefs || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
|
|
2033
|
-
const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
|
|
2034
|
-
lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
|
|
2035
|
-
lines.push("");
|
|
2036
|
-
const targetStr = opts.minCoverage !== undefined ? ` (target: ${opts.minCoverage}%) ${diff.newCoverage >= opts.minCoverage ? "✅" : "❌"}` : "";
|
|
2037
|
-
lines.push(`**Patch coverage:** ${diff.newCoverage}%${targetStr}`);
|
|
2038
|
-
if (diff.newUndocumented.length > 0) {
|
|
2039
|
-
lines.push(`**New undocumented exports:** ${diff.newUndocumented.length}`);
|
|
2040
|
-
}
|
|
2041
|
-
if (diff.driftIntroduced > 0) {
|
|
2042
|
-
lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
|
|
2043
|
-
}
|
|
2044
|
-
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
2045
|
-
lines.push(`**Stale doc references:** ${opts.staleDocsRefs.length}`);
|
|
2046
|
-
}
|
|
2047
|
-
if (opts.semverBump) {
|
|
2048
|
-
const emoji = opts.semverBump.bump === "major" ? "\uD83D\uDD34" : opts.semverBump.bump === "minor" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
|
|
2049
|
-
lines.push(`**Semver:** ${emoji} ${opts.semverBump.bump.toUpperCase()} (${opts.semverBump.reason})`);
|
|
2050
|
-
}
|
|
2051
|
-
if (diff.newUndocumented.length > 0) {
|
|
2052
|
-
lines.push("");
|
|
2053
|
-
lines.push("### Undocumented exports in this PR");
|
|
2054
|
-
lines.push("");
|
|
2055
|
-
renderUndocumentedExports(lines, diff.newUndocumented, headSpec, opts, limit);
|
|
2056
|
-
}
|
|
2057
|
-
if (diff.driftIntroduced > 0 && headSpec) {
|
|
2058
|
-
lines.push("");
|
|
2059
|
-
lines.push("### Doc drift detected");
|
|
2060
|
-
lines.push("");
|
|
2061
|
-
renderDriftIssues(lines, diff.newUndocumented, headSpec, doccov, opts, limit);
|
|
2062
|
-
}
|
|
2063
|
-
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
2064
|
-
lines.push("");
|
|
2065
|
-
lines.push("### \uD83D\uDCDD Stale documentation references");
|
|
2066
|
-
lines.push("");
|
|
2067
|
-
lines.push("These markdown files reference exports that no longer exist:");
|
|
2068
|
-
lines.push("");
|
|
2069
|
-
renderStaleDocsRefs(lines, opts.staleDocsRefs, opts, limit);
|
|
2070
|
-
}
|
|
2071
|
-
const fixGuidance = renderFixGuidance(diff, opts);
|
|
2072
|
-
if (fixGuidance) {
|
|
2073
|
-
lines.push("");
|
|
2074
|
-
lines.push("### How to fix");
|
|
2075
|
-
lines.push("");
|
|
2076
|
-
lines.push(fixGuidance);
|
|
2077
|
-
}
|
|
2078
|
-
lines.push("");
|
|
2079
|
-
lines.push("<details>");
|
|
2080
|
-
lines.push("<summary>View full report</summary>");
|
|
2081
|
-
lines.push("");
|
|
2082
|
-
renderDetailsTable(lines, diff);
|
|
2083
|
-
lines.push("");
|
|
2084
|
-
lines.push("</details>");
|
|
2085
|
-
if (opts.includeBadge !== false && opts.repoUrl) {
|
|
2086
|
-
const repoMatch = opts.repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
2087
|
-
if (repoMatch) {
|
|
2088
|
-
const [, owner, repo] = repoMatch;
|
|
2089
|
-
lines.push("");
|
|
2090
|
-
lines.push("---");
|
|
2091
|
-
lines.push("");
|
|
2092
|
-
lines.push(`[](https://doccov.dev/${owner}/${repo})`);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
return lines.join(`
|
|
2096
|
-
`);
|
|
2097
|
-
}
|
|
2098
|
-
function renderUndocumentedExports(lines, undocumented, headSpec, opts, limit) {
|
|
2099
|
-
if (!headSpec) {
|
|
2100
|
-
for (const name of undocumented.slice(0, limit)) {
|
|
2101
|
-
lines.push(`- \`${name}\``);
|
|
2102
|
-
}
|
|
2103
|
-
if (undocumented.length > limit) {
|
|
2104
|
-
lines.push(`- _...and ${undocumented.length - limit} more_`);
|
|
2105
|
-
}
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
const byFile = new Map;
|
|
2109
|
-
const undocSet = new Set(undocumented);
|
|
2110
|
-
for (const exp of headSpec.exports) {
|
|
2111
|
-
if (undocSet.has(exp.name)) {
|
|
2112
|
-
const file = exp.source?.file ?? "unknown";
|
|
2113
|
-
const list = byFile.get(file) ?? [];
|
|
2114
|
-
list.push(exp);
|
|
2115
|
-
byFile.set(file, list);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
let count = 0;
|
|
2119
|
-
for (const [file, exports] of byFile) {
|
|
2120
|
-
if (count >= limit)
|
|
2121
|
-
break;
|
|
2122
|
-
const fileLink = buildFileLink(file, opts);
|
|
2123
|
-
lines.push(`\uD83D\uDCC1 ${fileLink}`);
|
|
2124
|
-
for (const exp of exports) {
|
|
2125
|
-
if (count >= limit) {
|
|
2126
|
-
lines.push(`- _...and more_`);
|
|
2127
|
-
break;
|
|
2128
|
-
}
|
|
2129
|
-
const sig = formatExportSignature(exp);
|
|
2130
|
-
lines.push(`- \`${sig}\``);
|
|
2131
|
-
const missing = getMissingSignals(exp);
|
|
2132
|
-
if (missing.length > 0) {
|
|
2133
|
-
lines.push(` - Missing: ${missing.join(", ")}`);
|
|
2134
|
-
}
|
|
2135
|
-
count++;
|
|
2136
|
-
}
|
|
2137
|
-
lines.push("");
|
|
2138
|
-
}
|
|
2139
|
-
if (undocumented.length > count) {
|
|
2140
|
-
lines.push(`_...and ${undocumented.length - count} more undocumented exports_`);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
function renderDriftIssues(lines, _undocumented, headSpec, doccov, opts, limit) {
|
|
2144
|
-
const driftIssues = [];
|
|
2145
|
-
for (const exp of headSpec.exports) {
|
|
2146
|
-
const drifts = doccov ? getExportDrift2(exp, doccov) : [];
|
|
2147
|
-
for (const d of drifts) {
|
|
2148
|
-
driftIssues.push({
|
|
2149
|
-
exportName: exp.name,
|
|
2150
|
-
file: exp.source?.file,
|
|
2151
|
-
drift: d
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (driftIssues.length === 0) {
|
|
2156
|
-
lines.push("_No specific drift details available_");
|
|
2157
|
-
return;
|
|
2158
|
-
}
|
|
2159
|
-
for (const issue of driftIssues.slice(0, limit)) {
|
|
2160
|
-
const fileRef = issue.file ? `\`${issue.file}\`` : "unknown file";
|
|
2161
|
-
const fileLink = issue.file ? buildFileLink(issue.file, opts) : fileRef;
|
|
2162
|
-
lines.push(`⚠️ ${fileLink}: \`${issue.exportName}\``);
|
|
2163
|
-
lines.push(`- ${issue.drift.issue}`);
|
|
2164
|
-
if (issue.drift.suggestion) {
|
|
2165
|
-
lines.push(`- Fix: ${issue.drift.suggestion}`);
|
|
2166
|
-
}
|
|
2167
|
-
lines.push("");
|
|
2168
|
-
}
|
|
2169
|
-
if (driftIssues.length > limit) {
|
|
2170
|
-
lines.push(`_...and ${driftIssues.length - limit} more drift issues_`);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
function buildFileLink(file, opts) {
|
|
2174
|
-
if (opts.repoUrl && opts.sha) {
|
|
2175
|
-
const url = `${opts.repoUrl}/blob/${opts.sha}/${file}`;
|
|
2176
|
-
return `[\`${file}\`](${url})`;
|
|
2177
|
-
}
|
|
2178
|
-
return `\`${file}\``;
|
|
2179
|
-
}
|
|
2180
|
-
function getExportKeyword(kind) {
|
|
2181
|
-
switch (kind) {
|
|
2182
|
-
case "type":
|
|
2183
|
-
return "type";
|
|
2184
|
-
case "interface":
|
|
2185
|
-
return "interface";
|
|
2186
|
-
case "class":
|
|
2187
|
-
return "class";
|
|
2188
|
-
default:
|
|
2189
|
-
return "function";
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
function formatExportSignature(exp) {
|
|
2193
|
-
const prefix2 = `export ${getExportKeyword(exp.kind)}`;
|
|
2194
|
-
if (exp.kind === "function" && exp.signatures?.[0]) {
|
|
2195
|
-
const sig = exp.signatures[0];
|
|
2196
|
-
const params = sig.parameters?.map((p) => `${p.name}${p.required === false ? "?" : ""}`).join(", ") ?? "";
|
|
2197
|
-
const ret = extractReturnType(sig.returns?.schema);
|
|
2198
|
-
return `${prefix2} ${exp.name}(${params}): ${ret}`;
|
|
2199
|
-
}
|
|
2200
|
-
if (exp.kind === "type" || exp.kind === "interface") {
|
|
2201
|
-
return `${prefix2} ${exp.name}`;
|
|
2202
|
-
}
|
|
2203
|
-
if (exp.kind === "class") {
|
|
2204
|
-
return `${prefix2} ${exp.name}`;
|
|
2205
|
-
}
|
|
2206
|
-
return `export ${exp.kind} ${exp.name}`;
|
|
2207
|
-
}
|
|
2208
|
-
function getMissingSignals(exp) {
|
|
2209
|
-
const missing = [];
|
|
2210
|
-
if (!exp.description) {
|
|
2211
|
-
missing.push("description");
|
|
2212
|
-
}
|
|
2213
|
-
if (exp.kind === "function" && exp.signatures?.[0]) {
|
|
2214
|
-
const sig = exp.signatures[0];
|
|
2215
|
-
const undocParams = sig.parameters?.filter((p) => !p.description) ?? [];
|
|
2216
|
-
if (undocParams.length > 0) {
|
|
2217
|
-
missing.push(`\`@param ${undocParams.map((p) => p.name).join(", ")}\``);
|
|
2218
|
-
}
|
|
2219
|
-
if (!sig.returns?.description && extractReturnType(sig.returns?.schema) !== "void") {
|
|
2220
|
-
missing.push("`@returns`");
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
return missing;
|
|
2224
|
-
}
|
|
2225
|
-
function renderStaleDocsRefs(lines, refs, opts, limit) {
|
|
2226
|
-
const byFile = new Map;
|
|
2227
|
-
for (const ref of refs) {
|
|
2228
|
-
const list = byFile.get(ref.file) ?? [];
|
|
2229
|
-
list.push({ line: ref.line, exportName: ref.exportName });
|
|
2230
|
-
byFile.set(ref.file, list);
|
|
2231
|
-
}
|
|
2232
|
-
let count = 0;
|
|
2233
|
-
for (const [file, fileRefs] of byFile) {
|
|
2234
|
-
if (count >= limit)
|
|
2235
|
-
break;
|
|
2236
|
-
const fileLink = buildFileLink(file, opts);
|
|
2237
|
-
lines.push(`\uD83D\uDCC1 ${fileLink}`);
|
|
2238
|
-
for (const ref of fileRefs) {
|
|
2239
|
-
if (count >= limit)
|
|
2240
|
-
break;
|
|
2241
|
-
lines.push(`- Line ${ref.line}: \`${ref.exportName}\` does not exist`);
|
|
2242
|
-
count++;
|
|
2243
|
-
}
|
|
2244
|
-
lines.push("");
|
|
2245
|
-
}
|
|
2246
|
-
if (refs.length > count) {
|
|
2247
|
-
lines.push(`_...and ${refs.length - count} more stale references_`);
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
function renderFixGuidance(diff, opts) {
|
|
2251
|
-
const sections = [];
|
|
2252
|
-
if (diff.newUndocumented.length > 0) {
|
|
2253
|
-
sections.push(`**For undocumented exports:**
|
|
2254
|
-
` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
|
|
2255
|
-
}
|
|
2256
|
-
if (diff.driftIntroduced > 0) {
|
|
2257
|
-
const fixableNote = opts.fixableDriftCount && opts.fixableDriftCount > 0 ? `
|
|
2258
|
-
|
|
2259
|
-
**Quick fix:** Run \`npx doccov check --fix\` to auto-fix ${opts.fixableDriftCount} issue(s).` : "";
|
|
2260
|
-
sections.push(`**For doc drift:**
|
|
2261
|
-
Update JSDoc to match current code signatures.${fixableNote}`);
|
|
2262
|
-
}
|
|
2263
|
-
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
2264
|
-
sections.push(`**For stale docs:**
|
|
2265
|
-
` + "Update or remove code examples that reference deleted exports.");
|
|
2266
|
-
}
|
|
2267
|
-
if (diff.breaking.length > 0) {
|
|
2268
|
-
sections.push(`**For breaking changes:**
|
|
2269
|
-
` + "Consider adding a migration guide or updating changelog.");
|
|
2270
|
-
}
|
|
2271
|
-
if (sections.length === 0) {
|
|
2272
|
-
return "";
|
|
2273
|
-
}
|
|
2274
|
-
sections.push(`
|
|
2275
|
-
Push your changes — DocCov re-checks automatically.`);
|
|
2276
|
-
return sections.join(`
|
|
2277
|
-
|
|
2278
|
-
`);
|
|
2279
|
-
}
|
|
2280
|
-
function renderDetailsTable(lines, diff) {
|
|
2281
|
-
const delta = (n) => n > 0 ? `+${n}` : n === 0 ? "0" : String(n);
|
|
2282
|
-
lines.push("| Metric | Before | After | Delta |");
|
|
2283
|
-
lines.push("|--------|--------|-------|-------|");
|
|
2284
|
-
lines.push(`| Coverage | ${diff.oldCoverage}% | ${diff.newCoverage}% | ${delta(diff.coverageDelta)}% |`);
|
|
2285
|
-
lines.push(`| Breaking changes | - | ${diff.breaking.length} | - |`);
|
|
2286
|
-
lines.push(`| New exports | - | ${diff.nonBreaking.length} | - |`);
|
|
2287
|
-
lines.push(`| Undocumented | - | ${diff.newUndocumented.length} | - |`);
|
|
2288
|
-
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
2289
|
-
const driftDelta = diff.driftIntroduced - diff.driftResolved;
|
|
2290
|
-
lines.push(`| Drift | - | - | ${delta(driftDelta)} |`);
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
1781
|
// src/reports/stats.ts
|
|
2294
|
-
import { getExportAnalysis, getExportDrift as
|
|
1782
|
+
import { getExportAnalysis, getExportDrift as getExportDrift2, isFixableDrift } from "@doccov/sdk";
|
|
2295
1783
|
import {
|
|
2296
1784
|
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
2297
1785
|
} from "@doccov/spec";
|
|
@@ -2332,7 +1820,7 @@ function computeStats(openpkg, doccov) {
|
|
|
2332
1820
|
partiallyDocumented++;
|
|
2333
1821
|
else
|
|
2334
1822
|
undocumented++;
|
|
2335
|
-
for (const d of
|
|
1823
|
+
for (const d of getExportDrift2(exp, doccov)) {
|
|
2336
1824
|
const item = {
|
|
2337
1825
|
exportName: exp.name,
|
|
2338
1826
|
type: d.type,
|
|
@@ -2467,7 +1955,7 @@ function displayHealthTree(health, log) {
|
|
|
2467
1955
|
}
|
|
2468
1956
|
function displayHealthVerbose(health, log) {
|
|
2469
1957
|
const tree = supportsUnicode() ? { branch: "├─", corner: "└─" } : { branch: "|-", corner: "\\-" };
|
|
2470
|
-
log(colors.bold("COMPLETENESS")
|
|
1958
|
+
log(`${colors.bold("COMPLETENESS")} ${health.completeness.score}%`);
|
|
2471
1959
|
const missingRules = Object.entries(health.completeness.missing);
|
|
2472
1960
|
for (let i = 0;i < missingRules.length; i++) {
|
|
2473
1961
|
const [rule, count] = missingRules[i];
|
|
@@ -2489,7 +1977,7 @@ function displayHealthVerbose(health, log) {
|
|
|
2489
1977
|
}
|
|
2490
1978
|
if (health.examples) {
|
|
2491
1979
|
log("");
|
|
2492
|
-
log(colors.bold("EXAMPLES")
|
|
1980
|
+
log(`${colors.bold("EXAMPLES")} ${health.examples.score}%`);
|
|
2493
1981
|
log(`${tree.branch} ${colors.muted("passed".padEnd(12))} ${health.examples.passed}`);
|
|
2494
1982
|
log(`${tree.corner} ${colors.muted("failed".padEnd(12))} ${health.examples.failed}`);
|
|
2495
1983
|
}
|
|
@@ -2505,7 +1993,6 @@ function displayTextOutput(options, deps) {
|
|
|
2505
1993
|
driftExports,
|
|
2506
1994
|
typecheckErrors,
|
|
2507
1995
|
staleRefs,
|
|
2508
|
-
exampleResult,
|
|
2509
1996
|
specWarnings,
|
|
2510
1997
|
specInfos,
|
|
2511
1998
|
verbose
|
|
@@ -2657,10 +2144,8 @@ function handleNonTextOutput(options, deps) {
|
|
|
2657
2144
|
format,
|
|
2658
2145
|
openpkg,
|
|
2659
2146
|
doccov,
|
|
2660
|
-
coverageScore,
|
|
2661
2147
|
minHealth,
|
|
2662
2148
|
minApiSurface,
|
|
2663
|
-
driftExports,
|
|
2664
2149
|
typecheckErrors,
|
|
2665
2150
|
limit,
|
|
2666
2151
|
stdout,
|
|
@@ -2671,7 +2156,7 @@ function handleNonTextOutput(options, deps) {
|
|
|
2671
2156
|
const stats = computeStats(openpkg, doccov);
|
|
2672
2157
|
const report = generateReportFromDocCov(openpkg, doccov);
|
|
2673
2158
|
const jsonContent = JSON.stringify(report, null, 2);
|
|
2674
|
-
const healthScore = doccov.summary.health?.score ?? coverageScore;
|
|
2159
|
+
const healthScore = doccov.summary.health?.score ?? stats.coverageScore;
|
|
2675
2160
|
let formatContent;
|
|
2676
2161
|
switch (format) {
|
|
2677
2162
|
case "json":
|
|
@@ -2680,15 +2165,6 @@ function handleNonTextOutput(options, deps) {
|
|
|
2680
2165
|
case "markdown":
|
|
2681
2166
|
formatContent = renderMarkdown(stats, { limit });
|
|
2682
2167
|
break;
|
|
2683
|
-
case "html":
|
|
2684
|
-
formatContent = renderHtml(stats, { limit });
|
|
2685
|
-
break;
|
|
2686
|
-
case "github":
|
|
2687
|
-
formatContent = renderGithubSummary(stats, {
|
|
2688
|
-
coverageScore,
|
|
2689
|
-
driftCount: driftExports.length
|
|
2690
|
-
});
|
|
2691
|
-
break;
|
|
2692
2168
|
default:
|
|
2693
2169
|
throw new Error(`Unknown format: ${format}`);
|
|
2694
2170
|
}
|
|
@@ -2838,12 +2314,12 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2838
2314
|
...defaultDependencies,
|
|
2839
2315
|
...dependencies
|
|
2840
2316
|
};
|
|
2841
|
-
program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-health <percentage>", "Minimum health score (0-100)", (value) => Number(value)).option("--
|
|
2317
|
+
program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-health <percentage>", "Minimum health score (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--preview", "Preview fixes with diff output (implies --fix)").option("--format <format>", "Output format: text, json, markdown", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)", (value) => {
|
|
2842
2318
|
const n = parseInt(value, 10);
|
|
2843
2319
|
if (Number.isNaN(n) || n < 1)
|
|
2844
2320
|
throw new Error("--max-type-depth must be a positive integer");
|
|
2845
2321
|
return n;
|
|
2846
|
-
}).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--
|
|
2322
|
+
}).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--api-surface", "Show only API surface / forgotten exports info").option("-v, --verbose", "Show detailed output including forgotten exports").action(async (entry, options) => {
|
|
2847
2323
|
try {
|
|
2848
2324
|
const spin = spinner("Analyzing...");
|
|
2849
2325
|
let validations = parseExamplesFlag(options.examples);
|
|
@@ -2865,27 +2341,11 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2865
2341
|
}
|
|
2866
2342
|
hasExamples = validations.length > 0;
|
|
2867
2343
|
}
|
|
2868
|
-
if (options.minCoverage !== undefined) {
|
|
2869
|
-
log(chalk7.yellow("Warning: --min-coverage is deprecated. Use --min-health instead."));
|
|
2870
|
-
}
|
|
2871
|
-
if (options.maxDrift !== undefined) {
|
|
2872
|
-
log(chalk7.yellow("Warning: --max-drift is deprecated. Use --min-health instead."));
|
|
2873
|
-
}
|
|
2874
|
-
if (config?.check?.minCoverage !== undefined) {
|
|
2875
|
-
log(chalk7.yellow("Warning: config.check.minCoverage is deprecated. Use minHealth."));
|
|
2876
|
-
}
|
|
2877
|
-
if (config?.check?.maxDrift !== undefined) {
|
|
2878
|
-
log(chalk7.yellow("Warning: config.check.maxDrift is deprecated. Use minHealth."));
|
|
2879
|
-
}
|
|
2880
2344
|
const DEFAULT_MIN_HEALTH = 80;
|
|
2881
2345
|
const minHealthRaw = options.minHealth ?? config?.check?.minHealth ?? DEFAULT_MIN_HEALTH;
|
|
2882
2346
|
const minHealth = clampPercentage(minHealthRaw);
|
|
2883
|
-
const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage ?? DEFAULT_MIN_HEALTH;
|
|
2884
|
-
const minCoverage = clampPercentage(minCoverageRaw);
|
|
2885
|
-
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
2886
|
-
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
2887
2347
|
const apiSurfaceConfig = config?.check?.apiSurface;
|
|
2888
|
-
const minApiSurfaceRaw =
|
|
2348
|
+
const minApiSurfaceRaw = apiSurfaceConfig?.minCompleteness;
|
|
2889
2349
|
const minApiSurface = minApiSurfaceRaw !== undefined ? clampPercentage(minApiSurfaceRaw) : undefined;
|
|
2890
2350
|
const warnBelowApiSurface = apiSurfaceConfig?.warnBelow ? clampPercentage(apiSurfaceConfig.warnBelow) : undefined;
|
|
2891
2351
|
const apiSurfaceIgnore = apiSurfaceConfig?.ignore ?? [];
|
|
@@ -2924,7 +2384,6 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2924
2384
|
const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
|
|
2925
2385
|
const isPreview = options.preview;
|
|
2926
2386
|
const shouldFix = options.fix || isPreview;
|
|
2927
|
-
let exampleResult;
|
|
2928
2387
|
let typecheckErrors = [];
|
|
2929
2388
|
let runtimeDrifts = [];
|
|
2930
2389
|
if (hasExamples) {
|
|
@@ -2932,7 +2391,6 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2932
2391
|
validations,
|
|
2933
2392
|
targetDir
|
|
2934
2393
|
});
|
|
2935
|
-
exampleResult = validation.result;
|
|
2936
2394
|
typecheckErrors = validation.typecheckErrors;
|
|
2937
2395
|
runtimeDrifts = validation.runtimeDrifts;
|
|
2938
2396
|
}
|
|
@@ -2972,10 +2430,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2972
2430
|
format,
|
|
2973
2431
|
openpkg,
|
|
2974
2432
|
doccov,
|
|
2975
|
-
coverageScore,
|
|
2976
2433
|
minHealth,
|
|
2977
2434
|
minApiSurface,
|
|
2978
|
-
driftExports,
|
|
2979
2435
|
typecheckErrors,
|
|
2980
2436
|
limit: parseInt(options.limit, 10) || 20,
|
|
2981
2437
|
stdout: options.stdout,
|
|
@@ -2997,7 +2453,6 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2997
2453
|
driftExports,
|
|
2998
2454
|
typecheckErrors,
|
|
2999
2455
|
staleRefs,
|
|
3000
|
-
exampleResult,
|
|
3001
2456
|
specWarnings,
|
|
3002
2457
|
specInfos,
|
|
3003
2458
|
verbose: options.verbose ?? false
|
|
@@ -3050,7 +2505,7 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
3050
2505
|
...defaultDependencies2,
|
|
3051
2506
|
...dependencies
|
|
3052
2507
|
};
|
|
3053
|
-
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
|
|
2508
|
+
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", collect2, []).option("--no-cache", "Bypass cache and force regeneration").option("--recommend-version", "Output recommended semver version bump").action(async (baseArg, headArg, options) => {
|
|
3054
2509
|
try {
|
|
3055
2510
|
const baseFile = options.base ?? baseArg;
|
|
3056
2511
|
const headFile = options.head ?? headArg;
|
|
@@ -3183,43 +2638,6 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
3183
2638
|
case "github":
|
|
3184
2639
|
printGitHubAnnotations(diff, log);
|
|
3185
2640
|
break;
|
|
3186
|
-
case "pr-comment": {
|
|
3187
|
-
const semverRecommendation = recommendSemverBump(diff);
|
|
3188
|
-
const content = renderPRComment({ diff, baseName, headName, headSpec }, {
|
|
3189
|
-
repoUrl: options.repoUrl,
|
|
3190
|
-
sha: options.sha,
|
|
3191
|
-
minCoverage,
|
|
3192
|
-
limit,
|
|
3193
|
-
semverBump: {
|
|
3194
|
-
bump: semverRecommendation.bump,
|
|
3195
|
-
reason: semverRecommendation.reason
|
|
3196
|
-
}
|
|
3197
|
-
});
|
|
3198
|
-
log(content);
|
|
3199
|
-
break;
|
|
3200
|
-
}
|
|
3201
|
-
case "changelog": {
|
|
3202
|
-
const content = renderChangelog({
|
|
3203
|
-
diff,
|
|
3204
|
-
categorizedBreaking: diff.categorizedBreaking,
|
|
3205
|
-
version: headSpec.meta?.version
|
|
3206
|
-
}, {
|
|
3207
|
-
version: headSpec.meta?.version,
|
|
3208
|
-
compareUrl: options.repoUrl ? `${options.repoUrl}/compare/${baseSpec.meta?.version ?? "v0"}...${headSpec.meta?.version ?? "HEAD"}` : undefined
|
|
3209
|
-
});
|
|
3210
|
-
if (options.stdout) {
|
|
3211
|
-
log(content);
|
|
3212
|
-
} else {
|
|
3213
|
-
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "md");
|
|
3214
|
-
writeReport({
|
|
3215
|
-
format: "markdown",
|
|
3216
|
-
content,
|
|
3217
|
-
outputPath: outputPath.replace(/\.(json|html)$/, ".changelog.md"),
|
|
3218
|
-
cwd: options.cwd
|
|
3219
|
-
});
|
|
3220
|
-
}
|
|
3221
|
-
break;
|
|
3222
|
-
}
|
|
3223
2641
|
}
|
|
3224
2642
|
const failures = validateDiff(diff, headSpec, {
|
|
3225
2643
|
minCoverage,
|
|
@@ -3398,57 +2816,10 @@ function printGitHubAnnotations(diff, log) {
|
|
|
3398
2816
|
}
|
|
3399
2817
|
}
|
|
3400
2818
|
|
|
3401
|
-
// src/commands/info.ts
|
|
3402
|
-
import { buildDocCovSpec as buildDocCovSpec2, DocCov as DocCov2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
3403
|
-
import chalk9 from "chalk";
|
|
3404
|
-
function registerInfoCommand(program) {
|
|
3405
|
-
program.command("info [entry]").description("Show brief documentation coverage summary").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution from node_modules").action(async (entry, options) => {
|
|
3406
|
-
const spin = spinner("Analyzing documentation coverage");
|
|
3407
|
-
try {
|
|
3408
|
-
const fileSystem = new NodeFileSystem2(options.cwd);
|
|
3409
|
-
const resolved = await resolveTarget2(fileSystem, {
|
|
3410
|
-
cwd: options.cwd,
|
|
3411
|
-
package: options.package,
|
|
3412
|
-
entry
|
|
3413
|
-
});
|
|
3414
|
-
const { entryFile, targetDir } = resolved;
|
|
3415
|
-
const resolveExternalTypes = !options.skipResolve;
|
|
3416
|
-
const analyzer = new DocCov2({
|
|
3417
|
-
resolveExternalTypes
|
|
3418
|
-
});
|
|
3419
|
-
const specResult = await analyzer.analyzeFileWithDiagnostics(entryFile);
|
|
3420
|
-
if (!specResult) {
|
|
3421
|
-
spin.fail("Failed to analyze");
|
|
3422
|
-
throw new Error("Failed to analyze documentation coverage.");
|
|
3423
|
-
}
|
|
3424
|
-
const openpkg = specResult.spec;
|
|
3425
|
-
const doccov = buildDocCovSpec2({
|
|
3426
|
-
openpkg,
|
|
3427
|
-
openpkgPath: entryFile,
|
|
3428
|
-
packagePath: targetDir,
|
|
3429
|
-
forgottenExports: specResult.forgottenExports
|
|
3430
|
-
});
|
|
3431
|
-
const stats = computeStats(openpkg, doccov);
|
|
3432
|
-
spin.success("Analysis complete");
|
|
3433
|
-
console.log("");
|
|
3434
|
-
console.log(chalk9.bold(`${stats.packageName}@${stats.version}`));
|
|
3435
|
-
console.log("");
|
|
3436
|
-
console.log(` Exports: ${chalk9.bold(stats.totalExports.toString())}`);
|
|
3437
|
-
console.log(` Coverage: ${chalk9.bold(`${stats.coverageScore}%`)}`);
|
|
3438
|
-
console.log(` Drift: ${chalk9.bold(`${stats.driftScore}%`)}`);
|
|
3439
|
-
console.log("");
|
|
3440
|
-
} catch (err) {
|
|
3441
|
-
spin.fail("Analysis failed");
|
|
3442
|
-
console.error(chalk9.red("Error:"), err instanceof Error ? err.message : err);
|
|
3443
|
-
process.exit(1);
|
|
3444
|
-
}
|
|
3445
|
-
});
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
2819
|
// src/commands/init.ts
|
|
3449
2820
|
import * as fs5 from "node:fs";
|
|
3450
2821
|
import * as path7 from "node:path";
|
|
3451
|
-
import
|
|
2822
|
+
import chalk9 from "chalk";
|
|
3452
2823
|
var defaultDependencies3 = {
|
|
3453
2824
|
fileExists: fs5.existsSync,
|
|
3454
2825
|
writeFileSync: fs5.writeFileSync,
|
|
@@ -3466,7 +2837,7 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
3466
2837
|
const cwd = path7.resolve(options.cwd);
|
|
3467
2838
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
3468
2839
|
if (existing) {
|
|
3469
|
-
error(
|
|
2840
|
+
error(chalk9.red(`A DocCov config already exists at ${path7.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
3470
2841
|
process.exitCode = 1;
|
|
3471
2842
|
return;
|
|
3472
2843
|
}
|
|
@@ -3475,7 +2846,7 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
3475
2846
|
const fileName = `doccov.config.${targetFormat}`;
|
|
3476
2847
|
const outputPath = path7.join(cwd, fileName);
|
|
3477
2848
|
if (fileExists2(outputPath)) {
|
|
3478
|
-
error(
|
|
2849
|
+
error(chalk9.red(`Cannot create ${fileName}; file already exists.`));
|
|
3479
2850
|
process.exitCode = 1;
|
|
3480
2851
|
return;
|
|
3481
2852
|
}
|
|
@@ -3496,16 +2867,16 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
3496
2867
|
}
|
|
3497
2868
|
const repoInfo = detectRepoInfo(cwd, fileExists2, readFileSync4);
|
|
3498
2869
|
log("");
|
|
3499
|
-
log(
|
|
2870
|
+
log(chalk9.bold("Add this badge to your README:"));
|
|
3500
2871
|
log("");
|
|
3501
2872
|
if (repoInfo) {
|
|
3502
|
-
log(
|
|
2873
|
+
log(chalk9.cyan(`[](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
|
|
3503
2874
|
} else {
|
|
3504
|
-
log(
|
|
3505
|
-
log(
|
|
2875
|
+
log(chalk9.cyan(`[](https://doccov.dev/OWNER/REPO)`));
|
|
2876
|
+
log(chalk9.dim(" Replace OWNER/REPO with your GitHub repo"));
|
|
3506
2877
|
}
|
|
3507
2878
|
log("");
|
|
3508
|
-
log(
|
|
2879
|
+
log(chalk9.dim("Run `doccov check` to verify your documentation coverage"));
|
|
3509
2880
|
});
|
|
3510
2881
|
}
|
|
3511
2882
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
@@ -3632,21 +3003,21 @@ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
|
|
|
3632
3003
|
import * as fs6 from "node:fs";
|
|
3633
3004
|
import * as path8 from "node:path";
|
|
3634
3005
|
import {
|
|
3635
|
-
buildDocCovSpec as
|
|
3636
|
-
DocCov as
|
|
3006
|
+
buildDocCovSpec as buildDocCovSpec2,
|
|
3007
|
+
DocCov as DocCov2,
|
|
3637
3008
|
detectPackageManager,
|
|
3638
|
-
NodeFileSystem as
|
|
3009
|
+
NodeFileSystem as NodeFileSystem2,
|
|
3639
3010
|
renderApiSurface,
|
|
3640
|
-
resolveTarget as
|
|
3011
|
+
resolveTarget as resolveTarget2
|
|
3641
3012
|
} from "@doccov/sdk";
|
|
3642
3013
|
import { validateDocCovSpec } from "@doccov/spec";
|
|
3643
3014
|
import {
|
|
3644
3015
|
normalize,
|
|
3645
3016
|
validateSpec
|
|
3646
3017
|
} from "@openpkg-ts/spec";
|
|
3647
|
-
import
|
|
3018
|
+
import chalk10 from "chalk";
|
|
3648
3019
|
var defaultDependencies4 = {
|
|
3649
|
-
createDocCov: (options) => new
|
|
3020
|
+
createDocCov: (options) => new DocCov2(options),
|
|
3650
3021
|
writeFileSync: fs6.writeFileSync,
|
|
3651
3022
|
log: console.log,
|
|
3652
3023
|
error: console.error
|
|
@@ -3657,7 +3028,7 @@ function getArrayLength(value) {
|
|
|
3657
3028
|
function formatDiagnosticOutput(prefix2, diagnostic, baseDir) {
|
|
3658
3029
|
const location = diagnostic.location;
|
|
3659
3030
|
const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
|
|
3660
|
-
const locationText = location && relativePath ?
|
|
3031
|
+
const locationText = location && relativePath ? chalk10.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
3661
3032
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
3662
3033
|
return `${prefix2} ${locationPrefix}${diagnostic.message}`;
|
|
3663
3034
|
}
|
|
@@ -3669,19 +3040,19 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3669
3040
|
program.command("spec [entry]").description("Generate OpenPkg + DocCov specifications").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <dir>", "Output directory", ".doccov").option("-f, --format <format>", "Output format: json (default) or api-surface", "json").option("--openpkg-only", "Only generate openpkg.json (skip coverage analysis)").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--runtime", "Enable Standard Schema runtime extraction (richer output for Zod, Valibot, etc.)").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
|
|
3670
3041
|
try {
|
|
3671
3042
|
const spin = spinner("Generating spec...");
|
|
3672
|
-
const fileSystem = new
|
|
3673
|
-
const resolved = await
|
|
3043
|
+
const fileSystem = new NodeFileSystem2(options.cwd);
|
|
3044
|
+
const resolved = await resolveTarget2(fileSystem, {
|
|
3674
3045
|
cwd: options.cwd,
|
|
3675
3046
|
package: options.package,
|
|
3676
3047
|
entry
|
|
3677
3048
|
});
|
|
3678
|
-
const { targetDir, entryFile
|
|
3049
|
+
const { targetDir, entryFile } = resolved;
|
|
3679
3050
|
let config = null;
|
|
3680
3051
|
try {
|
|
3681
3052
|
config = await loadDocCovConfig(targetDir);
|
|
3682
3053
|
} catch (configError) {
|
|
3683
3054
|
spin.fail("Failed to load config");
|
|
3684
|
-
error(
|
|
3055
|
+
error(chalk10.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
3685
3056
|
process.exit(1);
|
|
3686
3057
|
}
|
|
3687
3058
|
const cliFilters = {
|
|
@@ -3714,15 +3085,15 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3714
3085
|
const validation = validateSpec(normalized);
|
|
3715
3086
|
if (!validation.ok) {
|
|
3716
3087
|
spin.fail("Validation failed");
|
|
3717
|
-
error(
|
|
3088
|
+
error(chalk10.red("Spec failed schema validation"));
|
|
3718
3089
|
for (const err of validation.errors) {
|
|
3719
|
-
error(
|
|
3090
|
+
error(chalk10.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
3720
3091
|
}
|
|
3721
3092
|
process.exit(1);
|
|
3722
3093
|
}
|
|
3723
3094
|
let doccovSpec = null;
|
|
3724
3095
|
if (!options.openpkgOnly) {
|
|
3725
|
-
doccovSpec =
|
|
3096
|
+
doccovSpec = buildDocCovSpec2({
|
|
3726
3097
|
openpkgPath: "openpkg.json",
|
|
3727
3098
|
openpkg: normalized,
|
|
3728
3099
|
packagePath: targetDir,
|
|
@@ -3731,9 +3102,9 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3731
3102
|
const doccovValidation = validateDocCovSpec(doccovSpec);
|
|
3732
3103
|
if (!doccovValidation.ok) {
|
|
3733
3104
|
spin.fail("DocCov validation failed");
|
|
3734
|
-
error(
|
|
3105
|
+
error(chalk10.red("DocCov spec failed schema validation"));
|
|
3735
3106
|
for (const err of doccovValidation.errors) {
|
|
3736
|
-
error(
|
|
3107
|
+
error(chalk10.red(`doccov: ${err.instancePath || "/"} ${err.message}`));
|
|
3737
3108
|
}
|
|
3738
3109
|
process.exit(1);
|
|
3739
3110
|
}
|
|
@@ -3753,12 +3124,12 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3753
3124
|
const doccovPath = path8.join(outputDir, "doccov.json");
|
|
3754
3125
|
writeFileSync4(doccovPath, JSON.stringify(doccovSpec, null, 2));
|
|
3755
3126
|
spin.success(`Generated ${options.output}/`);
|
|
3756
|
-
log(
|
|
3757
|
-
log(
|
|
3127
|
+
log(chalk10.gray(` openpkg.json: ${getArrayLength(normalized.exports)} exports`));
|
|
3128
|
+
log(chalk10.gray(` doccov.json: ${doccovSpec.summary.score}% coverage, ${doccovSpec.summary.drift.total} drift issues`));
|
|
3758
3129
|
} else {
|
|
3759
3130
|
spin.success(`Generated ${options.output}/openpkg.json`);
|
|
3760
|
-
log(
|
|
3761
|
-
log(
|
|
3131
|
+
log(chalk10.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
3132
|
+
log(chalk10.gray(` ${getArrayLength(normalized.types)} types`));
|
|
3762
3133
|
}
|
|
3763
3134
|
}
|
|
3764
3135
|
const isFullGenerationInfo = (gen) => {
|
|
@@ -3770,62 +3141,62 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3770
3141
|
const pm = await detectPackageManager(fileSystem);
|
|
3771
3142
|
const buildCmd = pm.name === "npm" ? "npm run build" : `${pm.name} run build`;
|
|
3772
3143
|
log("");
|
|
3773
|
-
log(
|
|
3774
|
-
log(
|
|
3144
|
+
log(chalk10.yellow("⚠ Runtime extraction requested but no schemas extracted."));
|
|
3145
|
+
log(chalk10.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
|
|
3775
3146
|
}
|
|
3776
3147
|
if (options.verbose && fullGen) {
|
|
3777
3148
|
log("");
|
|
3778
|
-
log(
|
|
3779
|
-
log(
|
|
3780
|
-
log(
|
|
3781
|
-
log(
|
|
3782
|
-
log(
|
|
3783
|
-
log(
|
|
3784
|
-
log(
|
|
3149
|
+
log(chalk10.bold("Generation Info"));
|
|
3150
|
+
log(chalk10.gray(` Timestamp: ${fullGen.timestamp}`));
|
|
3151
|
+
log(chalk10.gray(` Generator: ${fullGen.generator.name}@${fullGen.generator.version}`));
|
|
3152
|
+
log(chalk10.gray(` Entry point: ${fullGen.analysis.entryPoint}`));
|
|
3153
|
+
log(chalk10.gray(` Detected via: ${fullGen.analysis.entryPointSource}`));
|
|
3154
|
+
log(chalk10.gray(` Declaration only: ${fullGen.analysis.isDeclarationOnly ? "yes" : "no"}`));
|
|
3155
|
+
log(chalk10.gray(` External types: ${fullGen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
|
|
3785
3156
|
if (fullGen.analysis.maxTypeDepth) {
|
|
3786
|
-
log(
|
|
3157
|
+
log(chalk10.gray(` Max type depth: ${fullGen.analysis.maxTypeDepth}`));
|
|
3787
3158
|
}
|
|
3788
3159
|
if (fullGen.analysis.schemaExtraction) {
|
|
3789
3160
|
const se = fullGen.analysis.schemaExtraction;
|
|
3790
|
-
log(
|
|
3161
|
+
log(chalk10.gray(` Schema extraction: ${se.method}`));
|
|
3791
3162
|
if (se.runtimeCount) {
|
|
3792
|
-
log(
|
|
3163
|
+
log(chalk10.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
|
|
3793
3164
|
}
|
|
3794
3165
|
}
|
|
3795
3166
|
log("");
|
|
3796
|
-
log(
|
|
3797
|
-
log(
|
|
3167
|
+
log(chalk10.bold("Environment"));
|
|
3168
|
+
log(chalk10.gray(` node_modules: ${fullGen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
3798
3169
|
if (fullGen.environment.packageManager) {
|
|
3799
|
-
log(
|
|
3170
|
+
log(chalk10.gray(` Package manager: ${fullGen.environment.packageManager}`));
|
|
3800
3171
|
}
|
|
3801
3172
|
if (fullGen.environment.isMonorepo) {
|
|
3802
|
-
log(
|
|
3173
|
+
log(chalk10.gray(` Monorepo: yes`));
|
|
3803
3174
|
}
|
|
3804
3175
|
if (fullGen.environment.targetPackage) {
|
|
3805
|
-
log(
|
|
3176
|
+
log(chalk10.gray(` Target package: ${fullGen.environment.targetPackage}`));
|
|
3806
3177
|
}
|
|
3807
3178
|
if (fullGen.issues.length > 0) {
|
|
3808
3179
|
log("");
|
|
3809
|
-
log(
|
|
3180
|
+
log(chalk10.bold("Issues"));
|
|
3810
3181
|
for (const issue of fullGen.issues) {
|
|
3811
|
-
const prefix2 = issue.severity === "error" ?
|
|
3182
|
+
const prefix2 = issue.severity === "error" ? chalk10.red(">") : issue.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
|
|
3812
3183
|
log(`${prefix2} [${issue.code}] ${issue.message}`);
|
|
3813
3184
|
if (issue.suggestion) {
|
|
3814
|
-
log(
|
|
3185
|
+
log(chalk10.gray(` ${issue.suggestion}`));
|
|
3815
3186
|
}
|
|
3816
3187
|
}
|
|
3817
3188
|
}
|
|
3818
3189
|
}
|
|
3819
3190
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
3820
3191
|
log("");
|
|
3821
|
-
log(
|
|
3192
|
+
log(chalk10.bold("Diagnostics"));
|
|
3822
3193
|
for (const diagnostic of result.diagnostics) {
|
|
3823
|
-
const prefix2 = diagnostic.severity === "error" ?
|
|
3194
|
+
const prefix2 = diagnostic.severity === "error" ? chalk10.red(">") : diagnostic.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
|
|
3824
3195
|
log(formatDiagnosticOutput(prefix2, diagnostic, targetDir));
|
|
3825
3196
|
}
|
|
3826
3197
|
}
|
|
3827
3198
|
} catch (commandError) {
|
|
3828
|
-
error(
|
|
3199
|
+
error(chalk10.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
3829
3200
|
process.exit(1);
|
|
3830
3201
|
}
|
|
3831
3202
|
});
|
|
@@ -3839,13 +3210,11 @@ import {
|
|
|
3839
3210
|
getExtendedTrend,
|
|
3840
3211
|
getTrend,
|
|
3841
3212
|
loadSnapshots,
|
|
3842
|
-
pruneByTier,
|
|
3843
3213
|
pruneHistory,
|
|
3844
|
-
RETENTION_DAYS,
|
|
3845
3214
|
renderSparkline,
|
|
3846
3215
|
saveSnapshot
|
|
3847
3216
|
} from "@doccov/sdk";
|
|
3848
|
-
import
|
|
3217
|
+
import chalk11 from "chalk";
|
|
3849
3218
|
function formatDate(timestamp) {
|
|
3850
3219
|
const date = new Date(timestamp);
|
|
3851
3220
|
return date.toLocaleDateString("en-US", {
|
|
@@ -3858,19 +3227,19 @@ function formatDate(timestamp) {
|
|
|
3858
3227
|
}
|
|
3859
3228
|
function getColorForScore(score) {
|
|
3860
3229
|
if (score >= 90)
|
|
3861
|
-
return
|
|
3230
|
+
return chalk11.green;
|
|
3862
3231
|
if (score >= 70)
|
|
3863
|
-
return
|
|
3232
|
+
return chalk11.yellow;
|
|
3864
3233
|
if (score >= 50)
|
|
3865
|
-
return
|
|
3866
|
-
return
|
|
3234
|
+
return chalk11.hex("#FFA500");
|
|
3235
|
+
return chalk11.red;
|
|
3867
3236
|
}
|
|
3868
3237
|
function formatSnapshot(snapshot) {
|
|
3869
3238
|
const color = getColorForScore(snapshot.coverageScore);
|
|
3870
3239
|
const date = formatDate(snapshot.timestamp);
|
|
3871
3240
|
const version = snapshot.version ? ` v${snapshot.version}` : "";
|
|
3872
|
-
const commit = snapshot.commit ?
|
|
3873
|
-
return `${
|
|
3241
|
+
const commit = snapshot.commit ? chalk11.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
|
|
3242
|
+
return `${chalk11.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version}${commit}`;
|
|
3874
3243
|
}
|
|
3875
3244
|
function formatWeekDate(timestamp) {
|
|
3876
3245
|
const date = new Date(timestamp);
|
|
@@ -3878,30 +3247,26 @@ function formatWeekDate(timestamp) {
|
|
|
3878
3247
|
}
|
|
3879
3248
|
function formatVelocity(velocity) {
|
|
3880
3249
|
if (velocity > 0)
|
|
3881
|
-
return
|
|
3250
|
+
return chalk11.green(`+${velocity}%/day`);
|
|
3882
3251
|
if (velocity < 0)
|
|
3883
|
-
return
|
|
3884
|
-
return
|
|
3252
|
+
return chalk11.red(`${velocity}%/day`);
|
|
3253
|
+
return chalk11.gray("0%/day");
|
|
3885
3254
|
}
|
|
3886
3255
|
function registerTrendsCommand(program) {
|
|
3887
|
-
program.command("trends").description("Show coverage trends over time").option("--cwd <dir>", "Working directory", process.cwd()).option("-n, --limit <count>", "Number of snapshots to show", "10").option("--prune <count>", "Prune history to keep only N snapshots").option("--record", "Record current coverage to history").option("--json", "Output as JSON").option("--extended", "Show extended trend analysis (velocity, projections)").option("--
|
|
3256
|
+
program.command("trends").description("Show coverage trends over time").option("--cwd <dir>", "Working directory", process.cwd()).option("-n, --limit <count>", "Number of snapshots to show", "10").option("--prune <count>", "Prune history to keep only N snapshots").option("--record", "Record current coverage to history").option("--json", "Output as JSON").option("--extended", "Show extended trend analysis (velocity, projections)").option("--weekly", "Show weekly summary breakdown").action(async (options) => {
|
|
3888
3257
|
const cwd = path9.resolve(options.cwd);
|
|
3889
|
-
const tier = options.tier ?? "pro";
|
|
3890
3258
|
if (options.prune) {
|
|
3891
3259
|
const keepCount = parseInt(options.prune, 10);
|
|
3892
3260
|
if (!Number.isNaN(keepCount)) {
|
|
3893
3261
|
const deleted = pruneHistory(cwd, keepCount);
|
|
3894
|
-
console.log(
|
|
3895
|
-
} else {
|
|
3896
|
-
const deleted = pruneByTier(cwd, tier);
|
|
3897
|
-
console.log(chalk12.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
|
|
3262
|
+
console.log(chalk11.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
|
|
3898
3263
|
}
|
|
3899
3264
|
return;
|
|
3900
3265
|
}
|
|
3901
3266
|
if (options.record) {
|
|
3902
3267
|
const specPath = path9.resolve(cwd, "openpkg.json");
|
|
3903
3268
|
if (!fs7.existsSync(specPath)) {
|
|
3904
|
-
console.error(
|
|
3269
|
+
console.error(chalk11.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
|
|
3905
3270
|
process.exit(1);
|
|
3906
3271
|
}
|
|
3907
3272
|
const spin = spinner("Recording coverage snapshot");
|
|
@@ -3914,21 +3279,21 @@ function registerTrendsCommand(program) {
|
|
|
3914
3279
|
console.log(formatSnapshot(trend.current));
|
|
3915
3280
|
if (trend.delta !== undefined) {
|
|
3916
3281
|
const deltaStr = formatDelta2(trend.delta);
|
|
3917
|
-
const deltaColor = trend.delta > 0 ?
|
|
3918
|
-
console.log(
|
|
3282
|
+
const deltaColor = trend.delta > 0 ? chalk11.green : trend.delta < 0 ? chalk11.red : chalk11.gray;
|
|
3283
|
+
console.log(chalk11.gray("Change from previous:"), deltaColor(deltaStr));
|
|
3919
3284
|
}
|
|
3920
3285
|
return;
|
|
3921
3286
|
} catch (error) {
|
|
3922
3287
|
spin.fail("Failed to record snapshot");
|
|
3923
|
-
console.error(
|
|
3288
|
+
console.error(chalk11.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
|
|
3924
3289
|
process.exit(1);
|
|
3925
3290
|
}
|
|
3926
3291
|
}
|
|
3927
3292
|
const snapshots = loadSnapshots(cwd);
|
|
3928
3293
|
const limit = parseInt(options.limit ?? "10", 10);
|
|
3929
3294
|
if (snapshots.length === 0) {
|
|
3930
|
-
console.log(
|
|
3931
|
-
console.log(
|
|
3295
|
+
console.log(chalk11.yellow("No coverage history found."));
|
|
3296
|
+
console.log(chalk11.gray("Run `doccov trends --record` to save the current coverage."));
|
|
3932
3297
|
return;
|
|
3933
3298
|
}
|
|
3934
3299
|
if (options.json) {
|
|
@@ -3943,17 +3308,17 @@ function registerTrendsCommand(program) {
|
|
|
3943
3308
|
}
|
|
3944
3309
|
const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
|
|
3945
3310
|
const sparkline = renderSparkline(sparklineData);
|
|
3946
|
-
console.log(
|
|
3947
|
-
console.log(
|
|
3948
|
-
console.log(
|
|
3311
|
+
console.log(chalk11.bold("Coverage Trends"));
|
|
3312
|
+
console.log(chalk11.gray(`Package: ${snapshots[0].package}`));
|
|
3313
|
+
console.log(chalk11.gray(`Sparkline: ${sparkline}`));
|
|
3949
3314
|
console.log("");
|
|
3950
3315
|
if (snapshots.length >= 2) {
|
|
3951
3316
|
const oldest = snapshots[snapshots.length - 1];
|
|
3952
3317
|
const newest = snapshots[0];
|
|
3953
3318
|
const overallDelta = newest.coverageScore - oldest.coverageScore;
|
|
3954
3319
|
const deltaStr = formatDelta2(overallDelta);
|
|
3955
|
-
const deltaColor = overallDelta > 0 ?
|
|
3956
|
-
console.log(
|
|
3320
|
+
const deltaColor = overallDelta > 0 ? chalk11.green : overallDelta < 0 ? chalk11.red : chalk11.gray;
|
|
3321
|
+
console.log(chalk11.gray("Overall trend:"), deltaColor(deltaStr), chalk11.gray(`(${snapshots.length} snapshots)`));
|
|
3957
3322
|
console.log("");
|
|
3958
3323
|
}
|
|
3959
3324
|
if (options.extended) {
|
|
@@ -3962,58 +3327,56 @@ function registerTrendsCommand(program) {
|
|
|
3962
3327
|
try {
|
|
3963
3328
|
const specContent = fs7.readFileSync(specPath, "utf-8");
|
|
3964
3329
|
const spec = JSON.parse(specContent);
|
|
3965
|
-
const extended = getExtendedTrend(spec, cwd
|
|
3966
|
-
console.log(
|
|
3967
|
-
console.log(
|
|
3330
|
+
const extended = getExtendedTrend(spec, cwd);
|
|
3331
|
+
console.log(chalk11.bold("Extended Analysis"));
|
|
3332
|
+
console.log(chalk11.gray("90-day retention"));
|
|
3968
3333
|
console.log("");
|
|
3969
3334
|
console.log(" Velocity:");
|
|
3970
3335
|
console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
|
|
3971
3336
|
console.log(` 30-day: ${formatVelocity(extended.velocity30d)}`);
|
|
3972
|
-
|
|
3973
|
-
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
3974
|
-
}
|
|
3337
|
+
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
3975
3338
|
console.log("");
|
|
3976
|
-
const projColor = extended.projected30d >= extended.trend.current.coverageScore ?
|
|
3339
|
+
const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk11.green : chalk11.red;
|
|
3977
3340
|
console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
|
|
3978
|
-
console.log(` All-time high: ${
|
|
3979
|
-
console.log(` All-time low: ${
|
|
3341
|
+
console.log(` All-time high: ${chalk11.green(`${extended.allTimeHigh}%`)}`);
|
|
3342
|
+
console.log(` All-time low: ${chalk11.red(`${extended.allTimeLow}%`)}`);
|
|
3980
3343
|
if (extended.dataRange) {
|
|
3981
3344
|
const startDate = formatWeekDate(extended.dataRange.start);
|
|
3982
3345
|
const endDate = formatWeekDate(extended.dataRange.end);
|
|
3983
|
-
console.log(
|
|
3346
|
+
console.log(chalk11.gray(` Data range: ${startDate} - ${endDate}`));
|
|
3984
3347
|
}
|
|
3985
3348
|
console.log("");
|
|
3986
3349
|
if (options.weekly && extended.weeklySummaries.length > 0) {
|
|
3987
|
-
console.log(
|
|
3350
|
+
console.log(chalk11.bold("Weekly Summary"));
|
|
3988
3351
|
const weekLimit = Math.min(extended.weeklySummaries.length, 8);
|
|
3989
3352
|
for (let i = 0;i < weekLimit; i++) {
|
|
3990
3353
|
const week = extended.weeklySummaries[i];
|
|
3991
3354
|
const weekStart = formatWeekDate(week.weekStart);
|
|
3992
3355
|
const weekEnd = formatWeekDate(week.weekEnd);
|
|
3993
|
-
const deltaColor = week.delta > 0 ?
|
|
3356
|
+
const deltaColor = week.delta > 0 ? chalk11.green : week.delta < 0 ? chalk11.red : chalk11.gray;
|
|
3994
3357
|
const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
|
|
3995
3358
|
console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
|
|
3996
3359
|
}
|
|
3997
3360
|
if (extended.weeklySummaries.length > weekLimit) {
|
|
3998
|
-
console.log(
|
|
3361
|
+
console.log(chalk11.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
|
|
3999
3362
|
}
|
|
4000
3363
|
console.log("");
|
|
4001
3364
|
}
|
|
4002
3365
|
} catch {
|
|
4003
|
-
console.log(
|
|
3366
|
+
console.log(chalk11.yellow("Could not load openpkg.json for extended analysis"));
|
|
4004
3367
|
console.log("");
|
|
4005
3368
|
}
|
|
4006
3369
|
}
|
|
4007
3370
|
}
|
|
4008
|
-
console.log(
|
|
3371
|
+
console.log(chalk11.bold("History"));
|
|
4009
3372
|
const displaySnapshots = snapshots.slice(0, limit);
|
|
4010
3373
|
for (let i = 0;i < displaySnapshots.length; i++) {
|
|
4011
3374
|
const snapshot = displaySnapshots[i];
|
|
4012
|
-
const prefix2 = i === 0 ?
|
|
3375
|
+
const prefix2 = i === 0 ? chalk11.cyan("→") : " ";
|
|
4013
3376
|
console.log(`${prefix2} ${formatSnapshot(snapshot)}`);
|
|
4014
3377
|
}
|
|
4015
3378
|
if (snapshots.length > limit) {
|
|
4016
|
-
console.log(
|
|
3379
|
+
console.log(chalk11.gray(` ... and ${snapshots.length - limit} more`));
|
|
4017
3380
|
}
|
|
4018
3381
|
});
|
|
4019
3382
|
}
|
|
@@ -4025,7 +3388,6 @@ var packageJson = JSON.parse(readFileSync5(path10.join(__dirname2, "../package.j
|
|
|
4025
3388
|
var program = new Command;
|
|
4026
3389
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
4027
3390
|
registerCheckCommand(program);
|
|
4028
|
-
registerInfoCommand(program);
|
|
4029
3391
|
registerSpecCommand(program);
|
|
4030
3392
|
registerDiffCommand(program);
|
|
4031
3393
|
registerInitCommand(program);
|