@doccov/cli 0.17.0 → 0.19.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 +759 -445
- package/dist/config/index.d.ts +13 -22
- package/dist/config/index.js +4 -26
- package/package.json +3 -3
- package/dist/cli.d.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config/doccov-config.ts
|
|
4
|
-
import { access
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { parse as parseYaml } from "yaml";
|
|
8
7
|
|
|
9
8
|
// src/config/schema.ts
|
|
10
9
|
import { z } from "zod";
|
|
@@ -16,7 +15,6 @@ var docsConfigSchema = z.object({
|
|
|
16
15
|
include: stringList.optional(),
|
|
17
16
|
exclude: stringList.optional()
|
|
18
17
|
});
|
|
19
|
-
var severitySchema = z.enum(["error", "warn", "off"]);
|
|
20
18
|
var exampleModeSchema = z.enum([
|
|
21
19
|
"presence",
|
|
22
20
|
"typecheck",
|
|
@@ -32,16 +30,12 @@ var checkConfigSchema = z.object({
|
|
|
32
30
|
minCoverage: z.number().min(0).max(100).optional(),
|
|
33
31
|
maxDrift: z.number().min(0).max(100).optional()
|
|
34
32
|
});
|
|
35
|
-
var qualityConfigSchema = z.object({
|
|
36
|
-
rules: z.record(severitySchema).optional()
|
|
37
|
-
});
|
|
38
33
|
var docCovConfigSchema = z.object({
|
|
39
34
|
include: stringList.optional(),
|
|
40
35
|
exclude: stringList.optional(),
|
|
41
36
|
plugins: z.array(z.unknown()).optional(),
|
|
42
37
|
docs: docsConfigSchema.optional(),
|
|
43
|
-
check: checkConfigSchema.optional()
|
|
44
|
-
quality: qualityConfigSchema.optional()
|
|
38
|
+
check: checkConfigSchema.optional()
|
|
45
39
|
});
|
|
46
40
|
var normalizeList = (value) => {
|
|
47
41
|
if (!value) {
|
|
@@ -73,19 +67,12 @@ var normalizeConfig = (input) => {
|
|
|
73
67
|
maxDrift: input.check.maxDrift
|
|
74
68
|
};
|
|
75
69
|
}
|
|
76
|
-
let quality;
|
|
77
|
-
if (input.quality) {
|
|
78
|
-
quality = {
|
|
79
|
-
rules: input.quality.rules
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
70
|
return {
|
|
83
71
|
include,
|
|
84
72
|
exclude,
|
|
85
73
|
plugins: input.plugins,
|
|
86
74
|
docs,
|
|
87
|
-
check
|
|
88
|
-
quality
|
|
75
|
+
check
|
|
89
76
|
};
|
|
90
77
|
};
|
|
91
78
|
|
|
@@ -93,12 +80,8 @@ var normalizeConfig = (input) => {
|
|
|
93
80
|
var DOCCOV_CONFIG_FILENAMES = [
|
|
94
81
|
"doccov.config.ts",
|
|
95
82
|
"doccov.config.mts",
|
|
96
|
-
"doccov.config.cts",
|
|
97
83
|
"doccov.config.js",
|
|
98
|
-
"doccov.config.mjs"
|
|
99
|
-
"doccov.config.cjs",
|
|
100
|
-
"doccov.yml",
|
|
101
|
-
"doccov.yaml"
|
|
84
|
+
"doccov.config.mjs"
|
|
102
85
|
];
|
|
103
86
|
var fileExists = async (filePath) => {
|
|
104
87
|
try {
|
|
@@ -125,11 +108,6 @@ var findConfigFile = async (cwd) => {
|
|
|
125
108
|
}
|
|
126
109
|
};
|
|
127
110
|
var importConfigModule = async (absolutePath) => {
|
|
128
|
-
const ext = path.extname(absolutePath);
|
|
129
|
-
if (ext === ".yml" || ext === ".yaml") {
|
|
130
|
-
const content = await readFile(absolutePath, "utf-8");
|
|
131
|
-
return parseYaml(content);
|
|
132
|
-
}
|
|
133
111
|
const fileUrl = pathToFileURL(absolutePath);
|
|
134
112
|
fileUrl.searchParams.set("t", Date.now().toString());
|
|
135
113
|
const module = await import(fileUrl.href);
|
|
@@ -168,8 +146,8 @@ ${formatIssues(issues)}`);
|
|
|
168
146
|
// src/config/index.ts
|
|
169
147
|
var defineConfig = (config) => config;
|
|
170
148
|
// src/cli.ts
|
|
171
|
-
import { readFileSync as
|
|
172
|
-
import * as
|
|
149
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
150
|
+
import * as path9 from "node:path";
|
|
173
151
|
import { fileURLToPath } from "node:url";
|
|
174
152
|
import { Command } from "commander";
|
|
175
153
|
|
|
@@ -189,6 +167,7 @@ import {
|
|
|
189
167
|
NodeFileSystem,
|
|
190
168
|
parseExamplesFlag,
|
|
191
169
|
parseJSDocToPatch,
|
|
170
|
+
parseMarkdownFiles,
|
|
192
171
|
resolveTarget,
|
|
193
172
|
serializeJSDoc,
|
|
194
173
|
validateExamples
|
|
@@ -196,8 +175,76 @@ import {
|
|
|
196
175
|
import {
|
|
197
176
|
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
198
177
|
} from "@openpkg-ts/spec";
|
|
199
|
-
import
|
|
178
|
+
import chalk4 from "chalk";
|
|
179
|
+
import { glob } from "glob";
|
|
200
180
|
|
|
181
|
+
// src/reports/changelog-renderer.ts
|
|
182
|
+
function renderChangelog(data, options = {}) {
|
|
183
|
+
const { diff, categorizedBreaking } = data;
|
|
184
|
+
const lines = [];
|
|
185
|
+
const version = options.version ?? data.version ?? "Unreleased";
|
|
186
|
+
const date = options.date instanceof Date ? options.date.toISOString().split("T")[0] : options.date ?? new Date().toISOString().split("T")[0];
|
|
187
|
+
lines.push(`## [${version}] - ${date}`);
|
|
188
|
+
lines.push("");
|
|
189
|
+
if (diff.breaking.length > 0) {
|
|
190
|
+
lines.push("### ⚠️ BREAKING CHANGES");
|
|
191
|
+
lines.push("");
|
|
192
|
+
if (categorizedBreaking && categorizedBreaking.length > 0) {
|
|
193
|
+
for (const breaking of categorizedBreaking) {
|
|
194
|
+
const severity = breaking.severity === "high" ? "**" : "";
|
|
195
|
+
lines.push(`- ${severity}${breaking.name}${severity}: ${breaking.reason}`);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
for (const id of diff.breaking) {
|
|
199
|
+
lines.push(`- \`${id}\` removed or changed`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
lines.push("");
|
|
203
|
+
}
|
|
204
|
+
if (diff.nonBreaking.length > 0) {
|
|
205
|
+
lines.push("### Added");
|
|
206
|
+
lines.push("");
|
|
207
|
+
for (const id of diff.nonBreaking) {
|
|
208
|
+
lines.push(`- \`${id}\``);
|
|
209
|
+
}
|
|
210
|
+
lines.push("");
|
|
211
|
+
}
|
|
212
|
+
if (diff.docsOnly.length > 0) {
|
|
213
|
+
lines.push("### Documentation");
|
|
214
|
+
lines.push("");
|
|
215
|
+
for (const id of diff.docsOnly) {
|
|
216
|
+
lines.push(`- Updated documentation for \`${id}\``);
|
|
217
|
+
}
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
if (diff.coverageDelta !== 0) {
|
|
221
|
+
lines.push("### Coverage");
|
|
222
|
+
lines.push("");
|
|
223
|
+
const arrow = diff.coverageDelta > 0 ? "↑" : "↓";
|
|
224
|
+
const sign = diff.coverageDelta > 0 ? "+" : "";
|
|
225
|
+
lines.push(`- Documentation coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% (${arrow} ${sign}${diff.coverageDelta}%)`);
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
229
|
+
if (!lines.some((l) => l.startsWith("### Coverage"))) {
|
|
230
|
+
lines.push("### Coverage");
|
|
231
|
+
lines.push("");
|
|
232
|
+
}
|
|
233
|
+
if (diff.driftResolved > 0) {
|
|
234
|
+
lines.push(`- Fixed ${diff.driftResolved} drift issue${diff.driftResolved === 1 ? "" : "s"}`);
|
|
235
|
+
}
|
|
236
|
+
if (diff.driftIntroduced > 0) {
|
|
237
|
+
lines.push(`- ${diff.driftIntroduced} new drift issue${diff.driftIntroduced === 1 ? "" : "s"} detected`);
|
|
238
|
+
}
|
|
239
|
+
lines.push("");
|
|
240
|
+
}
|
|
241
|
+
if (options.compareUrl) {
|
|
242
|
+
lines.push(`**Full Changelog**: ${options.compareUrl}`);
|
|
243
|
+
lines.push("");
|
|
244
|
+
}
|
|
245
|
+
return lines.join(`
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
201
248
|
// src/reports/diff-markdown.ts
|
|
202
249
|
import * as path2 from "node:path";
|
|
203
250
|
function bar(pct, width = 10) {
|
|
@@ -570,7 +617,8 @@ function renderPRComment(data, opts = {}) {
|
|
|
570
617
|
const { diff, headSpec } = data;
|
|
571
618
|
const limit = opts.limit ?? 10;
|
|
572
619
|
const lines = [];
|
|
573
|
-
const
|
|
620
|
+
const hasStaleRefs = (opts.staleDocsRefs?.length ?? 0) > 0;
|
|
621
|
+
const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || hasStaleRefs || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
|
|
574
622
|
const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
|
|
575
623
|
lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
|
|
576
624
|
lines.push("");
|
|
@@ -582,6 +630,13 @@ function renderPRComment(data, opts = {}) {
|
|
|
582
630
|
if (diff.driftIntroduced > 0) {
|
|
583
631
|
lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
|
|
584
632
|
}
|
|
633
|
+
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
634
|
+
lines.push(`**Stale doc references:** ${opts.staleDocsRefs.length}`);
|
|
635
|
+
}
|
|
636
|
+
if (opts.semverBump) {
|
|
637
|
+
const emoji = opts.semverBump.bump === "major" ? "\uD83D\uDD34" : opts.semverBump.bump === "minor" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
|
|
638
|
+
lines.push(`**Semver:** ${emoji} ${opts.semverBump.bump.toUpperCase()} (${opts.semverBump.reason})`);
|
|
639
|
+
}
|
|
585
640
|
if (diff.newUndocumented.length > 0) {
|
|
586
641
|
lines.push("");
|
|
587
642
|
lines.push("### Undocumented exports in this PR");
|
|
@@ -594,7 +649,15 @@ function renderPRComment(data, opts = {}) {
|
|
|
594
649
|
lines.push("");
|
|
595
650
|
renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
|
|
596
651
|
}
|
|
597
|
-
|
|
652
|
+
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
653
|
+
lines.push("");
|
|
654
|
+
lines.push("### \uD83D\uDCDD Stale documentation references");
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("These markdown files reference exports that no longer exist:");
|
|
657
|
+
lines.push("");
|
|
658
|
+
renderStaleDocsRefs(lines, opts.staleDocsRefs, opts, limit);
|
|
659
|
+
}
|
|
660
|
+
const fixGuidance = renderFixGuidance(diff, opts);
|
|
598
661
|
if (fixGuidance) {
|
|
599
662
|
lines.push("");
|
|
600
663
|
lines.push("### How to fix");
|
|
@@ -608,6 +671,16 @@ function renderPRComment(data, opts = {}) {
|
|
|
608
671
|
renderDetailsTable(lines, diff);
|
|
609
672
|
lines.push("");
|
|
610
673
|
lines.push("</details>");
|
|
674
|
+
if (opts.includeBadge !== false && opts.repoUrl) {
|
|
675
|
+
const repoMatch = opts.repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
676
|
+
if (repoMatch) {
|
|
677
|
+
const [, owner, repo] = repoMatch;
|
|
678
|
+
lines.push("");
|
|
679
|
+
lines.push("---");
|
|
680
|
+
lines.push("");
|
|
681
|
+
lines.push(`[](https://doccov.dev/${owner}/${repo})`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
611
684
|
return lines.join(`
|
|
612
685
|
`);
|
|
613
686
|
}
|
|
@@ -728,15 +801,47 @@ function getMissingSignals(exp) {
|
|
|
728
801
|
}
|
|
729
802
|
return missing;
|
|
730
803
|
}
|
|
731
|
-
function
|
|
804
|
+
function renderStaleDocsRefs(lines, refs, opts, limit) {
|
|
805
|
+
const byFile = new Map;
|
|
806
|
+
for (const ref of refs) {
|
|
807
|
+
const list = byFile.get(ref.file) ?? [];
|
|
808
|
+
list.push({ line: ref.line, exportName: ref.exportName });
|
|
809
|
+
byFile.set(ref.file, list);
|
|
810
|
+
}
|
|
811
|
+
let count = 0;
|
|
812
|
+
for (const [file, fileRefs] of byFile) {
|
|
813
|
+
if (count >= limit)
|
|
814
|
+
break;
|
|
815
|
+
const fileLink = buildFileLink(file, opts);
|
|
816
|
+
lines.push(`\uD83D\uDCC1 ${fileLink}`);
|
|
817
|
+
for (const ref of fileRefs) {
|
|
818
|
+
if (count >= limit)
|
|
819
|
+
break;
|
|
820
|
+
lines.push(`- Line ${ref.line}: \`${ref.exportName}\` does not exist`);
|
|
821
|
+
count++;
|
|
822
|
+
}
|
|
823
|
+
lines.push("");
|
|
824
|
+
}
|
|
825
|
+
if (refs.length > count) {
|
|
826
|
+
lines.push(`_...and ${refs.length - count} more stale references_`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function renderFixGuidance(diff, opts) {
|
|
732
830
|
const sections = [];
|
|
733
831
|
if (diff.newUndocumented.length > 0) {
|
|
734
832
|
sections.push(`**For undocumented exports:**
|
|
735
833
|
` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
|
|
736
834
|
}
|
|
737
835
|
if (diff.driftIntroduced > 0) {
|
|
836
|
+
const fixableNote = opts.fixableDriftCount && opts.fixableDriftCount > 0 ? `
|
|
837
|
+
|
|
838
|
+
**Quick fix:** Run \`npx doccov check --fix\` to auto-fix ${opts.fixableDriftCount} issue(s).` : "";
|
|
738
839
|
sections.push(`**For doc drift:**
|
|
739
|
-
` + "Update
|
|
840
|
+
` + "Update JSDoc to match current code signatures." + fixableNote);
|
|
841
|
+
}
|
|
842
|
+
if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
|
|
843
|
+
sections.push(`**For stale docs:**
|
|
844
|
+
` + "Update or remove code examples that reference deleted exports.");
|
|
740
845
|
}
|
|
741
846
|
if (diff.breaking.length > 0) {
|
|
742
847
|
sections.push(`**For breaking changes:**
|
|
@@ -899,76 +1004,59 @@ function writeReports(options) {
|
|
|
899
1004
|
}));
|
|
900
1005
|
return results;
|
|
901
1006
|
}
|
|
902
|
-
// src/utils/
|
|
903
|
-
import {
|
|
904
|
-
import
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
Look for comments that appear to specify expected output values, such as:
|
|
919
|
-
- "// should be 3"
|
|
920
|
-
- "// returns 5"
|
|
921
|
-
- "// outputs: hello"
|
|
922
|
-
- "// expected: [1, 2, 3]"
|
|
923
|
-
- "// 42" (bare value after console.log)
|
|
924
|
-
- "// result: true"
|
|
925
|
-
|
|
926
|
-
Do NOT include:
|
|
927
|
-
- Regular code comments that explain what the code does
|
|
928
|
-
- Comments that are instructions or documentation
|
|
929
|
-
- Comments with // => (already using standard syntax)
|
|
930
|
-
|
|
931
|
-
For each assertion found, extract:
|
|
932
|
-
1. The line number (1-indexed)
|
|
933
|
-
2. The expected value (just the value, not the comment prefix)
|
|
934
|
-
3. The original comment text
|
|
935
|
-
4. A suggested rewrite of the ENTIRE line using "// => value" syntax
|
|
936
|
-
|
|
937
|
-
Code:
|
|
938
|
-
\`\`\`
|
|
939
|
-
${code}
|
|
940
|
-
\`\`\``;
|
|
941
|
-
function getModel() {
|
|
942
|
-
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
943
|
-
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
944
|
-
const anthropic = createAnthropic();
|
|
945
|
-
return anthropic("claude-sonnet-4-20250514");
|
|
946
|
-
}
|
|
947
|
-
const openai = createOpenAI();
|
|
948
|
-
return openai("gpt-4o-mini");
|
|
949
|
-
}
|
|
950
|
-
function isLLMAssertionParsingAvailable() {
|
|
951
|
-
return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
952
|
-
}
|
|
953
|
-
async function parseAssertionsWithLLM(code) {
|
|
954
|
-
if (!isLLMAssertionParsingAvailable()) {
|
|
955
|
-
return null;
|
|
1007
|
+
// src/utils/filter-options.ts
|
|
1008
|
+
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1009
|
+
import chalk2 from "chalk";
|
|
1010
|
+
var parseVisibilityFlag = (value) => {
|
|
1011
|
+
if (!value)
|
|
1012
|
+
return;
|
|
1013
|
+
const validTags = ["public", "beta", "alpha", "internal"];
|
|
1014
|
+
const parsed = parseListFlag(value);
|
|
1015
|
+
if (!parsed)
|
|
1016
|
+
return;
|
|
1017
|
+
const result = [];
|
|
1018
|
+
for (const tag of parsed) {
|
|
1019
|
+
const lower = tag.toLowerCase();
|
|
1020
|
+
if (validTags.includes(lower)) {
|
|
1021
|
+
result.push(lower);
|
|
1022
|
+
}
|
|
956
1023
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
return object;
|
|
965
|
-
} catch {
|
|
966
|
-
return null;
|
|
1024
|
+
return result.length > 0 ? result : undefined;
|
|
1025
|
+
};
|
|
1026
|
+
var formatList = (label, values) => `${label}: ${values.map((value) => chalk2.cyan(value)).join(", ")}`;
|
|
1027
|
+
var mergeFilterOptions = (config, cliOptions) => {
|
|
1028
|
+
const messages = [];
|
|
1029
|
+
if (config?.include) {
|
|
1030
|
+
messages.push(formatList("include filters from config", config.include));
|
|
967
1031
|
}
|
|
968
|
-
|
|
1032
|
+
if (config?.exclude) {
|
|
1033
|
+
messages.push(formatList("exclude filters from config", config.exclude));
|
|
1034
|
+
}
|
|
1035
|
+
if (cliOptions.include) {
|
|
1036
|
+
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
1037
|
+
}
|
|
1038
|
+
if (cliOptions.exclude) {
|
|
1039
|
+
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
1040
|
+
}
|
|
1041
|
+
if (cliOptions.visibility) {
|
|
1042
|
+
messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
|
|
1043
|
+
}
|
|
1044
|
+
const resolved = mergeFilters(config, cliOptions);
|
|
1045
|
+
if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
|
|
1046
|
+
return { messages };
|
|
1047
|
+
}
|
|
1048
|
+
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
1049
|
+
return {
|
|
1050
|
+
include: resolved.include,
|
|
1051
|
+
exclude: resolved.exclude,
|
|
1052
|
+
visibility: cliOptions.visibility,
|
|
1053
|
+
source,
|
|
1054
|
+
messages
|
|
1055
|
+
};
|
|
1056
|
+
};
|
|
969
1057
|
|
|
970
1058
|
// src/utils/progress.ts
|
|
971
|
-
import
|
|
1059
|
+
import chalk3 from "chalk";
|
|
972
1060
|
class StepProgress {
|
|
973
1061
|
steps;
|
|
974
1062
|
currentStep = 0;
|
|
@@ -996,7 +1084,7 @@ class StepProgress {
|
|
|
996
1084
|
this.completeCurrentStep();
|
|
997
1085
|
if (message) {
|
|
998
1086
|
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
999
|
-
console.log(`${
|
|
1087
|
+
console.log(`${chalk3.green("✓")} ${message} ${chalk3.dim(`(${elapsed}s)`)}`);
|
|
1000
1088
|
}
|
|
1001
1089
|
}
|
|
1002
1090
|
render() {
|
|
@@ -1004,17 +1092,17 @@ class StepProgress {
|
|
|
1004
1092
|
if (!step)
|
|
1005
1093
|
return;
|
|
1006
1094
|
const label = step.activeLabel ?? step.label;
|
|
1007
|
-
const prefix =
|
|
1008
|
-
process.stdout.write(`\r${prefix} ${
|
|
1095
|
+
const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1096
|
+
process.stdout.write(`\r${prefix} ${chalk3.cyan(label)}...`);
|
|
1009
1097
|
}
|
|
1010
1098
|
completeCurrentStep() {
|
|
1011
1099
|
const step = this.steps[this.currentStep];
|
|
1012
1100
|
if (!step)
|
|
1013
1101
|
return;
|
|
1014
1102
|
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
1015
|
-
const prefix =
|
|
1103
|
+
const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1016
1104
|
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
1017
|
-
console.log(`${prefix} ${step.label} ${
|
|
1105
|
+
console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
|
|
1018
1106
|
}
|
|
1019
1107
|
}
|
|
1020
1108
|
|
|
@@ -1058,10 +1146,10 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1058
1146
|
...defaultDependencies,
|
|
1059
1147
|
...dependencies
|
|
1060
1148
|
};
|
|
1061
|
-
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-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (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("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--
|
|
1149
|
+
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-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (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("--write", "Alias for --fix").option("--preview", "Preview fixes with diff output (implies --fix)").option("--dry-run", "Alias for --preview").option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--update-snapshot", "Force regenerate .doccov/report.json").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)").option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
|
|
1062
1150
|
try {
|
|
1063
|
-
|
|
1064
|
-
|
|
1151
|
+
let validations = parseExamplesFlag(options.examples);
|
|
1152
|
+
let hasExamples = validations.length > 0;
|
|
1065
1153
|
const stepList = [
|
|
1066
1154
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
1067
1155
|
{ label: "Loaded config", activeLabel: "Loading config" },
|
|
@@ -1081,10 +1169,29 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1081
1169
|
const { targetDir, entryFile } = resolved;
|
|
1082
1170
|
steps.next();
|
|
1083
1171
|
const config = await loadDocCovConfig(targetDir);
|
|
1084
|
-
|
|
1085
|
-
|
|
1172
|
+
if (!hasExamples && config?.check?.examples) {
|
|
1173
|
+
const configExamples = config.check.examples;
|
|
1174
|
+
if (Array.isArray(configExamples)) {
|
|
1175
|
+
validations = configExamples;
|
|
1176
|
+
} else if (typeof configExamples === "string") {
|
|
1177
|
+
validations = parseExamplesFlag(configExamples);
|
|
1178
|
+
}
|
|
1179
|
+
hasExamples = validations.length > 0;
|
|
1180
|
+
}
|
|
1181
|
+
const DEFAULT_MIN_COVERAGE = 80;
|
|
1182
|
+
const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage ?? DEFAULT_MIN_COVERAGE;
|
|
1183
|
+
const minCoverage = clampPercentage(minCoverageRaw);
|
|
1086
1184
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
1087
1185
|
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
1186
|
+
const cliFilters = {
|
|
1187
|
+
include: undefined,
|
|
1188
|
+
exclude: undefined,
|
|
1189
|
+
visibility: parseVisibilityFlag(options.visibility)
|
|
1190
|
+
};
|
|
1191
|
+
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
1192
|
+
if (resolvedFilters.visibility) {
|
|
1193
|
+
log(chalk4.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
|
|
1194
|
+
}
|
|
1088
1195
|
steps.next();
|
|
1089
1196
|
const resolveExternalTypes = !options.skipResolve;
|
|
1090
1197
|
let specResult;
|
|
@@ -1094,7 +1201,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1094
1201
|
useCache: options.cache !== false,
|
|
1095
1202
|
cwd: options.cwd
|
|
1096
1203
|
});
|
|
1097
|
-
|
|
1204
|
+
const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
|
|
1205
|
+
specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1098
1206
|
if (!specResult) {
|
|
1099
1207
|
throw new Error("Failed to analyze documentation coverage.");
|
|
1100
1208
|
}
|
|
@@ -1104,13 +1212,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1104
1212
|
steps.next();
|
|
1105
1213
|
const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
|
|
1106
1214
|
const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
for (const exp of spec.exports ?? []) {
|
|
1110
|
-
for (const v of exp.docs?.violations ?? []) {
|
|
1111
|
-
violations.push({ exportName: exp.name, violation: v });
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1215
|
+
const isPreview = options.preview || options.dryRun;
|
|
1216
|
+
const shouldFix = options.fix || options.write || isPreview;
|
|
1114
1217
|
let exampleResult;
|
|
1115
1218
|
const typecheckErrors = [];
|
|
1116
1219
|
const runtimeDrifts = [];
|
|
@@ -1120,11 +1223,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1120
1223
|
packagePath: targetDir,
|
|
1121
1224
|
exportNames: (spec.exports ?? []).map((e) => e.name),
|
|
1122
1225
|
timeout: 5000,
|
|
1123
|
-
installTimeout: 60000
|
|
1124
|
-
llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
|
|
1125
|
-
const result = await parseAssertionsWithLLM(example);
|
|
1126
|
-
return result;
|
|
1127
|
-
} : undefined
|
|
1226
|
+
installTimeout: 60000
|
|
1128
1227
|
});
|
|
1129
1228
|
if (exampleResult.typecheck) {
|
|
1130
1229
|
for (const err of exampleResult.typecheck.errors) {
|
|
@@ -1147,6 +1246,41 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1147
1246
|
}
|
|
1148
1247
|
steps.next();
|
|
1149
1248
|
}
|
|
1249
|
+
const staleRefs = [];
|
|
1250
|
+
let docsPatterns = options.docs;
|
|
1251
|
+
if (docsPatterns.length === 0 && config?.docs?.include) {
|
|
1252
|
+
docsPatterns = config.docs.include;
|
|
1253
|
+
}
|
|
1254
|
+
if (docsPatterns.length > 0) {
|
|
1255
|
+
const markdownFiles = await loadMarkdownFiles(docsPatterns, targetDir);
|
|
1256
|
+
if (markdownFiles.length > 0) {
|
|
1257
|
+
const exportNames = (spec.exports ?? []).map((e) => e.name);
|
|
1258
|
+
const exportSet = new Set(exportNames);
|
|
1259
|
+
for (const mdFile of markdownFiles) {
|
|
1260
|
+
for (const block of mdFile.codeBlocks) {
|
|
1261
|
+
const codeLines = block.code.split(`
|
|
1262
|
+
`);
|
|
1263
|
+
for (let i = 0;i < codeLines.length; i++) {
|
|
1264
|
+
const line = codeLines[i];
|
|
1265
|
+
const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
|
|
1266
|
+
if (importMatch) {
|
|
1267
|
+
const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
|
|
1268
|
+
for (const imp of imports) {
|
|
1269
|
+
if (imp && !exportSet.has(imp)) {
|
|
1270
|
+
staleRefs.push({
|
|
1271
|
+
file: mdFile.path,
|
|
1272
|
+
line: block.lineStart + i,
|
|
1273
|
+
exportName: imp,
|
|
1274
|
+
context: line.trim()
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1150
1284
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
1151
1285
|
const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
1152
1286
|
let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
|
|
@@ -1156,12 +1290,12 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1156
1290
|
if (allDrifts.length > 0) {
|
|
1157
1291
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1158
1292
|
if (fixable.length === 0) {
|
|
1159
|
-
log(
|
|
1293
|
+
log(chalk4.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1160
1294
|
} else {
|
|
1161
1295
|
log("");
|
|
1162
|
-
log(
|
|
1296
|
+
log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
1163
1297
|
if (nonFixable.length > 0) {
|
|
1164
|
-
log(
|
|
1298
|
+
log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
1165
1299
|
}
|
|
1166
1300
|
log("");
|
|
1167
1301
|
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
@@ -1169,22 +1303,22 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1169
1303
|
const editsByFile = new Map;
|
|
1170
1304
|
for (const [exp, drifts] of groupedDrifts) {
|
|
1171
1305
|
if (!exp.source?.file) {
|
|
1172
|
-
log(
|
|
1306
|
+
log(chalk4.gray(` Skipping ${exp.name}: no source location`));
|
|
1173
1307
|
continue;
|
|
1174
1308
|
}
|
|
1175
1309
|
if (exp.source.file.endsWith(".d.ts")) {
|
|
1176
|
-
log(
|
|
1310
|
+
log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
|
|
1177
1311
|
continue;
|
|
1178
1312
|
}
|
|
1179
1313
|
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1180
1314
|
if (!fs2.existsSync(filePath)) {
|
|
1181
|
-
log(
|
|
1315
|
+
log(chalk4.gray(` Skipping ${exp.name}: file not found`));
|
|
1182
1316
|
continue;
|
|
1183
1317
|
}
|
|
1184
1318
|
const sourceFile = createSourceFile(filePath);
|
|
1185
1319
|
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1186
1320
|
if (!location) {
|
|
1187
|
-
log(
|
|
1321
|
+
log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
1188
1322
|
continue;
|
|
1189
1323
|
}
|
|
1190
1324
|
let existingPatch = {};
|
|
@@ -1216,34 +1350,61 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1216
1350
|
editsByFile.set(filePath, fileEdits);
|
|
1217
1351
|
}
|
|
1218
1352
|
if (edits.length > 0) {
|
|
1219
|
-
if (
|
|
1220
|
-
log(
|
|
1353
|
+
if (isPreview) {
|
|
1354
|
+
log(chalk4.bold("Preview - changes that would be made:"));
|
|
1221
1355
|
log("");
|
|
1222
1356
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
1223
1357
|
const relativePath = path4.relative(targetDir, filePath);
|
|
1224
|
-
log(chalk3.cyan(` ${relativePath}:`));
|
|
1225
1358
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
1226
|
-
|
|
1227
|
-
log(`
|
|
1228
|
-
|
|
1229
|
-
|
|
1359
|
+
log(chalk4.cyan(`${relativePath}:${edit.startLine + 1}`));
|
|
1360
|
+
log(chalk4.bold(` ${exp.name}`));
|
|
1361
|
+
log("");
|
|
1362
|
+
if (edit.hasExisting && edit.existingJSDoc) {
|
|
1363
|
+
const oldLines = edit.existingJSDoc.split(`
|
|
1364
|
+
`);
|
|
1365
|
+
const newLines = edit.newJSDoc.split(`
|
|
1366
|
+
`);
|
|
1367
|
+
for (const line of oldLines) {
|
|
1368
|
+
log(chalk4.red(` - ${line}`));
|
|
1369
|
+
}
|
|
1370
|
+
for (const line of newLines) {
|
|
1371
|
+
log(chalk4.green(` + ${line}`));
|
|
1372
|
+
}
|
|
1373
|
+
} else {
|
|
1374
|
+
const newLines = edit.newJSDoc.split(`
|
|
1375
|
+
`);
|
|
1376
|
+
for (const line of newLines) {
|
|
1377
|
+
log(chalk4.green(` + ${line}`));
|
|
1378
|
+
}
|
|
1230
1379
|
}
|
|
1380
|
+
log("");
|
|
1381
|
+
log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
|
|
1382
|
+
log("");
|
|
1231
1383
|
}
|
|
1232
|
-
log("");
|
|
1233
1384
|
}
|
|
1234
|
-
|
|
1385
|
+
const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
1386
|
+
log(chalk4.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
|
|
1387
|
+
log(chalk4.gray("Run with --fix to apply these changes."));
|
|
1235
1388
|
} else {
|
|
1236
1389
|
const applyResult = await applyEdits(edits);
|
|
1237
1390
|
if (applyResult.errors.length > 0) {
|
|
1238
1391
|
for (const err of applyResult.errors) {
|
|
1239
|
-
error(
|
|
1392
|
+
error(chalk4.red(` ${err.file}: ${err.error}`));
|
|
1240
1393
|
}
|
|
1241
1394
|
}
|
|
1395
|
+
const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
1396
|
+
log("");
|
|
1397
|
+
log(chalk4.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
|
|
1398
|
+
for (const [filePath, fileEdits] of editsByFile) {
|
|
1399
|
+
const relativePath = path4.relative(targetDir, filePath);
|
|
1400
|
+
const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
|
|
1401
|
+
log(chalk4.dim(` ${relativePath} (${fixCount} fixes)`));
|
|
1402
|
+
}
|
|
1242
1403
|
}
|
|
1243
1404
|
}
|
|
1244
1405
|
}
|
|
1245
1406
|
}
|
|
1246
|
-
if (!
|
|
1407
|
+
if (!isPreview) {
|
|
1247
1408
|
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1248
1409
|
}
|
|
1249
1410
|
}
|
|
@@ -1267,8 +1428,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1267
1428
|
case "github":
|
|
1268
1429
|
formatContent = renderGithubSummary(stats, {
|
|
1269
1430
|
coverageScore,
|
|
1270
|
-
driftCount: driftExports.length
|
|
1271
|
-
qualityIssues: violations.length
|
|
1431
|
+
driftCount: driftExports.length
|
|
1272
1432
|
});
|
|
1273
1433
|
break;
|
|
1274
1434
|
default:
|
|
@@ -1288,11 +1448,10 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1288
1448
|
const totalExportsForDrift2 = spec.exports?.length ?? 0;
|
|
1289
1449
|
const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
|
|
1290
1450
|
const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
|
|
1291
|
-
const coverageFailed2 =
|
|
1451
|
+
const coverageFailed2 = coverageScore < minCoverage;
|
|
1292
1452
|
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
1293
|
-
const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1294
1453
|
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
1295
|
-
if (coverageFailed2 || driftFailed2 ||
|
|
1454
|
+
if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
|
|
1296
1455
|
process.exit(1);
|
|
1297
1456
|
}
|
|
1298
1457
|
return;
|
|
@@ -1300,48 +1459,41 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1300
1459
|
const totalExportsForDrift = spec.exports?.length ?? 0;
|
|
1301
1460
|
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
1302
1461
|
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
1303
|
-
const coverageFailed =
|
|
1462
|
+
const coverageFailed = coverageScore < minCoverage;
|
|
1304
1463
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1305
|
-
const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1306
1464
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1307
1465
|
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
1308
1466
|
log("");
|
|
1309
1467
|
for (const diag of specWarnings) {
|
|
1310
|
-
log(
|
|
1468
|
+
log(chalk4.yellow(`⚠ ${diag.message}`));
|
|
1311
1469
|
if (diag.suggestion) {
|
|
1312
|
-
log(
|
|
1470
|
+
log(chalk4.gray(` ${diag.suggestion}`));
|
|
1313
1471
|
}
|
|
1314
1472
|
}
|
|
1315
1473
|
for (const diag of specInfos) {
|
|
1316
|
-
log(
|
|
1474
|
+
log(chalk4.cyan(`ℹ ${diag.message}`));
|
|
1317
1475
|
if (diag.suggestion) {
|
|
1318
|
-
log(
|
|
1476
|
+
log(chalk4.gray(` ${diag.suggestion}`));
|
|
1319
1477
|
}
|
|
1320
1478
|
}
|
|
1321
1479
|
}
|
|
1322
1480
|
const pkgName = spec.meta?.name ?? "unknown";
|
|
1323
1481
|
const pkgVersion = spec.meta?.version ?? "";
|
|
1324
1482
|
const totalExports = spec.exports?.length ?? 0;
|
|
1325
|
-
const errorCount = violations.filter((v) => v.violation.severity === "error").length;
|
|
1326
|
-
const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
|
|
1327
1483
|
log("");
|
|
1328
|
-
log(
|
|
1484
|
+
log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1329
1485
|
log("");
|
|
1330
1486
|
log(` Exports: ${totalExports}`);
|
|
1331
|
-
if (
|
|
1332
|
-
|
|
1333
|
-
log(chalk3.red(` Coverage: ✗ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
|
|
1334
|
-
} else {
|
|
1335
|
-
log(chalk3.green(` Coverage: ✓ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
|
|
1336
|
-
}
|
|
1487
|
+
if (coverageFailed) {
|
|
1488
|
+
log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1337
1489
|
} else {
|
|
1338
|
-
log(` Coverage: ${coverageScore}%`);
|
|
1490
|
+
log(chalk4.green(` Coverage: ✓ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1339
1491
|
}
|
|
1340
1492
|
if (maxDrift !== undefined) {
|
|
1341
1493
|
if (driftFailed) {
|
|
1342
|
-
log(
|
|
1494
|
+
log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1343
1495
|
} else {
|
|
1344
|
-
log(
|
|
1496
|
+
log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1345
1497
|
}
|
|
1346
1498
|
} else {
|
|
1347
1499
|
log(` Drift: ${driftScore}%`);
|
|
@@ -1349,48 +1501,51 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1349
1501
|
if (exampleResult) {
|
|
1350
1502
|
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
1351
1503
|
if (typecheckCount > 0) {
|
|
1352
|
-
log(` Examples: ${typecheckCount} type
|
|
1504
|
+
log(chalk4.yellow(` Examples: ${typecheckCount} type error(s)`));
|
|
1505
|
+
for (const err of typecheckErrors.slice(0, 5)) {
|
|
1506
|
+
const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
|
|
1507
|
+
log(chalk4.dim(` ${err.exportName} ${loc}`));
|
|
1508
|
+
log(chalk4.red(` ${err.error.message}`));
|
|
1509
|
+
}
|
|
1510
|
+
if (typecheckErrors.length > 5) {
|
|
1511
|
+
log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
|
|
1512
|
+
}
|
|
1353
1513
|
} else {
|
|
1354
|
-
log(
|
|
1514
|
+
log(chalk4.green(` Examples: ✓ validated`));
|
|
1355
1515
|
}
|
|
1356
1516
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1517
|
+
const hasStaleRefs = staleRefs.length > 0;
|
|
1518
|
+
if (hasStaleRefs) {
|
|
1519
|
+
log(chalk4.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
|
|
1520
|
+
for (const ref of staleRefs.slice(0, 5)) {
|
|
1521
|
+
log(chalk4.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
|
|
1522
|
+
}
|
|
1523
|
+
if (staleRefs.length > 5) {
|
|
1524
|
+
log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
|
|
1525
|
+
}
|
|
1364
1526
|
}
|
|
1365
1527
|
log("");
|
|
1366
|
-
const failed = coverageFailed || driftFailed ||
|
|
1528
|
+
const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
|
|
1367
1529
|
if (!failed) {
|
|
1368
1530
|
const thresholdParts = [];
|
|
1369
|
-
|
|
1370
|
-
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1371
|
-
}
|
|
1531
|
+
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1372
1532
|
if (maxDrift !== undefined) {
|
|
1373
1533
|
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1374
1534
|
}
|
|
1375
|
-
|
|
1376
|
-
log(chalk3.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1377
|
-
} else {
|
|
1378
|
-
log(chalk3.green("✓ Check passed"));
|
|
1379
|
-
log(chalk3.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
|
|
1380
|
-
}
|
|
1535
|
+
log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1381
1536
|
return;
|
|
1382
1537
|
}
|
|
1383
|
-
if (hasQualityErrors) {
|
|
1384
|
-
log(chalk3.red(`✗ ${errorCount} quality errors`));
|
|
1385
|
-
}
|
|
1386
1538
|
if (hasTypecheckErrors) {
|
|
1387
|
-
log(
|
|
1539
|
+
log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1540
|
+
}
|
|
1541
|
+
if (hasStaleRefs) {
|
|
1542
|
+
log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
|
|
1388
1543
|
}
|
|
1389
1544
|
log("");
|
|
1390
|
-
log(
|
|
1545
|
+
log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
|
|
1391
1546
|
process.exit(1);
|
|
1392
1547
|
} catch (commandError) {
|
|
1393
|
-
error(
|
|
1548
|
+
error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1394
1549
|
process.exit(1);
|
|
1395
1550
|
}
|
|
1396
1551
|
});
|
|
@@ -1414,6 +1569,23 @@ function collectDrift(exportsList) {
|
|
|
1414
1569
|
}
|
|
1415
1570
|
return drifts;
|
|
1416
1571
|
}
|
|
1572
|
+
function collect(value, previous) {
|
|
1573
|
+
return previous.concat([value]);
|
|
1574
|
+
}
|
|
1575
|
+
async function loadMarkdownFiles(patterns, cwd) {
|
|
1576
|
+
const files = [];
|
|
1577
|
+
for (const pattern of patterns) {
|
|
1578
|
+
const matches = await glob(pattern, { nodir: true, cwd });
|
|
1579
|
+
for (const filePath of matches) {
|
|
1580
|
+
try {
|
|
1581
|
+
const fullPath = path4.resolve(cwd, filePath);
|
|
1582
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
1583
|
+
files.push({ path: filePath, content });
|
|
1584
|
+
} catch {}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return parseMarkdownFiles(files);
|
|
1588
|
+
}
|
|
1417
1589
|
|
|
1418
1590
|
// src/commands/diff.ts
|
|
1419
1591
|
import * as fs3 from "node:fs";
|
|
@@ -1425,72 +1597,11 @@ import {
|
|
|
1425
1597
|
getDocsImpactSummary,
|
|
1426
1598
|
hasDocsImpact,
|
|
1427
1599
|
hashString,
|
|
1428
|
-
parseMarkdownFiles
|
|
1600
|
+
parseMarkdownFiles as parseMarkdownFiles2
|
|
1429
1601
|
} from "@doccov/sdk";
|
|
1430
|
-
import
|
|
1431
|
-
import
|
|
1432
|
-
|
|
1433
|
-
// src/utils/docs-impact-ai.ts
|
|
1434
|
-
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
1435
|
-
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
1436
|
-
import { generateObject as generateObject2, generateText } from "ai";
|
|
1437
|
-
import { z as z3 } from "zod";
|
|
1438
|
-
var CodeBlockUsageSchema = z3.object({
|
|
1439
|
-
isImpacted: z3.boolean().describe("Whether the code block is affected by the change"),
|
|
1440
|
-
reason: z3.string().describe("Explanation of why/why not the code is impacted"),
|
|
1441
|
-
usageType: z3.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
|
|
1442
|
-
suggestedFix: z3.string().optional().describe("If impacted, the suggested code change"),
|
|
1443
|
-
confidence: z3.enum(["high", "medium", "low"]).describe("Confidence level of the analysis")
|
|
1444
|
-
});
|
|
1445
|
-
var MultiBlockAnalysisSchema = z3.object({
|
|
1446
|
-
groups: z3.array(z3.object({
|
|
1447
|
-
blockIndices: z3.array(z3.number()).describe("Indices of blocks that should run together"),
|
|
1448
|
-
reason: z3.string().describe("Why these blocks are related")
|
|
1449
|
-
})).describe("Groups of related code blocks"),
|
|
1450
|
-
skippedBlocks: z3.array(z3.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
|
|
1451
|
-
});
|
|
1452
|
-
function getModel2() {
|
|
1453
|
-
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
1454
|
-
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
1455
|
-
const anthropic = createAnthropic2();
|
|
1456
|
-
return anthropic("claude-sonnet-4-20250514");
|
|
1457
|
-
}
|
|
1458
|
-
const openai = createOpenAI2();
|
|
1459
|
-
return openai("gpt-4o-mini");
|
|
1460
|
-
}
|
|
1461
|
-
function isAIDocsAnalysisAvailable() {
|
|
1462
|
-
return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
1463
|
-
}
|
|
1464
|
-
async function generateImpactSummary(impacts) {
|
|
1465
|
-
if (!isAIDocsAnalysisAvailable()) {
|
|
1466
|
-
return null;
|
|
1467
|
-
}
|
|
1468
|
-
if (impacts.length === 0) {
|
|
1469
|
-
return "No documentation impacts detected.";
|
|
1470
|
-
}
|
|
1471
|
-
try {
|
|
1472
|
-
const { text } = await generateText({
|
|
1473
|
-
model: getModel2(),
|
|
1474
|
-
prompt: `Summarize these documentation impacts for a GitHub PR comment.
|
|
1475
|
-
|
|
1476
|
-
Impacts:
|
|
1477
|
-
${impacts.map((i) => `- ${i.file}: ${i.exportName} (${i.changeType})`).join(`
|
|
1478
|
-
`)}
|
|
1479
|
-
|
|
1480
|
-
Write a brief, actionable summary (2-3 sentences) explaining:
|
|
1481
|
-
1. How many files/references are affected
|
|
1482
|
-
2. What type of updates are needed
|
|
1483
|
-
3. Priority recommendation
|
|
1484
|
-
|
|
1485
|
-
Keep it concise and developer-friendly.`
|
|
1486
|
-
});
|
|
1487
|
-
return text.trim();
|
|
1488
|
-
} catch {
|
|
1489
|
-
return null;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// src/commands/diff.ts
|
|
1602
|
+
import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
|
|
1603
|
+
import chalk5 from "chalk";
|
|
1604
|
+
import { glob as glob2 } from "glob";
|
|
1494
1605
|
var defaultDependencies2 = {
|
|
1495
1606
|
readFileSync: fs3.readFileSync,
|
|
1496
1607
|
log: console.log,
|
|
@@ -1511,11 +1622,11 @@ function getStrictChecks(preset) {
|
|
|
1511
1622
|
return checks;
|
|
1512
1623
|
}
|
|
1513
1624
|
function registerDiffCommand(program, dependencies = {}) {
|
|
1514
|
-
const { readFileSync:
|
|
1625
|
+
const { readFileSync: readFileSync3, log, error } = {
|
|
1515
1626
|
...defaultDependencies2,
|
|
1516
1627
|
...dependencies
|
|
1517
1628
|
};
|
|
1518
|
-
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",
|
|
1629
|
+
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, changelog", "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", collect2, []).option("--no-cache", "Bypass cache and force regeneration").option("--recommend-version", "Output recommended semver version bump").action(async (baseArg, headArg, options) => {
|
|
1519
1630
|
try {
|
|
1520
1631
|
const baseFile = options.base ?? baseArg;
|
|
1521
1632
|
const headFile = options.head ?? headArg;
|
|
@@ -1524,8 +1635,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1524
1635
|
` + `Usage: doccov diff <base> <head>
|
|
1525
1636
|
` + " or: doccov diff --base main.json --head feature.json");
|
|
1526
1637
|
}
|
|
1527
|
-
const baseSpec = loadSpec(baseFile,
|
|
1528
|
-
const headSpec = loadSpec(headFile,
|
|
1638
|
+
const baseSpec = loadSpec(baseFile, readFileSync3);
|
|
1639
|
+
const headSpec = loadSpec(headFile, readFileSync3);
|
|
1529
1640
|
const config = await loadDocCovConfig(options.cwd);
|
|
1530
1641
|
const baseHash = hashString(JSON.stringify(baseSpec));
|
|
1531
1642
|
const headHash = hashString(JSON.stringify(headSpec));
|
|
@@ -1546,6 +1657,29 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1546
1657
|
}
|
|
1547
1658
|
const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
|
|
1548
1659
|
const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
|
|
1660
|
+
if (options.recommendVersion) {
|
|
1661
|
+
const recommendation = recommendSemverBump(diff);
|
|
1662
|
+
const currentVersion = headSpec.meta?.version ?? "0.0.0";
|
|
1663
|
+
const nextVersion = calculateNextVersion(currentVersion, recommendation.bump);
|
|
1664
|
+
if (options.format === "json") {
|
|
1665
|
+
log(JSON.stringify({
|
|
1666
|
+
current: currentVersion,
|
|
1667
|
+
recommended: nextVersion,
|
|
1668
|
+
bump: recommendation.bump,
|
|
1669
|
+
reason: recommendation.reason,
|
|
1670
|
+
breakingCount: recommendation.breakingCount,
|
|
1671
|
+
additionCount: recommendation.additionCount,
|
|
1672
|
+
docsOnlyChanges: recommendation.docsOnlyChanges
|
|
1673
|
+
}, null, 2));
|
|
1674
|
+
} else {
|
|
1675
|
+
log("");
|
|
1676
|
+
log(chalk5.bold("Semver Recommendation"));
|
|
1677
|
+
log(` Current version: ${currentVersion}`);
|
|
1678
|
+
log(` Recommended: ${chalk5.cyan(nextVersion)} (${chalk5.yellow(recommendation.bump.toUpperCase())})`);
|
|
1679
|
+
log(` Reason: ${recommendation.reason}`);
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1549
1683
|
const format = options.format ?? "text";
|
|
1550
1684
|
const limit = parseInt(options.limit, 10) || 10;
|
|
1551
1685
|
const checks = getStrictChecks(options.strict);
|
|
@@ -1559,9 +1693,6 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1559
1693
|
switch (format) {
|
|
1560
1694
|
case "text":
|
|
1561
1695
|
printSummary(diff, baseName, headName, fromCache, log);
|
|
1562
|
-
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
1563
|
-
await printAISummary(diff, log);
|
|
1564
|
-
}
|
|
1565
1696
|
if (!options.stdout) {
|
|
1566
1697
|
const jsonPath = getDiffReportPath(baseHash, headHash, "json");
|
|
1567
1698
|
if (!fromCache) {
|
|
@@ -1573,8 +1704,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1573
1704
|
silent: true
|
|
1574
1705
|
});
|
|
1575
1706
|
}
|
|
1576
|
-
const cacheNote = fromCache ?
|
|
1577
|
-
log(
|
|
1707
|
+
const cacheNote = fromCache ? chalk5.cyan(" (cached)") : "";
|
|
1708
|
+
log(chalk5.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
1578
1709
|
}
|
|
1579
1710
|
break;
|
|
1580
1711
|
case "json": {
|
|
@@ -1626,15 +1757,39 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1626
1757
|
printGitHubAnnotations(diff, log);
|
|
1627
1758
|
break;
|
|
1628
1759
|
case "pr-comment": {
|
|
1760
|
+
const semverRecommendation = recommendSemverBump(diff);
|
|
1629
1761
|
const content = renderPRComment({ diff, baseName, headName, headSpec }, {
|
|
1630
1762
|
repoUrl: options.repoUrl,
|
|
1631
1763
|
sha: options.sha,
|
|
1632
1764
|
minCoverage,
|
|
1633
|
-
limit
|
|
1765
|
+
limit,
|
|
1766
|
+
semverBump: { bump: semverRecommendation.bump, reason: semverRecommendation.reason }
|
|
1634
1767
|
});
|
|
1635
1768
|
log(content);
|
|
1636
1769
|
break;
|
|
1637
1770
|
}
|
|
1771
|
+
case "changelog": {
|
|
1772
|
+
const content = renderChangelog({
|
|
1773
|
+
diff,
|
|
1774
|
+
categorizedBreaking: diff.categorizedBreaking,
|
|
1775
|
+
version: headSpec.meta?.version
|
|
1776
|
+
}, {
|
|
1777
|
+
version: headSpec.meta?.version,
|
|
1778
|
+
compareUrl: options.repoUrl ? `${options.repoUrl}/compare/${baseSpec.meta?.version ?? "v0"}...${headSpec.meta?.version ?? "HEAD"}` : undefined
|
|
1779
|
+
});
|
|
1780
|
+
if (options.stdout) {
|
|
1781
|
+
log(content);
|
|
1782
|
+
} else {
|
|
1783
|
+
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "md");
|
|
1784
|
+
writeReport({
|
|
1785
|
+
format: "markdown",
|
|
1786
|
+
content,
|
|
1787
|
+
outputPath: outputPath.replace(/\.(json|html)$/, ".changelog.md"),
|
|
1788
|
+
cwd: options.cwd
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1638
1793
|
}
|
|
1639
1794
|
const failures = validateDiff(diff, headSpec, {
|
|
1640
1795
|
minCoverage,
|
|
@@ -1642,29 +1797,29 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1642
1797
|
checks
|
|
1643
1798
|
});
|
|
1644
1799
|
if (failures.length > 0) {
|
|
1645
|
-
log(
|
|
1800
|
+
log(chalk5.red(`
|
|
1646
1801
|
✗ Check failed`));
|
|
1647
1802
|
for (const f of failures) {
|
|
1648
|
-
log(
|
|
1803
|
+
log(chalk5.red(` - ${f}`));
|
|
1649
1804
|
}
|
|
1650
1805
|
process.exitCode = 1;
|
|
1651
1806
|
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
1652
|
-
log(
|
|
1807
|
+
log(chalk5.green(`
|
|
1653
1808
|
✓ All checks passed`));
|
|
1654
1809
|
}
|
|
1655
1810
|
} catch (commandError) {
|
|
1656
|
-
error(
|
|
1811
|
+
error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1657
1812
|
process.exitCode = 1;
|
|
1658
1813
|
}
|
|
1659
1814
|
});
|
|
1660
1815
|
}
|
|
1661
|
-
function
|
|
1816
|
+
function collect2(value, previous) {
|
|
1662
1817
|
return previous.concat([value]);
|
|
1663
1818
|
}
|
|
1664
|
-
async function
|
|
1819
|
+
async function loadMarkdownFiles2(patterns) {
|
|
1665
1820
|
const files = [];
|
|
1666
1821
|
for (const pattern of patterns) {
|
|
1667
|
-
const matches = await
|
|
1822
|
+
const matches = await glob2(pattern, { nodir: true });
|
|
1668
1823
|
for (const filePath of matches) {
|
|
1669
1824
|
try {
|
|
1670
1825
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
@@ -1672,7 +1827,7 @@ async function loadMarkdownFiles(patterns) {
|
|
|
1672
1827
|
} catch {}
|
|
1673
1828
|
}
|
|
1674
1829
|
}
|
|
1675
|
-
return
|
|
1830
|
+
return parseMarkdownFiles2(files);
|
|
1676
1831
|
}
|
|
1677
1832
|
async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
1678
1833
|
let markdownFiles;
|
|
@@ -1680,21 +1835,21 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
1680
1835
|
if (!docsPatterns || docsPatterns.length === 0) {
|
|
1681
1836
|
if (config?.docs?.include) {
|
|
1682
1837
|
docsPatterns = config.docs.include;
|
|
1683
|
-
log(
|
|
1838
|
+
log(chalk5.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
1684
1839
|
}
|
|
1685
1840
|
}
|
|
1686
1841
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
1687
|
-
markdownFiles = await
|
|
1842
|
+
markdownFiles = await loadMarkdownFiles2(docsPatterns);
|
|
1688
1843
|
}
|
|
1689
1844
|
return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
|
|
1690
1845
|
}
|
|
1691
|
-
function loadSpec(filePath,
|
|
1846
|
+
function loadSpec(filePath, readFileSync3) {
|
|
1692
1847
|
const resolvedPath = path5.resolve(filePath);
|
|
1693
1848
|
if (!fs3.existsSync(resolvedPath)) {
|
|
1694
1849
|
throw new Error(`File not found: ${filePath}`);
|
|
1695
1850
|
}
|
|
1696
1851
|
try {
|
|
1697
|
-
const content =
|
|
1852
|
+
const content = readFileSync3(resolvedPath, "utf-8");
|
|
1698
1853
|
const spec = JSON.parse(content);
|
|
1699
1854
|
return ensureSpecCoverage(spec);
|
|
1700
1855
|
} catch (parseError) {
|
|
@@ -1703,60 +1858,40 @@ function loadSpec(filePath, readFileSync2) {
|
|
|
1703
1858
|
}
|
|
1704
1859
|
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1705
1860
|
log("");
|
|
1706
|
-
const cacheIndicator = fromCache ?
|
|
1707
|
-
log(
|
|
1861
|
+
const cacheIndicator = fromCache ? chalk5.cyan(" (cached)") : "";
|
|
1862
|
+
log(chalk5.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
1708
1863
|
log("─".repeat(40));
|
|
1709
1864
|
log("");
|
|
1710
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
1865
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk5.green : diff.coverageDelta < 0 ? chalk5.red : chalk5.gray;
|
|
1711
1866
|
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1712
1867
|
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
1713
1868
|
const breakingCount = diff.breaking.length;
|
|
1714
1869
|
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
1715
1870
|
if (breakingCount > 0) {
|
|
1716
|
-
const severityNote = highSeverity > 0 ?
|
|
1717
|
-
log(` Breaking: ${
|
|
1871
|
+
const severityNote = highSeverity > 0 ? chalk5.red(` (${highSeverity} high severity)`) : "";
|
|
1872
|
+
log(` Breaking: ${chalk5.red(breakingCount)} changes${severityNote}`);
|
|
1718
1873
|
} else {
|
|
1719
|
-
log(` Breaking: ${
|
|
1874
|
+
log(` Breaking: ${chalk5.green("0")} changes`);
|
|
1720
1875
|
}
|
|
1721
1876
|
const newCount = diff.nonBreaking.length;
|
|
1722
1877
|
const undocCount = diff.newUndocumented.length;
|
|
1723
1878
|
if (newCount > 0) {
|
|
1724
|
-
const undocNote = undocCount > 0 ?
|
|
1725
|
-
log(` New: ${
|
|
1879
|
+
const undocNote = undocCount > 0 ? chalk5.yellow(` (${undocCount} undocumented)`) : "";
|
|
1880
|
+
log(` New: ${chalk5.green(newCount)} exports${undocNote}`);
|
|
1726
1881
|
}
|
|
1727
1882
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1728
1883
|
const parts = [];
|
|
1729
1884
|
if (diff.driftIntroduced > 0)
|
|
1730
|
-
parts.push(
|
|
1885
|
+
parts.push(chalk5.red(`+${diff.driftIntroduced}`));
|
|
1731
1886
|
if (diff.driftResolved > 0)
|
|
1732
|
-
parts.push(
|
|
1887
|
+
parts.push(chalk5.green(`-${diff.driftResolved}`));
|
|
1733
1888
|
log(` Drift: ${parts.join(", ")}`);
|
|
1734
1889
|
}
|
|
1890
|
+
const recommendation = recommendSemverBump(diff);
|
|
1891
|
+
const bumpColor = recommendation.bump === "major" ? chalk5.red : recommendation.bump === "minor" ? chalk5.yellow : chalk5.green;
|
|
1892
|
+
log(` Semver: ${bumpColor(recommendation.bump.toUpperCase())} (${recommendation.reason})`);
|
|
1735
1893
|
log("");
|
|
1736
1894
|
}
|
|
1737
|
-
async function printAISummary(diff, log) {
|
|
1738
|
-
if (!isAIDocsAnalysisAvailable()) {
|
|
1739
|
-
log(chalk4.yellow(`
|
|
1740
|
-
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
if (!diff.docsImpact)
|
|
1744
|
-
return;
|
|
1745
|
-
log(chalk4.gray(`
|
|
1746
|
-
Generating AI summary...`));
|
|
1747
|
-
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
1748
|
-
file: f.file,
|
|
1749
|
-
exportName: r.exportName,
|
|
1750
|
-
changeType: r.changeType,
|
|
1751
|
-
context: r.context
|
|
1752
|
-
})));
|
|
1753
|
-
const summary = await generateImpactSummary(impacts);
|
|
1754
|
-
if (summary) {
|
|
1755
|
-
log("");
|
|
1756
|
-
log(chalk4.bold("AI Summary"));
|
|
1757
|
-
log(chalk4.cyan(` ${summary}`));
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
1895
|
function validateDiff(diff, headSpec, options) {
|
|
1761
1896
|
const { minCoverage, maxDrift, checks } = options;
|
|
1762
1897
|
const failures = [];
|
|
@@ -1835,7 +1970,7 @@ function printGitHubAnnotations(diff, log) {
|
|
|
1835
1970
|
|
|
1836
1971
|
// src/commands/info.ts
|
|
1837
1972
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
1838
|
-
import
|
|
1973
|
+
import chalk6 from "chalk";
|
|
1839
1974
|
function registerInfoCommand(program) {
|
|
1840
1975
|
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) => {
|
|
1841
1976
|
try {
|
|
@@ -1857,14 +1992,14 @@ function registerInfoCommand(program) {
|
|
|
1857
1992
|
const spec = enrichSpec2(specResult.spec);
|
|
1858
1993
|
const stats = computeStats(spec);
|
|
1859
1994
|
console.log("");
|
|
1860
|
-
console.log(
|
|
1995
|
+
console.log(chalk6.bold(`${stats.packageName}@${stats.version}`));
|
|
1861
1996
|
console.log("");
|
|
1862
|
-
console.log(` Exports: ${
|
|
1863
|
-
console.log(` Coverage: ${
|
|
1864
|
-
console.log(` Drift: ${
|
|
1997
|
+
console.log(` Exports: ${chalk6.bold(stats.totalExports.toString())}`);
|
|
1998
|
+
console.log(` Coverage: ${chalk6.bold(`${stats.coverageScore}%`)}`);
|
|
1999
|
+
console.log(` Drift: ${chalk6.bold(`${stats.driftScore}%`)}`);
|
|
1865
2000
|
console.log("");
|
|
1866
2001
|
} catch (err) {
|
|
1867
|
-
console.error(
|
|
2002
|
+
console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
|
|
1868
2003
|
process.exit(1);
|
|
1869
2004
|
}
|
|
1870
2005
|
});
|
|
@@ -1873,53 +2008,65 @@ function registerInfoCommand(program) {
|
|
|
1873
2008
|
// src/commands/init.ts
|
|
1874
2009
|
import * as fs4 from "node:fs";
|
|
1875
2010
|
import * as path6 from "node:path";
|
|
1876
|
-
import
|
|
2011
|
+
import chalk7 from "chalk";
|
|
1877
2012
|
var defaultDependencies3 = {
|
|
1878
2013
|
fileExists: fs4.existsSync,
|
|
1879
2014
|
writeFileSync: fs4.writeFileSync,
|
|
1880
2015
|
readFileSync: fs4.readFileSync,
|
|
2016
|
+
mkdirSync: fs4.mkdirSync,
|
|
1881
2017
|
log: console.log,
|
|
1882
2018
|
error: console.error
|
|
1883
2019
|
};
|
|
1884
2020
|
function registerInitCommand(program, dependencies = {}) {
|
|
1885
|
-
const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync:
|
|
2021
|
+
const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync4, mkdirSync: mkdirSync3, log, error } = {
|
|
1886
2022
|
...defaultDependencies3,
|
|
1887
2023
|
...dependencies
|
|
1888
2024
|
};
|
|
1889
|
-
program.command("init").description("
|
|
2025
|
+
program.command("init").description("Initialize DocCov: config, GitHub Action, and badge").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-action", "Skip GitHub Action workflow creation").action((options) => {
|
|
1890
2026
|
const cwd = path6.resolve(options.cwd);
|
|
1891
|
-
const formatOption = String(options.format ?? "auto").toLowerCase();
|
|
1892
|
-
if (!isValidFormat(formatOption)) {
|
|
1893
|
-
error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
|
|
1894
|
-
process.exitCode = 1;
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
2027
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
1898
2028
|
if (existing) {
|
|
1899
|
-
error(
|
|
2029
|
+
error(chalk7.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
1900
2030
|
process.exitCode = 1;
|
|
1901
2031
|
return;
|
|
1902
2032
|
}
|
|
1903
|
-
const packageType = detectPackageType(cwd, fileExists2,
|
|
1904
|
-
const targetFormat =
|
|
1905
|
-
|
|
1906
|
-
log(chalk6.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
|
|
1907
|
-
}
|
|
1908
|
-
const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
|
|
2033
|
+
const packageType = detectPackageType(cwd, fileExists2, readFileSync4);
|
|
2034
|
+
const targetFormat = packageType === "module" ? "ts" : "mts";
|
|
2035
|
+
const fileName = `doccov.config.${targetFormat}`;
|
|
1909
2036
|
const outputPath = path6.join(cwd, fileName);
|
|
1910
2037
|
if (fileExists2(outputPath)) {
|
|
1911
|
-
error(
|
|
2038
|
+
error(chalk7.red(`Cannot create ${fileName}; file already exists.`));
|
|
1912
2039
|
process.exitCode = 1;
|
|
1913
2040
|
return;
|
|
1914
2041
|
}
|
|
1915
|
-
const template =
|
|
2042
|
+
const template = buildConfigTemplate();
|
|
1916
2043
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
1917
|
-
log(
|
|
2044
|
+
log(chalk7.green(`✓ Created ${fileName}`));
|
|
2045
|
+
if (!options.skipAction) {
|
|
2046
|
+
const workflowDir = path6.join(cwd, ".github", "workflows");
|
|
2047
|
+
const workflowPath = path6.join(workflowDir, "doccov.yml");
|
|
2048
|
+
if (!fileExists2(workflowPath)) {
|
|
2049
|
+
mkdirSync3(workflowDir, { recursive: true });
|
|
2050
|
+
writeFileSync3(workflowPath, buildWorkflowTemplate(), { encoding: "utf8" });
|
|
2051
|
+
log(chalk7.green(`✓ Created .github/workflows/doccov.yml`));
|
|
2052
|
+
} else {
|
|
2053
|
+
log(chalk7.yellow(` Skipped .github/workflows/doccov.yml (already exists)`));
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
const repoInfo = detectRepoInfo(cwd, fileExists2, readFileSync4);
|
|
2057
|
+
log("");
|
|
2058
|
+
log(chalk7.bold("Add this badge to your README:"));
|
|
2059
|
+
log("");
|
|
2060
|
+
if (repoInfo) {
|
|
2061
|
+
log(chalk7.cyan(`[](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
|
|
2062
|
+
} else {
|
|
2063
|
+
log(chalk7.cyan(`[](https://doccov.dev/OWNER/REPO)`));
|
|
2064
|
+
log(chalk7.dim(" Replace OWNER/REPO with your GitHub repo"));
|
|
2065
|
+
}
|
|
2066
|
+
log("");
|
|
2067
|
+
log(chalk7.dim("Run `doccov check` to verify your documentation coverage"));
|
|
1918
2068
|
});
|
|
1919
2069
|
}
|
|
1920
|
-
var isValidFormat = (value) => {
|
|
1921
|
-
return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
|
|
1922
|
-
};
|
|
1923
2070
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
1924
2071
|
let current = path6.resolve(cwd);
|
|
1925
2072
|
const { root } = path6.parse(current);
|
|
@@ -1937,13 +2084,13 @@ var findExistingConfig = (cwd, fileExists2) => {
|
|
|
1937
2084
|
}
|
|
1938
2085
|
return null;
|
|
1939
2086
|
};
|
|
1940
|
-
var detectPackageType = (cwd, fileExists2,
|
|
2087
|
+
var detectPackageType = (cwd, fileExists2, readFileSync4) => {
|
|
1941
2088
|
const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
|
|
1942
2089
|
if (!packageJsonPath) {
|
|
1943
2090
|
return;
|
|
1944
2091
|
}
|
|
1945
2092
|
try {
|
|
1946
|
-
const raw =
|
|
2093
|
+
const raw = readFileSync4(packageJsonPath, "utf8");
|
|
1947
2094
|
const parsed = JSON.parse(raw);
|
|
1948
2095
|
if (parsed.type === "module") {
|
|
1949
2096
|
return "module";
|
|
@@ -1969,121 +2116,85 @@ var findNearestPackageJson = (cwd, fileExists2) => {
|
|
|
1969
2116
|
}
|
|
1970
2117
|
return null;
|
|
1971
2118
|
};
|
|
1972
|
-
var
|
|
1973
|
-
|
|
1974
|
-
return "yaml";
|
|
1975
|
-
if (format === "auto") {
|
|
1976
|
-
return packageType === "module" ? "js" : "mjs";
|
|
1977
|
-
}
|
|
1978
|
-
return format;
|
|
1979
|
-
};
|
|
1980
|
-
var buildTemplate = (format) => {
|
|
1981
|
-
if (format === "yaml") {
|
|
1982
|
-
return `# doccov.yml
|
|
1983
|
-
# include:
|
|
1984
|
-
# - "MyClass"
|
|
1985
|
-
# - "myFunction"
|
|
1986
|
-
# exclude:
|
|
1987
|
-
# - "internal*"
|
|
1988
|
-
|
|
1989
|
-
check:
|
|
1990
|
-
# minCoverage: 80
|
|
1991
|
-
# maxDrift: 20
|
|
1992
|
-
# examples: typecheck
|
|
2119
|
+
var buildConfigTemplate = () => {
|
|
2120
|
+
return `import { defineConfig } from '@doccov/cli/config';
|
|
1993
2121
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
# has-description: warn
|
|
1997
|
-
# has-params: off
|
|
1998
|
-
# has-returns: off
|
|
1999
|
-
`;
|
|
2000
|
-
}
|
|
2001
|
-
const configBody = `{
|
|
2002
|
-
// Filter which exports to analyze
|
|
2122
|
+
export default defineConfig({
|
|
2123
|
+
// Filter exports to analyze (optional)
|
|
2003
2124
|
// include: ['MyClass', 'myFunction'],
|
|
2004
2125
|
// exclude: ['internal*'],
|
|
2005
2126
|
|
|
2006
|
-
// Check command thresholds
|
|
2007
2127
|
check: {
|
|
2008
|
-
//
|
|
2009
|
-
|
|
2128
|
+
// Fail if coverage drops below threshold
|
|
2129
|
+
minCoverage: 80,
|
|
2010
2130
|
|
|
2011
|
-
//
|
|
2131
|
+
// Fail if drift exceeds threshold
|
|
2012
2132
|
// maxDrift: 20,
|
|
2013
|
-
|
|
2014
|
-
// Example validation: 'presence' | 'typecheck' | 'run'
|
|
2015
|
-
// examples: 'typecheck',
|
|
2016
2133
|
},
|
|
2134
|
+
});
|
|
2135
|
+
`;
|
|
2136
|
+
};
|
|
2137
|
+
var buildWorkflowTemplate = () => {
|
|
2138
|
+
return `name: DocCov
|
|
2017
2139
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2140
|
+
on:
|
|
2141
|
+
push:
|
|
2142
|
+
branches: [main, master]
|
|
2143
|
+
pull_request:
|
|
2144
|
+
branches: [main, master]
|
|
2145
|
+
|
|
2146
|
+
jobs:
|
|
2147
|
+
doccov:
|
|
2148
|
+
runs-on: ubuntu-latest
|
|
2149
|
+
steps:
|
|
2150
|
+
- uses: actions/checkout@v4
|
|
2151
|
+
- uses: doccov/action@v1
|
|
2152
|
+
with:
|
|
2153
|
+
min-coverage: 80
|
|
2154
|
+
comment-on-pr: true
|
|
2155
|
+
`;
|
|
2156
|
+
};
|
|
2157
|
+
var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
|
|
2158
|
+
const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
|
|
2159
|
+
if (packageJsonPath) {
|
|
2160
|
+
try {
|
|
2161
|
+
const raw = readFileSync4(packageJsonPath, "utf8");
|
|
2162
|
+
const parsed = JSON.parse(raw);
|
|
2163
|
+
let repoUrl;
|
|
2164
|
+
if (typeof parsed.repository === "string") {
|
|
2165
|
+
repoUrl = parsed.repository;
|
|
2166
|
+
} else if (parsed.repository?.url) {
|
|
2167
|
+
repoUrl = parsed.repository.url;
|
|
2168
|
+
}
|
|
2169
|
+
if (repoUrl) {
|
|
2170
|
+
const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
2171
|
+
if (match) {
|
|
2172
|
+
return { owner: match[1], repo: match[2] };
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
} catch {}
|
|
2038
2176
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2177
|
+
const gitConfigPath = path6.join(cwd, ".git", "config");
|
|
2178
|
+
if (fileExists2(gitConfigPath)) {
|
|
2179
|
+
try {
|
|
2180
|
+
const config = readFileSync4(gitConfigPath, "utf8");
|
|
2181
|
+
const match = config.match(/url\s*=\s*.*github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
2182
|
+
if (match) {
|
|
2183
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
2184
|
+
}
|
|
2185
|
+
} catch {}
|
|
2186
|
+
}
|
|
2187
|
+
return null;
|
|
2046
2188
|
};
|
|
2047
2189
|
|
|
2048
2190
|
// src/commands/spec.ts
|
|
2049
2191
|
import * as fs5 from "node:fs";
|
|
2050
2192
|
import * as path7 from "node:path";
|
|
2051
|
-
import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2193
|
+
import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2052
2194
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2053
2195
|
import chalk8 from "chalk";
|
|
2054
2196
|
// package.json
|
|
2055
|
-
var version = "0.
|
|
2056
|
-
|
|
2057
|
-
// src/utils/filter-options.ts
|
|
2058
|
-
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
2059
|
-
import chalk7 from "chalk";
|
|
2060
|
-
var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
|
|
2061
|
-
var mergeFilterOptions = (config, cliOptions) => {
|
|
2062
|
-
const messages = [];
|
|
2063
|
-
if (config?.include) {
|
|
2064
|
-
messages.push(formatList("include filters from config", config.include));
|
|
2065
|
-
}
|
|
2066
|
-
if (config?.exclude) {
|
|
2067
|
-
messages.push(formatList("exclude filters from config", config.exclude));
|
|
2068
|
-
}
|
|
2069
|
-
if (cliOptions.include) {
|
|
2070
|
-
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
2071
|
-
}
|
|
2072
|
-
if (cliOptions.exclude) {
|
|
2073
|
-
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
2074
|
-
}
|
|
2075
|
-
const resolved = mergeFilters(config, cliOptions);
|
|
2076
|
-
if (!resolved.include && !resolved.exclude) {
|
|
2077
|
-
return { messages };
|
|
2078
|
-
}
|
|
2079
|
-
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
2080
|
-
return {
|
|
2081
|
-
include: resolved.include,
|
|
2082
|
-
exclude: resolved.exclude,
|
|
2083
|
-
source,
|
|
2084
|
-
messages
|
|
2085
|
-
};
|
|
2086
|
-
};
|
|
2197
|
+
var version = "0.18.0";
|
|
2087
2198
|
|
|
2088
2199
|
// src/commands/spec.ts
|
|
2089
2200
|
var defaultDependencies4 = {
|
|
@@ -2107,7 +2218,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2107
2218
|
...defaultDependencies4,
|
|
2108
2219
|
...dependencies
|
|
2109
2220
|
};
|
|
2110
|
-
program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").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) => {
|
|
2221
|
+
program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("-f, --format <format>", "Output format: json (default) or api-surface", "json").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) => {
|
|
2111
2222
|
try {
|
|
2112
2223
|
const steps = new StepProgress([
|
|
2113
2224
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
@@ -2135,7 +2246,8 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2135
2246
|
steps.next();
|
|
2136
2247
|
const cliFilters = {
|
|
2137
2248
|
include: parseListFlag(options.include),
|
|
2138
|
-
exclude: parseListFlag(options.exclude)
|
|
2249
|
+
exclude: parseListFlag(options.exclude),
|
|
2250
|
+
visibility: parseVisibilityFlag(options.visibility)
|
|
2139
2251
|
};
|
|
2140
2252
|
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
2141
2253
|
const resolveExternalTypes = !options.skipResolve;
|
|
@@ -2143,7 +2255,8 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2143
2255
|
resolveExternalTypes,
|
|
2144
2256
|
maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
|
|
2145
2257
|
useCache: options.cache !== false,
|
|
2146
|
-
cwd: options.cwd
|
|
2258
|
+
cwd: options.cwd,
|
|
2259
|
+
schemaExtraction: options.runtime ? "hybrid" : "static"
|
|
2147
2260
|
});
|
|
2148
2261
|
const generationInput = {
|
|
2149
2262
|
entryPoint: path7.relative(targetDir, entryFile),
|
|
@@ -2155,10 +2268,11 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2155
2268
|
isMonorepo: resolved.isMonorepo,
|
|
2156
2269
|
targetPackage: packageInfo?.name
|
|
2157
2270
|
};
|
|
2158
|
-
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
|
|
2271
|
+
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
|
|
2159
2272
|
filters: {
|
|
2160
2273
|
include: resolvedFilters.include,
|
|
2161
|
-
exclude: resolvedFilters.exclude
|
|
2274
|
+
exclude: resolvedFilters.exclude,
|
|
2275
|
+
visibility: resolvedFilters.visibility
|
|
2162
2276
|
},
|
|
2163
2277
|
generationInput
|
|
2164
2278
|
} : { generationInput };
|
|
@@ -2177,9 +2291,16 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2177
2291
|
process.exit(1);
|
|
2178
2292
|
}
|
|
2179
2293
|
steps.next();
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2294
|
+
const format = options.format ?? "json";
|
|
2295
|
+
const outputPath = path7.resolve(options.cwd, options.output);
|
|
2296
|
+
if (format === "api-surface") {
|
|
2297
|
+
const apiSurface = renderApiSurface(normalized);
|
|
2298
|
+
writeFileSync4(outputPath, apiSurface);
|
|
2299
|
+
steps.complete(`Generated ${options.output} (API surface)`);
|
|
2300
|
+
} else {
|
|
2301
|
+
writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
|
|
2302
|
+
steps.complete(`Generated ${options.output}`);
|
|
2303
|
+
}
|
|
2183
2304
|
log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
2184
2305
|
log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
|
|
2185
2306
|
if (options.verbose && normalized.generation) {
|
|
@@ -2195,6 +2316,13 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2195
2316
|
if (gen.analysis.maxTypeDepth) {
|
|
2196
2317
|
log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
|
|
2197
2318
|
}
|
|
2319
|
+
if (gen.analysis.schemaExtraction) {
|
|
2320
|
+
const se = gen.analysis.schemaExtraction;
|
|
2321
|
+
log(chalk8.gray(` Schema extraction: ${se.method}`));
|
|
2322
|
+
if (se.runtimeCount) {
|
|
2323
|
+
log(chalk8.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2198
2326
|
log("");
|
|
2199
2327
|
log(chalk8.bold("Environment"));
|
|
2200
2328
|
log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
@@ -2234,10 +2362,195 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2234
2362
|
});
|
|
2235
2363
|
}
|
|
2236
2364
|
|
|
2365
|
+
// src/commands/trends.ts
|
|
2366
|
+
import * as fs6 from "node:fs";
|
|
2367
|
+
import * as path8 from "node:path";
|
|
2368
|
+
import {
|
|
2369
|
+
formatDelta as formatDelta2,
|
|
2370
|
+
getExtendedTrend,
|
|
2371
|
+
getTrend,
|
|
2372
|
+
loadSnapshots,
|
|
2373
|
+
pruneByTier,
|
|
2374
|
+
pruneHistory,
|
|
2375
|
+
renderSparkline,
|
|
2376
|
+
RETENTION_DAYS,
|
|
2377
|
+
saveSnapshot
|
|
2378
|
+
} from "@doccov/sdk";
|
|
2379
|
+
import chalk9 from "chalk";
|
|
2380
|
+
function formatDate(timestamp) {
|
|
2381
|
+
const date = new Date(timestamp);
|
|
2382
|
+
return date.toLocaleDateString("en-US", {
|
|
2383
|
+
month: "short",
|
|
2384
|
+
day: "numeric",
|
|
2385
|
+
year: "numeric",
|
|
2386
|
+
hour: "2-digit",
|
|
2387
|
+
minute: "2-digit"
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
function getColorForScore(score) {
|
|
2391
|
+
if (score >= 90)
|
|
2392
|
+
return chalk9.green;
|
|
2393
|
+
if (score >= 70)
|
|
2394
|
+
return chalk9.yellow;
|
|
2395
|
+
if (score >= 50)
|
|
2396
|
+
return chalk9.hex("#FFA500");
|
|
2397
|
+
return chalk9.red;
|
|
2398
|
+
}
|
|
2399
|
+
function formatSnapshot(snapshot) {
|
|
2400
|
+
const color = getColorForScore(snapshot.coverageScore);
|
|
2401
|
+
const date = formatDate(snapshot.timestamp);
|
|
2402
|
+
const version2 = snapshot.version ? ` v${snapshot.version}` : "";
|
|
2403
|
+
const commit = snapshot.commit ? chalk9.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
|
|
2404
|
+
return `${chalk9.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
|
|
2405
|
+
}
|
|
2406
|
+
function formatWeekDate(timestamp) {
|
|
2407
|
+
const date = new Date(timestamp);
|
|
2408
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2409
|
+
}
|
|
2410
|
+
function formatVelocity(velocity) {
|
|
2411
|
+
if (velocity > 0)
|
|
2412
|
+
return chalk9.green(`+${velocity}%/day`);
|
|
2413
|
+
if (velocity < 0)
|
|
2414
|
+
return chalk9.red(`${velocity}%/day`);
|
|
2415
|
+
return chalk9.gray("0%/day");
|
|
2416
|
+
}
|
|
2417
|
+
function registerTrendsCommand(program) {
|
|
2418
|
+
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("--tier <tier>", "Retention tier: free (7d), team (30d), pro (90d)", "pro").option("--weekly", "Show weekly summary breakdown").action(async (options) => {
|
|
2419
|
+
const cwd = path8.resolve(options.cwd);
|
|
2420
|
+
const tier = options.tier ?? "pro";
|
|
2421
|
+
if (options.prune) {
|
|
2422
|
+
const keepCount = parseInt(options.prune, 10);
|
|
2423
|
+
if (!isNaN(keepCount)) {
|
|
2424
|
+
const deleted = pruneHistory(cwd, keepCount);
|
|
2425
|
+
console.log(chalk9.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
|
|
2426
|
+
} else {
|
|
2427
|
+
const deleted = pruneByTier(cwd, tier);
|
|
2428
|
+
console.log(chalk9.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
|
|
2429
|
+
}
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (options.record) {
|
|
2433
|
+
const specPath = path8.resolve(cwd, "openpkg.json");
|
|
2434
|
+
if (!fs6.existsSync(specPath)) {
|
|
2435
|
+
console.error(chalk9.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
|
|
2436
|
+
process.exit(1);
|
|
2437
|
+
}
|
|
2438
|
+
try {
|
|
2439
|
+
const specContent = fs6.readFileSync(specPath, "utf-8");
|
|
2440
|
+
const spec = JSON.parse(specContent);
|
|
2441
|
+
const trend = getTrend(spec, cwd);
|
|
2442
|
+
saveSnapshot(trend.current, cwd);
|
|
2443
|
+
console.log(chalk9.green("Recorded coverage snapshot:"));
|
|
2444
|
+
console.log(formatSnapshot(trend.current));
|
|
2445
|
+
if (trend.delta !== undefined) {
|
|
2446
|
+
const deltaStr = formatDelta2(trend.delta);
|
|
2447
|
+
const deltaColor = trend.delta > 0 ? chalk9.green : trend.delta < 0 ? chalk9.red : chalk9.gray;
|
|
2448
|
+
console.log(chalk9.gray("Change from previous:"), deltaColor(deltaStr));
|
|
2449
|
+
}
|
|
2450
|
+
return;
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
console.error(chalk9.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
|
|
2453
|
+
process.exit(1);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const snapshots = loadSnapshots(cwd);
|
|
2457
|
+
const limit = parseInt(options.limit ?? "10", 10);
|
|
2458
|
+
if (snapshots.length === 0) {
|
|
2459
|
+
console.log(chalk9.yellow("No coverage history found."));
|
|
2460
|
+
console.log(chalk9.gray("Run `doccov trends --record` to save the current coverage."));
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
if (options.json) {
|
|
2464
|
+
const output = {
|
|
2465
|
+
current: snapshots[0],
|
|
2466
|
+
history: snapshots.slice(1),
|
|
2467
|
+
delta: snapshots.length > 1 ? snapshots[0].coverageScore - snapshots[1].coverageScore : undefined,
|
|
2468
|
+
sparkline: snapshots.slice(0, 10).map((s) => s.coverageScore).reverse()
|
|
2469
|
+
};
|
|
2470
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
|
|
2474
|
+
const sparkline = renderSparkline(sparklineData);
|
|
2475
|
+
console.log(chalk9.bold("Coverage Trends"));
|
|
2476
|
+
console.log(chalk9.gray(`Package: ${snapshots[0].package}`));
|
|
2477
|
+
console.log(chalk9.gray(`Sparkline: ${sparkline}`));
|
|
2478
|
+
console.log("");
|
|
2479
|
+
if (snapshots.length >= 2) {
|
|
2480
|
+
const oldest = snapshots[snapshots.length - 1];
|
|
2481
|
+
const newest = snapshots[0];
|
|
2482
|
+
const overallDelta = newest.coverageScore - oldest.coverageScore;
|
|
2483
|
+
const deltaStr = formatDelta2(overallDelta);
|
|
2484
|
+
const deltaColor = overallDelta > 0 ? chalk9.green : overallDelta < 0 ? chalk9.red : chalk9.gray;
|
|
2485
|
+
console.log(chalk9.gray("Overall trend:"), deltaColor(deltaStr), chalk9.gray(`(${snapshots.length} snapshots)`));
|
|
2486
|
+
console.log("");
|
|
2487
|
+
}
|
|
2488
|
+
if (options.extended) {
|
|
2489
|
+
const specPath = path8.resolve(cwd, "openpkg.json");
|
|
2490
|
+
if (fs6.existsSync(specPath)) {
|
|
2491
|
+
try {
|
|
2492
|
+
const specContent = fs6.readFileSync(specPath, "utf-8");
|
|
2493
|
+
const spec = JSON.parse(specContent);
|
|
2494
|
+
const extended = getExtendedTrend(spec, cwd, { tier });
|
|
2495
|
+
console.log(chalk9.bold("Extended Analysis"));
|
|
2496
|
+
console.log(chalk9.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
|
|
2497
|
+
console.log("");
|
|
2498
|
+
console.log(" Velocity:");
|
|
2499
|
+
console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
|
|
2500
|
+
console.log(` 30-day: ${formatVelocity(extended.velocity30d)}`);
|
|
2501
|
+
if (extended.velocity90d !== undefined) {
|
|
2502
|
+
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
2503
|
+
}
|
|
2504
|
+
console.log("");
|
|
2505
|
+
const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk9.green : chalk9.red;
|
|
2506
|
+
console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
|
|
2507
|
+
console.log(` All-time high: ${chalk9.green(`${extended.allTimeHigh}%`)}`);
|
|
2508
|
+
console.log(` All-time low: ${chalk9.red(`${extended.allTimeLow}%`)}`);
|
|
2509
|
+
if (extended.dataRange) {
|
|
2510
|
+
const startDate = formatWeekDate(extended.dataRange.start);
|
|
2511
|
+
const endDate = formatWeekDate(extended.dataRange.end);
|
|
2512
|
+
console.log(chalk9.gray(` Data range: ${startDate} - ${endDate}`));
|
|
2513
|
+
}
|
|
2514
|
+
console.log("");
|
|
2515
|
+
if (options.weekly && extended.weeklySummaries.length > 0) {
|
|
2516
|
+
console.log(chalk9.bold("Weekly Summary"));
|
|
2517
|
+
const weekLimit = Math.min(extended.weeklySummaries.length, 8);
|
|
2518
|
+
for (let i = 0;i < weekLimit; i++) {
|
|
2519
|
+
const week = extended.weeklySummaries[i];
|
|
2520
|
+
const weekStart = formatWeekDate(week.weekStart);
|
|
2521
|
+
const weekEnd = formatWeekDate(week.weekEnd);
|
|
2522
|
+
const deltaColor = week.delta > 0 ? chalk9.green : week.delta < 0 ? chalk9.red : chalk9.gray;
|
|
2523
|
+
const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
|
|
2524
|
+
console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
|
|
2525
|
+
}
|
|
2526
|
+
if (extended.weeklySummaries.length > weekLimit) {
|
|
2527
|
+
console.log(chalk9.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
|
|
2528
|
+
}
|
|
2529
|
+
console.log("");
|
|
2530
|
+
}
|
|
2531
|
+
} catch {
|
|
2532
|
+
console.log(chalk9.yellow("Could not load openpkg.json for extended analysis"));
|
|
2533
|
+
console.log("");
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
console.log(chalk9.bold("History"));
|
|
2538
|
+
const displaySnapshots = snapshots.slice(0, limit);
|
|
2539
|
+
for (let i = 0;i < displaySnapshots.length; i++) {
|
|
2540
|
+
const snapshot = displaySnapshots[i];
|
|
2541
|
+
const prefix = i === 0 ? chalk9.cyan("→") : " ";
|
|
2542
|
+
console.log(`${prefix} ${formatSnapshot(snapshot)}`);
|
|
2543
|
+
}
|
|
2544
|
+
if (snapshots.length > limit) {
|
|
2545
|
+
console.log(chalk9.gray(` ... and ${snapshots.length - limit} more`));
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2237
2550
|
// src/cli.ts
|
|
2238
2551
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2239
|
-
var __dirname2 =
|
|
2240
|
-
var packageJson = JSON.parse(
|
|
2552
|
+
var __dirname2 = path9.dirname(__filename2);
|
|
2553
|
+
var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
|
|
2241
2554
|
var program = new Command;
|
|
2242
2555
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2243
2556
|
registerCheckCommand(program);
|
|
@@ -2245,6 +2558,7 @@ registerInfoCommand(program);
|
|
|
2245
2558
|
registerSpecCommand(program);
|
|
2246
2559
|
registerDiffCommand(program);
|
|
2247
2560
|
registerInitCommand(program);
|
|
2561
|
+
registerTrendsCommand(program);
|
|
2248
2562
|
program.command("*", { hidden: true }).action(() => {
|
|
2249
2563
|
program.outputHelp();
|
|
2250
2564
|
});
|