@doccov/cli 0.18.0 → 0.20.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 +448 -918
- package/dist/config/index.d.ts +3 -22
- package/dist/config/index.js +4 -35
- package/package.json +2 -2
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,23 +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
|
-
var policyConfigSchema = z.object({
|
|
39
|
-
path: z.string().min(1),
|
|
40
|
-
minCoverage: z.number().min(0).max(100).optional(),
|
|
41
|
-
maxDrift: z.number().min(0).max(100).optional(),
|
|
42
|
-
requireExamples: z.boolean().optional()
|
|
43
|
-
});
|
|
44
33
|
var docCovConfigSchema = z.object({
|
|
45
34
|
include: stringList.optional(),
|
|
46
35
|
exclude: stringList.optional(),
|
|
47
36
|
plugins: z.array(z.unknown()).optional(),
|
|
48
37
|
docs: docsConfigSchema.optional(),
|
|
49
|
-
check: checkConfigSchema.optional()
|
|
50
|
-
quality: qualityConfigSchema.optional(),
|
|
51
|
-
policies: z.array(policyConfigSchema).optional()
|
|
38
|
+
check: checkConfigSchema.optional()
|
|
52
39
|
});
|
|
53
40
|
var normalizeList = (value) => {
|
|
54
41
|
if (!value) {
|
|
@@ -80,21 +67,12 @@ var normalizeConfig = (input) => {
|
|
|
80
67
|
maxDrift: input.check.maxDrift
|
|
81
68
|
};
|
|
82
69
|
}
|
|
83
|
-
let quality;
|
|
84
|
-
if (input.quality) {
|
|
85
|
-
quality = {
|
|
86
|
-
rules: input.quality.rules
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const policies = input.policies;
|
|
90
70
|
return {
|
|
91
71
|
include,
|
|
92
72
|
exclude,
|
|
93
73
|
plugins: input.plugins,
|
|
94
74
|
docs,
|
|
95
|
-
check
|
|
96
|
-
quality,
|
|
97
|
-
policies
|
|
75
|
+
check
|
|
98
76
|
};
|
|
99
77
|
};
|
|
100
78
|
|
|
@@ -102,12 +80,8 @@ var normalizeConfig = (input) => {
|
|
|
102
80
|
var DOCCOV_CONFIG_FILENAMES = [
|
|
103
81
|
"doccov.config.ts",
|
|
104
82
|
"doccov.config.mts",
|
|
105
|
-
"doccov.config.cts",
|
|
106
83
|
"doccov.config.js",
|
|
107
|
-
"doccov.config.mjs"
|
|
108
|
-
"doccov.config.cjs",
|
|
109
|
-
"doccov.yml",
|
|
110
|
-
"doccov.yaml"
|
|
84
|
+
"doccov.config.mjs"
|
|
111
85
|
];
|
|
112
86
|
var fileExists = async (filePath) => {
|
|
113
87
|
try {
|
|
@@ -134,11 +108,6 @@ var findConfigFile = async (cwd) => {
|
|
|
134
108
|
}
|
|
135
109
|
};
|
|
136
110
|
var importConfigModule = async (absolutePath) => {
|
|
137
|
-
const ext = path.extname(absolutePath);
|
|
138
|
-
if (ext === ".yml" || ext === ".yaml") {
|
|
139
|
-
const content = await readFile(absolutePath, "utf-8");
|
|
140
|
-
return parseYaml(content);
|
|
141
|
-
}
|
|
142
111
|
const fileUrl = pathToFileURL(absolutePath);
|
|
143
112
|
fileUrl.searchParams.set("t", Date.now().toString());
|
|
144
113
|
const module = await import(fileUrl.href);
|
|
@@ -177,7 +146,7 @@ ${formatIssues(issues)}`);
|
|
|
177
146
|
// src/config/index.ts
|
|
178
147
|
var defineConfig = (config) => config;
|
|
179
148
|
// src/cli.ts
|
|
180
|
-
import { readFileSync as
|
|
149
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
181
150
|
import * as path9 from "node:path";
|
|
182
151
|
import { fileURLToPath } from "node:url";
|
|
183
152
|
import { Command } from "commander";
|
|
@@ -186,14 +155,11 @@ import { Command } from "commander";
|
|
|
186
155
|
import * as fs2 from "node:fs";
|
|
187
156
|
import * as path4 from "node:path";
|
|
188
157
|
import {
|
|
189
|
-
analyzeSpecContributors,
|
|
190
|
-
analyzeSpecOwnership,
|
|
191
158
|
applyEdits,
|
|
192
159
|
categorizeDrifts,
|
|
193
160
|
createSourceFile,
|
|
194
161
|
DocCov,
|
|
195
162
|
enrichSpec,
|
|
196
|
-
evaluatePolicies,
|
|
197
163
|
findJSDocLocation,
|
|
198
164
|
generateFixesForExport,
|
|
199
165
|
generateReport,
|
|
@@ -201,6 +167,7 @@ import {
|
|
|
201
167
|
NodeFileSystem,
|
|
202
168
|
parseExamplesFlag,
|
|
203
169
|
parseJSDocToPatch,
|
|
170
|
+
parseMarkdownFiles,
|
|
204
171
|
resolveTarget,
|
|
205
172
|
serializeJSDoc,
|
|
206
173
|
validateExamples
|
|
@@ -208,7 +175,8 @@ import {
|
|
|
208
175
|
import {
|
|
209
176
|
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
210
177
|
} from "@openpkg-ts/spec";
|
|
211
|
-
import
|
|
178
|
+
import chalk4 from "chalk";
|
|
179
|
+
import { glob } from "glob";
|
|
212
180
|
|
|
213
181
|
// src/reports/changelog-renderer.ts
|
|
214
182
|
function renderChangelog(data, options = {}) {
|
|
@@ -649,7 +617,8 @@ function renderPRComment(data, opts = {}) {
|
|
|
649
617
|
const { diff, headSpec } = data;
|
|
650
618
|
const limit = opts.limit ?? 10;
|
|
651
619
|
const lines = [];
|
|
652
|
-
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;
|
|
653
622
|
const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
|
|
654
623
|
lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
|
|
655
624
|
lines.push("");
|
|
@@ -661,6 +630,13 @@ function renderPRComment(data, opts = {}) {
|
|
|
661
630
|
if (diff.driftIntroduced > 0) {
|
|
662
631
|
lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
|
|
663
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
|
+
}
|
|
664
640
|
if (diff.newUndocumented.length > 0) {
|
|
665
641
|
lines.push("");
|
|
666
642
|
lines.push("### Undocumented exports in this PR");
|
|
@@ -673,7 +649,15 @@ function renderPRComment(data, opts = {}) {
|
|
|
673
649
|
lines.push("");
|
|
674
650
|
renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
|
|
675
651
|
}
|
|
676
|
-
|
|
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);
|
|
677
661
|
if (fixGuidance) {
|
|
678
662
|
lines.push("");
|
|
679
663
|
lines.push("### How to fix");
|
|
@@ -687,6 +671,16 @@ function renderPRComment(data, opts = {}) {
|
|
|
687
671
|
renderDetailsTable(lines, diff);
|
|
688
672
|
lines.push("");
|
|
689
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
|
+
}
|
|
690
684
|
return lines.join(`
|
|
691
685
|
`);
|
|
692
686
|
}
|
|
@@ -807,15 +801,47 @@ function getMissingSignals(exp) {
|
|
|
807
801
|
}
|
|
808
802
|
return missing;
|
|
809
803
|
}
|
|
810
|
-
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) {
|
|
811
830
|
const sections = [];
|
|
812
831
|
if (diff.newUndocumented.length > 0) {
|
|
813
832
|
sections.push(`**For undocumented exports:**
|
|
814
833
|
` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
|
|
815
834
|
}
|
|
816
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).` : "";
|
|
817
839
|
sections.push(`**For doc drift:**
|
|
818
|
-
` + "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.");
|
|
819
845
|
}
|
|
820
846
|
if (diff.breaking.length > 0) {
|
|
821
847
|
sections.push(`**For breaking changes:**
|
|
@@ -978,275 +1004,9 @@ function writeReports(options) {
|
|
|
978
1004
|
}));
|
|
979
1005
|
return results;
|
|
980
1006
|
}
|
|
981
|
-
// src/utils/ai-client.ts
|
|
982
|
-
import chalk2 from "chalk";
|
|
983
|
-
|
|
984
|
-
// src/utils/ai-generate.ts
|
|
985
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
986
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
987
|
-
import { generateObject } from "ai";
|
|
988
|
-
import { z as z2 } from "zod";
|
|
989
|
-
var JSDocGenerationSchema = z2.object({
|
|
990
|
-
description: z2.string().describe("1-2 sentence description of what this does"),
|
|
991
|
-
params: z2.array(z2.object({
|
|
992
|
-
name: z2.string(),
|
|
993
|
-
type: z2.string().optional(),
|
|
994
|
-
description: z2.string()
|
|
995
|
-
})).optional().describe("Parameter descriptions for functions"),
|
|
996
|
-
returns: z2.object({
|
|
997
|
-
type: z2.string().optional(),
|
|
998
|
-
description: z2.string()
|
|
999
|
-
}).optional().describe("Return value description for functions"),
|
|
1000
|
-
example: z2.string().optional().describe("Working code example showing typical usage"),
|
|
1001
|
-
typeParams: z2.array(z2.object({
|
|
1002
|
-
name: z2.string(),
|
|
1003
|
-
description: z2.string()
|
|
1004
|
-
})).optional().describe("Type parameter descriptions for generics")
|
|
1005
|
-
});
|
|
1006
|
-
function getModel() {
|
|
1007
|
-
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
1008
|
-
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
1009
|
-
const anthropic = createAnthropic();
|
|
1010
|
-
return anthropic("claude-sonnet-4-20250514");
|
|
1011
|
-
}
|
|
1012
|
-
const openai = createOpenAI();
|
|
1013
|
-
return openai("gpt-4o-mini");
|
|
1014
|
-
}
|
|
1015
|
-
function isAIGenerationAvailable() {
|
|
1016
|
-
return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
1017
|
-
}
|
|
1018
|
-
function buildSignature(exp) {
|
|
1019
|
-
if (exp.signatures && exp.signatures.length > 0) {
|
|
1020
|
-
const sig = exp.signatures[0];
|
|
1021
|
-
const params = sig.parameters?.map((p) => `${p.name}: ${p.type ?? "unknown"}`).join(", ") ?? "";
|
|
1022
|
-
const ret = sig.returnType ?? "void";
|
|
1023
|
-
return `(${params}) => ${ret}`;
|
|
1024
|
-
}
|
|
1025
|
-
if (exp.type) {
|
|
1026
|
-
return typeof exp.type === "string" ? exp.type : JSON.stringify(exp.type);
|
|
1027
|
-
}
|
|
1028
|
-
return exp.kind;
|
|
1029
|
-
}
|
|
1030
|
-
function buildMembersContext(exp) {
|
|
1031
|
-
if (!exp.members || exp.members.length === 0)
|
|
1032
|
-
return "";
|
|
1033
|
-
const memberLines = exp.members.slice(0, 10).map((m) => {
|
|
1034
|
-
const type = m.type ? `: ${typeof m.type === "string" ? m.type : "object"}` : "";
|
|
1035
|
-
return ` - ${m.name}${type}`;
|
|
1036
|
-
});
|
|
1037
|
-
if (exp.members.length > 10) {
|
|
1038
|
-
memberLines.push(` - ... and ${exp.members.length - 10} more members`);
|
|
1039
|
-
}
|
|
1040
|
-
return `
|
|
1041
|
-
|
|
1042
|
-
Members:
|
|
1043
|
-
${memberLines.join(`
|
|
1044
|
-
`)}`;
|
|
1045
|
-
}
|
|
1046
|
-
async function generateJSDocForExport(exp) {
|
|
1047
|
-
if (!isAIGenerationAvailable()) {
|
|
1048
|
-
return null;
|
|
1049
|
-
}
|
|
1050
|
-
const signature = buildSignature(exp);
|
|
1051
|
-
const membersContext = buildMembersContext(exp);
|
|
1052
|
-
const prompt = `Generate JSDoc documentation for this TypeScript export.
|
|
1053
|
-
|
|
1054
|
-
Name: ${exp.name}
|
|
1055
|
-
Kind: ${exp.kind}
|
|
1056
|
-
Signature: ${signature}${membersContext}
|
|
1057
|
-
|
|
1058
|
-
Requirements:
|
|
1059
|
-
- Description: 1-2 sentences explaining what this does and when to use it
|
|
1060
|
-
- For functions: describe each parameter and return value
|
|
1061
|
-
- Example: provide a working code snippet showing typical usage
|
|
1062
|
-
- Be concise but informative
|
|
1063
|
-
|
|
1064
|
-
The documentation should help developers understand the purpose and usage at a glance.`;
|
|
1065
|
-
try {
|
|
1066
|
-
const { object } = await generateObject({
|
|
1067
|
-
model: getModel(),
|
|
1068
|
-
schema: JSDocGenerationSchema,
|
|
1069
|
-
prompt
|
|
1070
|
-
});
|
|
1071
|
-
return object;
|
|
1072
|
-
} catch {
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
function toJSDocPatch(result) {
|
|
1077
|
-
const patch = {};
|
|
1078
|
-
if (result.description) {
|
|
1079
|
-
patch.description = result.description;
|
|
1080
|
-
}
|
|
1081
|
-
if (result.params && result.params.length > 0) {
|
|
1082
|
-
patch.params = result.params.map((p) => ({
|
|
1083
|
-
name: p.name,
|
|
1084
|
-
type: p.type,
|
|
1085
|
-
description: p.description
|
|
1086
|
-
}));
|
|
1087
|
-
}
|
|
1088
|
-
if (result.returns) {
|
|
1089
|
-
patch.returns = {
|
|
1090
|
-
type: result.returns.type,
|
|
1091
|
-
description: result.returns.description
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
if (result.example) {
|
|
1095
|
-
patch.examples = [result.example];
|
|
1096
|
-
}
|
|
1097
|
-
if (result.typeParams && result.typeParams.length > 0) {
|
|
1098
|
-
patch.typeParams = result.typeParams.map((tp) => ({
|
|
1099
|
-
name: tp.name,
|
|
1100
|
-
description: tp.description
|
|
1101
|
-
}));
|
|
1102
|
-
}
|
|
1103
|
-
return patch;
|
|
1104
|
-
}
|
|
1105
|
-
async function batchGenerateJSDocs(exports, options = {}) {
|
|
1106
|
-
const results = [];
|
|
1107
|
-
const { maxConcurrent = 3, onProgress } = options;
|
|
1108
|
-
for (let i = 0;i < exports.length; i += maxConcurrent) {
|
|
1109
|
-
const batch = exports.slice(i, i + maxConcurrent);
|
|
1110
|
-
const promises = batch.map(async (exp) => {
|
|
1111
|
-
const generated = await generateJSDocForExport(exp);
|
|
1112
|
-
if (generated) {
|
|
1113
|
-
onProgress?.(results.length + 1, exports.length, exp.name);
|
|
1114
|
-
return {
|
|
1115
|
-
exportName: exp.name,
|
|
1116
|
-
patch: toJSDocPatch(generated),
|
|
1117
|
-
generated: true
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
return {
|
|
1121
|
-
exportName: exp.name,
|
|
1122
|
-
patch: {},
|
|
1123
|
-
generated: false
|
|
1124
|
-
};
|
|
1125
|
-
});
|
|
1126
|
-
const batchResults = await Promise.all(promises);
|
|
1127
|
-
results.push(...batchResults);
|
|
1128
|
-
}
|
|
1129
|
-
return results;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// src/utils/ai-client.ts
|
|
1133
|
-
var API_BASE = process.env.DOCCOV_API_URL || "https://api.doccov.com";
|
|
1134
|
-
function isHostedAPIAvailable() {
|
|
1135
|
-
return Boolean(process.env.DOCCOV_API_KEY);
|
|
1136
|
-
}
|
|
1137
|
-
async function generateViaHostedAPI(exports, packageName) {
|
|
1138
|
-
const apiKey = process.env.DOCCOV_API_KEY;
|
|
1139
|
-
if (!apiKey) {
|
|
1140
|
-
return { error: "No DOCCOV_API_KEY set" };
|
|
1141
|
-
}
|
|
1142
|
-
try {
|
|
1143
|
-
const response = await fetch(`${API_BASE}/v1/ai/generate`, {
|
|
1144
|
-
method: "POST",
|
|
1145
|
-
headers: {
|
|
1146
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1147
|
-
"Content-Type": "application/json"
|
|
1148
|
-
},
|
|
1149
|
-
body: JSON.stringify({
|
|
1150
|
-
exports: exports.map((exp) => ({
|
|
1151
|
-
name: exp.name,
|
|
1152
|
-
kind: exp.kind,
|
|
1153
|
-
signature: buildSignature2(exp),
|
|
1154
|
-
members: exp.members?.slice(0, 10).map((m) => ({
|
|
1155
|
-
name: m.name,
|
|
1156
|
-
type: typeof m.type === "string" ? m.type : undefined
|
|
1157
|
-
}))
|
|
1158
|
-
})),
|
|
1159
|
-
packageName
|
|
1160
|
-
})
|
|
1161
|
-
});
|
|
1162
|
-
if (response.status === 429) {
|
|
1163
|
-
const data = await response.json();
|
|
1164
|
-
return {
|
|
1165
|
-
error: data.error || "Monthly AI limit reached",
|
|
1166
|
-
quotaExceeded: true
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
if (!response.ok) {
|
|
1170
|
-
const data = await response.json();
|
|
1171
|
-
return { error: data.error || "API request failed" };
|
|
1172
|
-
}
|
|
1173
|
-
return await response.json();
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
return { error: err instanceof Error ? err.message : "Network error" };
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
function buildSignature2(exp) {
|
|
1179
|
-
if (exp.signatures && exp.signatures.length > 0) {
|
|
1180
|
-
const sig = exp.signatures[0];
|
|
1181
|
-
const params = sig.parameters?.map((p) => `${p.name}: ${p.type ?? "unknown"}`).join(", ") ?? "";
|
|
1182
|
-
const ret = sig.returnType ?? "void";
|
|
1183
|
-
return `(${params}) => ${ret}`;
|
|
1184
|
-
}
|
|
1185
|
-
if (exp.type) {
|
|
1186
|
-
return typeof exp.type === "string" ? exp.type : JSON.stringify(exp.type);
|
|
1187
|
-
}
|
|
1188
|
-
return exp.kind;
|
|
1189
|
-
}
|
|
1190
|
-
async function generateWithFallback(exports, options = {}) {
|
|
1191
|
-
const { log = console.log } = options;
|
|
1192
|
-
if (isHostedAPIAvailable()) {
|
|
1193
|
-
log(chalk2.dim("Using DocCov hosted AI..."));
|
|
1194
|
-
const hostedResult = await generateViaHostedAPI(exports, options.packageName);
|
|
1195
|
-
if ("error" in hostedResult) {
|
|
1196
|
-
if (hostedResult.quotaExceeded) {
|
|
1197
|
-
log(chalk2.yellow(`
|
|
1198
|
-
⚠ ${hostedResult.error}`));
|
|
1199
|
-
log(chalk2.dim("Falling back to local API keys..."));
|
|
1200
|
-
if (isAIGenerationAvailable()) {
|
|
1201
|
-
return await generateViaBYOK(exports, options);
|
|
1202
|
-
}
|
|
1203
|
-
log(chalk2.red("No local API keys configured."));
|
|
1204
|
-
log(chalk2.dim("Set OPENAI_API_KEY or ANTHROPIC_API_KEY for unlimited generation."));
|
|
1205
|
-
return { results: [], source: "byok" };
|
|
1206
|
-
}
|
|
1207
|
-
log(chalk2.yellow(`
|
|
1208
|
-
⚠ Hosted API error: ${hostedResult.error}`));
|
|
1209
|
-
log(chalk2.dim("Falling back to local API keys..."));
|
|
1210
|
-
} else {
|
|
1211
|
-
const results = hostedResult.results.map((r) => ({
|
|
1212
|
-
exportName: r.name,
|
|
1213
|
-
patch: r.patch ?? {},
|
|
1214
|
-
generated: r.patch !== null
|
|
1215
|
-
}));
|
|
1216
|
-
return {
|
|
1217
|
-
results,
|
|
1218
|
-
source: "hosted",
|
|
1219
|
-
quotaRemaining: hostedResult.quota.remaining,
|
|
1220
|
-
quotaResetAt: hostedResult.quota.resetAt
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (isAIGenerationAvailable()) {
|
|
1225
|
-
return await generateViaBYOK(exports, options);
|
|
1226
|
-
}
|
|
1227
|
-
log(chalk2.yellow(`
|
|
1228
|
-
⚠ No AI configuration available.`));
|
|
1229
|
-
log(chalk2.dim("Options:"));
|
|
1230
|
-
log(chalk2.dim(" 1. Set DOCCOV_API_KEY for hosted AI (included with Team/Pro plan)"));
|
|
1231
|
-
log(chalk2.dim(" 2. Set OPENAI_API_KEY or ANTHROPIC_API_KEY for direct API access"));
|
|
1232
|
-
return { results: [], source: "byok" };
|
|
1233
|
-
}
|
|
1234
|
-
async function generateViaBYOK(exports, options) {
|
|
1235
|
-
const { log = console.log } = options;
|
|
1236
|
-
log(chalk2.dim("Using local API keys (BYOK)..."));
|
|
1237
|
-
const results = await batchGenerateJSDocs(exports, {
|
|
1238
|
-
maxConcurrent: options.maxConcurrent ?? 3,
|
|
1239
|
-
onProgress: options.onProgress
|
|
1240
|
-
});
|
|
1241
|
-
return {
|
|
1242
|
-
results,
|
|
1243
|
-
source: "byok"
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1007
|
// src/utils/filter-options.ts
|
|
1248
1008
|
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1249
|
-
import
|
|
1009
|
+
import chalk2 from "chalk";
|
|
1250
1010
|
var parseVisibilityFlag = (value) => {
|
|
1251
1011
|
if (!value)
|
|
1252
1012
|
return;
|
|
@@ -1263,7 +1023,7 @@ var parseVisibilityFlag = (value) => {
|
|
|
1263
1023
|
}
|
|
1264
1024
|
return result.length > 0 ? result : undefined;
|
|
1265
1025
|
};
|
|
1266
|
-
var formatList = (label, values) => `${label}: ${values.map((value) =>
|
|
1026
|
+
var formatList = (label, values) => `${label}: ${values.map((value) => chalk2.cyan(value)).join(", ")}`;
|
|
1267
1027
|
var mergeFilterOptions = (config, cliOptions) => {
|
|
1268
1028
|
const messages = [];
|
|
1269
1029
|
if (config?.include) {
|
|
@@ -1295,76 +1055,8 @@ var mergeFilterOptions = (config, cliOptions) => {
|
|
|
1295
1055
|
};
|
|
1296
1056
|
};
|
|
1297
1057
|
|
|
1298
|
-
// src/utils/llm-assertion-parser.ts
|
|
1299
|
-
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
1300
|
-
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
1301
|
-
import { generateObject as generateObject2 } from "ai";
|
|
1302
|
-
import { z as z3 } from "zod";
|
|
1303
|
-
var AssertionParseSchema = z3.object({
|
|
1304
|
-
assertions: z3.array(z3.object({
|
|
1305
|
-
lineNumber: z3.number().describe("1-indexed line number where the assertion appears"),
|
|
1306
|
-
expected: z3.string().describe("The expected output value"),
|
|
1307
|
-
originalComment: z3.string().describe("The original comment text"),
|
|
1308
|
-
suggestedSyntax: z3.string().describe("The line rewritten with standard // => value syntax")
|
|
1309
|
-
})).describe("List of assertion-like comments found in the code"),
|
|
1310
|
-
hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
|
|
1311
|
-
});
|
|
1312
|
-
var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
|
|
1313
|
-
|
|
1314
|
-
Look for comments that appear to specify expected output values, such as:
|
|
1315
|
-
- "// should be 3"
|
|
1316
|
-
- "// returns 5"
|
|
1317
|
-
- "// outputs: hello"
|
|
1318
|
-
- "// expected: [1, 2, 3]"
|
|
1319
|
-
- "// 42" (bare value after console.log)
|
|
1320
|
-
- "// result: true"
|
|
1321
|
-
|
|
1322
|
-
Do NOT include:
|
|
1323
|
-
- Regular code comments that explain what the code does
|
|
1324
|
-
- Comments that are instructions or documentation
|
|
1325
|
-
- Comments with // => (already using standard syntax)
|
|
1326
|
-
|
|
1327
|
-
For each assertion found, extract:
|
|
1328
|
-
1. The line number (1-indexed)
|
|
1329
|
-
2. The expected value (just the value, not the comment prefix)
|
|
1330
|
-
3. The original comment text
|
|
1331
|
-
4. A suggested rewrite of the ENTIRE line using "// => value" syntax
|
|
1332
|
-
|
|
1333
|
-
Code:
|
|
1334
|
-
\`\`\`
|
|
1335
|
-
${code}
|
|
1336
|
-
\`\`\``;
|
|
1337
|
-
function getModel2() {
|
|
1338
|
-
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
1339
|
-
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
1340
|
-
const anthropic = createAnthropic2();
|
|
1341
|
-
return anthropic("claude-sonnet-4-20250514");
|
|
1342
|
-
}
|
|
1343
|
-
const openai = createOpenAI2();
|
|
1344
|
-
return openai("gpt-4o-mini");
|
|
1345
|
-
}
|
|
1346
|
-
function isLLMAssertionParsingAvailable() {
|
|
1347
|
-
return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
1348
|
-
}
|
|
1349
|
-
async function parseAssertionsWithLLM(code) {
|
|
1350
|
-
if (!isLLMAssertionParsingAvailable()) {
|
|
1351
|
-
return null;
|
|
1352
|
-
}
|
|
1353
|
-
try {
|
|
1354
|
-
const model = getModel2();
|
|
1355
|
-
const { object } = await generateObject2({
|
|
1356
|
-
model,
|
|
1357
|
-
schema: AssertionParseSchema,
|
|
1358
|
-
prompt: ASSERTION_PARSE_PROMPT(code)
|
|
1359
|
-
});
|
|
1360
|
-
return object;
|
|
1361
|
-
} catch {
|
|
1362
|
-
return null;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
1058
|
// src/utils/progress.ts
|
|
1367
|
-
import
|
|
1059
|
+
import chalk3 from "chalk";
|
|
1368
1060
|
class StepProgress {
|
|
1369
1061
|
steps;
|
|
1370
1062
|
currentStep = 0;
|
|
@@ -1392,7 +1084,7 @@ class StepProgress {
|
|
|
1392
1084
|
this.completeCurrentStep();
|
|
1393
1085
|
if (message) {
|
|
1394
1086
|
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
1395
|
-
console.log(`${
|
|
1087
|
+
console.log(`${chalk3.green("✓")} ${message} ${chalk3.dim(`(${elapsed}s)`)}`);
|
|
1396
1088
|
}
|
|
1397
1089
|
}
|
|
1398
1090
|
render() {
|
|
@@ -1400,17 +1092,17 @@ class StepProgress {
|
|
|
1400
1092
|
if (!step)
|
|
1401
1093
|
return;
|
|
1402
1094
|
const label = step.activeLabel ?? step.label;
|
|
1403
|
-
const prefix =
|
|
1404
|
-
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)}...`);
|
|
1405
1097
|
}
|
|
1406
1098
|
completeCurrentStep() {
|
|
1407
1099
|
const step = this.steps[this.currentStep];
|
|
1408
1100
|
if (!step)
|
|
1409
1101
|
return;
|
|
1410
1102
|
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
1411
|
-
const prefix =
|
|
1103
|
+
const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1412
1104
|
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
1413
|
-
console.log(`${prefix} ${step.label} ${
|
|
1105
|
+
console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
|
|
1414
1106
|
}
|
|
1415
1107
|
}
|
|
1416
1108
|
|
|
@@ -1454,10 +1146,10 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1454
1146
|
...defaultDependencies,
|
|
1455
1147
|
...dependencies
|
|
1456
1148
|
};
|
|
1457
|
-
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) => {
|
|
1458
1150
|
try {
|
|
1459
|
-
|
|
1460
|
-
|
|
1151
|
+
let validations = parseExamplesFlag(options.examples);
|
|
1152
|
+
let hasExamples = validations.length > 0;
|
|
1461
1153
|
const stepList = [
|
|
1462
1154
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
1463
1155
|
{ label: "Loaded config", activeLabel: "Loading config" },
|
|
@@ -1477,8 +1169,18 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1477
1169
|
const { targetDir, entryFile } = resolved;
|
|
1478
1170
|
steps.next();
|
|
1479
1171
|
const config = await loadDocCovConfig(targetDir);
|
|
1480
|
-
|
|
1481
|
-
|
|
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);
|
|
1482
1184
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
1483
1185
|
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
1484
1186
|
const cliFilters = {
|
|
@@ -1488,7 +1190,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1488
1190
|
};
|
|
1489
1191
|
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
1490
1192
|
if (resolvedFilters.visibility) {
|
|
1491
|
-
log(
|
|
1193
|
+
log(chalk4.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
|
|
1492
1194
|
}
|
|
1493
1195
|
steps.next();
|
|
1494
1196
|
const resolveExternalTypes = !options.skipResolve;
|
|
@@ -1510,31 +1212,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1510
1212
|
steps.next();
|
|
1511
1213
|
const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
|
|
1512
1214
|
const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1515
|
-
for (const exp of spec.exports ?? []) {
|
|
1516
|
-
for (const v of exp.docs?.violations ?? []) {
|
|
1517
|
-
violations.push({ exportName: exp.name, violation: v });
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
let policyResult;
|
|
1521
|
-
if (config?.policies && config.policies.length > 0) {
|
|
1522
|
-
policyResult = evaluatePolicies(config.policies, spec, {
|
|
1523
|
-
baseDir: targetDir
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
let ownershipResult;
|
|
1527
|
-
if (options.owners) {
|
|
1528
|
-
ownershipResult = analyzeSpecOwnership(spec, {
|
|
1529
|
-
baseDir: targetDir
|
|
1530
|
-
}) ?? undefined;
|
|
1531
|
-
}
|
|
1532
|
-
let contributorResult;
|
|
1533
|
-
if (options.contributors) {
|
|
1534
|
-
contributorResult = analyzeSpecContributors(spec, {
|
|
1535
|
-
baseDir: targetDir
|
|
1536
|
-
}) ?? undefined;
|
|
1537
|
-
}
|
|
1215
|
+
const isPreview = options.preview || options.dryRun;
|
|
1216
|
+
const shouldFix = options.fix || options.write || isPreview;
|
|
1538
1217
|
let exampleResult;
|
|
1539
1218
|
const typecheckErrors = [];
|
|
1540
1219
|
const runtimeDrifts = [];
|
|
@@ -1544,11 +1223,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1544
1223
|
packagePath: targetDir,
|
|
1545
1224
|
exportNames: (spec.exports ?? []).map((e) => e.name),
|
|
1546
1225
|
timeout: 5000,
|
|
1547
|
-
installTimeout: 60000
|
|
1548
|
-
llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
|
|
1549
|
-
const result = await parseAssertionsWithLLM(example);
|
|
1550
|
-
return result;
|
|
1551
|
-
} : undefined
|
|
1226
|
+
installTimeout: 60000
|
|
1552
1227
|
});
|
|
1553
1228
|
if (exampleResult.typecheck) {
|
|
1554
1229
|
for (const err of exampleResult.typecheck.errors) {
|
|
@@ -1571,6 +1246,41 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1571
1246
|
}
|
|
1572
1247
|
steps.next();
|
|
1573
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
|
+
}
|
|
1574
1284
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
1575
1285
|
const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
1576
1286
|
let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
|
|
@@ -1580,12 +1290,12 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1580
1290
|
if (allDrifts.length > 0) {
|
|
1581
1291
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1582
1292
|
if (fixable.length === 0) {
|
|
1583
|
-
log(
|
|
1293
|
+
log(chalk4.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1584
1294
|
} else {
|
|
1585
1295
|
log("");
|
|
1586
|
-
log(
|
|
1296
|
+
log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
1587
1297
|
if (nonFixable.length > 0) {
|
|
1588
|
-
log(
|
|
1298
|
+
log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
1589
1299
|
}
|
|
1590
1300
|
log("");
|
|
1591
1301
|
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
@@ -1593,22 +1303,22 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1593
1303
|
const editsByFile = new Map;
|
|
1594
1304
|
for (const [exp, drifts] of groupedDrifts) {
|
|
1595
1305
|
if (!exp.source?.file) {
|
|
1596
|
-
log(
|
|
1306
|
+
log(chalk4.gray(` Skipping ${exp.name}: no source location`));
|
|
1597
1307
|
continue;
|
|
1598
1308
|
}
|
|
1599
1309
|
if (exp.source.file.endsWith(".d.ts")) {
|
|
1600
|
-
log(
|
|
1310
|
+
log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
|
|
1601
1311
|
continue;
|
|
1602
1312
|
}
|
|
1603
1313
|
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1604
1314
|
if (!fs2.existsSync(filePath)) {
|
|
1605
|
-
log(
|
|
1315
|
+
log(chalk4.gray(` Skipping ${exp.name}: file not found`));
|
|
1606
1316
|
continue;
|
|
1607
1317
|
}
|
|
1608
1318
|
const sourceFile = createSourceFile(filePath);
|
|
1609
1319
|
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1610
1320
|
if (!location) {
|
|
1611
|
-
log(
|
|
1321
|
+
log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
1612
1322
|
continue;
|
|
1613
1323
|
}
|
|
1614
1324
|
let existingPatch = {};
|
|
@@ -1640,129 +1350,63 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1640
1350
|
editsByFile.set(filePath, fileEdits);
|
|
1641
1351
|
}
|
|
1642
1352
|
if (edits.length > 0) {
|
|
1643
|
-
if (
|
|
1644
|
-
log(
|
|
1353
|
+
if (isPreview) {
|
|
1354
|
+
log(chalk4.bold("Preview - changes that would be made:"));
|
|
1645
1355
|
log("");
|
|
1646
1356
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
1647
1357
|
const relativePath = path4.relative(targetDir, filePath);
|
|
1648
|
-
log(chalk5.cyan(` ${relativePath}:`));
|
|
1649
1358
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
1650
|
-
|
|
1651
|
-
log(`
|
|
1652
|
-
|
|
1653
|
-
|
|
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
|
+
}
|
|
1654
1379
|
}
|
|
1380
|
+
log("");
|
|
1381
|
+
log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
|
|
1382
|
+
log("");
|
|
1655
1383
|
}
|
|
1656
|
-
log("");
|
|
1657
1384
|
}
|
|
1658
|
-
|
|
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."));
|
|
1659
1388
|
} else {
|
|
1660
1389
|
const applyResult = await applyEdits(edits);
|
|
1661
1390
|
if (applyResult.errors.length > 0) {
|
|
1662
1391
|
for (const err of applyResult.errors) {
|
|
1663
|
-
error(
|
|
1392
|
+
error(chalk4.red(` ${err.file}: ${err.error}`));
|
|
1664
1393
|
}
|
|
1665
1394
|
}
|
|
1666
|
-
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
if (!options.dryRun) {
|
|
1671
|
-
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
const generatedExportKeys = new Set;
|
|
1675
|
-
if (shouldFix && options.generate) {
|
|
1676
|
-
const hasAIAccess = isHostedAPIAvailable() || isAIGenerationAvailable();
|
|
1677
|
-
if (!hasAIAccess) {
|
|
1678
|
-
log("");
|
|
1679
|
-
log(chalk5.yellow("⚠ --generate requires AI access. Options:"));
|
|
1680
|
-
log(chalk5.dim(" 1. Set DOCCOV_API_KEY for hosted AI (included with Team/Pro plan)"));
|
|
1681
|
-
log(chalk5.dim(" 2. Set OPENAI_API_KEY or ANTHROPIC_API_KEY for direct API access"));
|
|
1682
|
-
} else {
|
|
1683
|
-
const undocumented = (spec.exports ?? []).filter((exp) => {
|
|
1684
|
-
if (exp.description)
|
|
1685
|
-
return false;
|
|
1686
|
-
if (exp.source?.file?.endsWith(".d.ts"))
|
|
1687
|
-
return false;
|
|
1688
|
-
if (!exp.source?.file)
|
|
1689
|
-
return false;
|
|
1690
|
-
return true;
|
|
1691
|
-
});
|
|
1692
|
-
if (undocumented.length > 0) {
|
|
1693
|
-
log("");
|
|
1694
|
-
log(chalk5.bold(`Generating JSDoc for ${undocumented.length} undocumented export(s)`));
|
|
1695
|
-
log("");
|
|
1696
|
-
const aiResult = await generateWithFallback(undocumented, {
|
|
1697
|
-
maxConcurrent: 3,
|
|
1698
|
-
onProgress: (completed, total, name) => {
|
|
1699
|
-
log(chalk5.dim(` [${completed}/${total}] ${name}`));
|
|
1700
|
-
},
|
|
1701
|
-
log,
|
|
1702
|
-
packageName: spec.meta?.name
|
|
1703
|
-
});
|
|
1704
|
-
const generated = aiResult.results;
|
|
1705
|
-
const edits = [];
|
|
1706
|
-
for (const result of generated) {
|
|
1707
|
-
if (!result.generated)
|
|
1708
|
-
continue;
|
|
1709
|
-
const exp = undocumented.find((e) => e.name === result.exportName);
|
|
1710
|
-
if (!exp || !exp.source?.file)
|
|
1711
|
-
continue;
|
|
1712
|
-
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1713
|
-
if (!fs2.existsSync(filePath))
|
|
1714
|
-
continue;
|
|
1715
|
-
const sourceFile = createSourceFile(filePath);
|
|
1716
|
-
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1717
|
-
if (!location)
|
|
1718
|
-
continue;
|
|
1719
|
-
let existingPatch = {};
|
|
1720
|
-
if (location.hasExisting && location.existingJSDoc) {
|
|
1721
|
-
existingPatch = parseJSDocToPatch(location.existingJSDoc);
|
|
1722
|
-
}
|
|
1723
|
-
const mergedPatch = { ...existingPatch, ...result.patch };
|
|
1724
|
-
const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
|
|
1725
|
-
edits.push({
|
|
1726
|
-
filePath,
|
|
1727
|
-
symbolName: exp.name,
|
|
1728
|
-
startLine: location.startLine,
|
|
1729
|
-
endLine: location.endLine,
|
|
1730
|
-
hasExisting: location.hasExisting,
|
|
1731
|
-
existingJSDoc: location.existingJSDoc,
|
|
1732
|
-
newJSDoc,
|
|
1733
|
-
indent: location.indent
|
|
1734
|
-
});
|
|
1735
|
-
generatedExportKeys.add(exp.name);
|
|
1736
|
-
}
|
|
1737
|
-
if (edits.length > 0) {
|
|
1738
|
-
if (options.dryRun) {
|
|
1739
|
-
log("");
|
|
1740
|
-
log(chalk5.bold("Dry run - JSDoc that would be generated:"));
|
|
1741
|
-
for (const edit of edits) {
|
|
1742
|
-
const relativePath = path4.relative(targetDir, edit.filePath);
|
|
1743
|
-
log(chalk5.cyan(` ${relativePath}:`));
|
|
1744
|
-
log(` ${chalk5.bold(edit.symbolName)} [line ${edit.startLine + 1}]`);
|
|
1745
|
-
log(chalk5.green(" + description, params, returns, example"));
|
|
1746
|
-
}
|
|
1395
|
+
const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
|
|
1747
1396
|
log("");
|
|
1748
|
-
log(
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
if (aiResult.source === "hosted" && aiResult.quotaRemaining !== undefined) {
|
|
1754
|
-
const remaining = aiResult.quotaRemaining === "unlimited" ? "unlimited" : aiResult.quotaRemaining.toLocaleString();
|
|
1755
|
-
log(chalk5.dim(` AI calls remaining: ${remaining}`));
|
|
1756
|
-
}
|
|
1757
|
-
if (applyResult.errors.length > 0) {
|
|
1758
|
-
for (const err of applyResult.errors) {
|
|
1759
|
-
error(chalk5.red(` ${err.file}: ${err.error}`));
|
|
1760
|
-
}
|
|
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)`));
|
|
1761
1402
|
}
|
|
1762
1403
|
}
|
|
1763
1404
|
}
|
|
1764
1405
|
}
|
|
1765
1406
|
}
|
|
1407
|
+
if (!isPreview) {
|
|
1408
|
+
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1409
|
+
}
|
|
1766
1410
|
}
|
|
1767
1411
|
steps.complete("Check complete");
|
|
1768
1412
|
if (format !== "text") {
|
|
@@ -1784,8 +1428,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1784
1428
|
case "github":
|
|
1785
1429
|
formatContent = renderGithubSummary(stats, {
|
|
1786
1430
|
coverageScore,
|
|
1787
|
-
driftCount: driftExports.length
|
|
1788
|
-
qualityIssues: violations.length
|
|
1431
|
+
driftCount: driftExports.length
|
|
1789
1432
|
});
|
|
1790
1433
|
break;
|
|
1791
1434
|
default:
|
|
@@ -1805,12 +1448,10 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1805
1448
|
const totalExportsForDrift2 = spec.exports?.length ?? 0;
|
|
1806
1449
|
const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
|
|
1807
1450
|
const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
|
|
1808
|
-
const coverageFailed2 =
|
|
1451
|
+
const coverageFailed2 = coverageScore < minCoverage;
|
|
1809
1452
|
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
1810
|
-
const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1811
1453
|
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
1812
|
-
|
|
1813
|
-
if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
|
|
1454
|
+
if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
|
|
1814
1455
|
process.exit(1);
|
|
1815
1456
|
}
|
|
1816
1457
|
return;
|
|
@@ -1818,49 +1459,41 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1818
1459
|
const totalExportsForDrift = spec.exports?.length ?? 0;
|
|
1819
1460
|
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
1820
1461
|
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
1821
|
-
const coverageFailed =
|
|
1462
|
+
const coverageFailed = coverageScore < minCoverage;
|
|
1822
1463
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1823
|
-
const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1824
1464
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1825
|
-
const policiesFailed = policyResult && !policyResult.allPassed;
|
|
1826
1465
|
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
1827
1466
|
log("");
|
|
1828
1467
|
for (const diag of specWarnings) {
|
|
1829
|
-
log(
|
|
1468
|
+
log(chalk4.yellow(`⚠ ${diag.message}`));
|
|
1830
1469
|
if (diag.suggestion) {
|
|
1831
|
-
log(
|
|
1470
|
+
log(chalk4.gray(` ${diag.suggestion}`));
|
|
1832
1471
|
}
|
|
1833
1472
|
}
|
|
1834
1473
|
for (const diag of specInfos) {
|
|
1835
|
-
log(
|
|
1474
|
+
log(chalk4.cyan(`ℹ ${diag.message}`));
|
|
1836
1475
|
if (diag.suggestion) {
|
|
1837
|
-
log(
|
|
1476
|
+
log(chalk4.gray(` ${diag.suggestion}`));
|
|
1838
1477
|
}
|
|
1839
1478
|
}
|
|
1840
1479
|
}
|
|
1841
1480
|
const pkgName = spec.meta?.name ?? "unknown";
|
|
1842
1481
|
const pkgVersion = spec.meta?.version ?? "";
|
|
1843
1482
|
const totalExports = spec.exports?.length ?? 0;
|
|
1844
|
-
const errorCount = violations.filter((v) => v.violation.severity === "error").length;
|
|
1845
|
-
const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
|
|
1846
1483
|
log("");
|
|
1847
|
-
log(
|
|
1484
|
+
log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1848
1485
|
log("");
|
|
1849
1486
|
log(` Exports: ${totalExports}`);
|
|
1850
|
-
if (
|
|
1851
|
-
|
|
1852
|
-
log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1853
|
-
} else {
|
|
1854
|
-
log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1855
|
-
}
|
|
1487
|
+
if (coverageFailed) {
|
|
1488
|
+
log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1856
1489
|
} else {
|
|
1857
|
-
log(` Coverage: ${coverageScore}%`);
|
|
1490
|
+
log(chalk4.green(` Coverage: ✓ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
|
|
1858
1491
|
}
|
|
1859
1492
|
if (maxDrift !== undefined) {
|
|
1860
1493
|
if (driftFailed) {
|
|
1861
|
-
log(
|
|
1494
|
+
log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1862
1495
|
} else {
|
|
1863
|
-
log(
|
|
1496
|
+
log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
|
|
1864
1497
|
}
|
|
1865
1498
|
} else {
|
|
1866
1499
|
log(` Drift: ${driftScore}%`);
|
|
@@ -1868,103 +1501,51 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1868
1501
|
if (exampleResult) {
|
|
1869
1502
|
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
1870
1503
|
if (typecheckCount > 0) {
|
|
1871
|
-
log(` Examples: ${typecheckCount} type
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
if (errorCount > 0 || warnCount > 0) {
|
|
1877
|
-
const parts = [];
|
|
1878
|
-
if (errorCount > 0)
|
|
1879
|
-
parts.push(`${errorCount} errors`);
|
|
1880
|
-
if (warnCount > 0)
|
|
1881
|
-
parts.push(`${warnCount} warnings`);
|
|
1882
|
-
log(` Quality: ${parts.join(", ")}`);
|
|
1883
|
-
}
|
|
1884
|
-
if (policyResult && policyResult.results.length > 0) {
|
|
1885
|
-
log("");
|
|
1886
|
-
log(chalk5.bold(" Policies:"));
|
|
1887
|
-
for (const result of policyResult.results) {
|
|
1888
|
-
const status = result.passed ? chalk5.green("✓") : chalk5.red("✗");
|
|
1889
|
-
const policyPath = result.policy.path;
|
|
1890
|
-
const matchCount = result.matchedExports.length;
|
|
1891
|
-
if (result.passed) {
|
|
1892
|
-
log(` ${status} ${policyPath} (${matchCount} exports)`);
|
|
1893
|
-
} else {
|
|
1894
|
-
log(` ${status} ${policyPath} (${matchCount} exports)`);
|
|
1895
|
-
for (const failure of result.failures) {
|
|
1896
|
-
log(chalk5.red(` ${failure.message}`));
|
|
1897
|
-
}
|
|
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}`));
|
|
1898
1509
|
}
|
|
1510
|
+
if (typecheckErrors.length > 5) {
|
|
1511
|
+
log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
|
|
1512
|
+
}
|
|
1513
|
+
} else {
|
|
1514
|
+
log(chalk4.green(` Examples: ✓ validated`));
|
|
1899
1515
|
}
|
|
1900
1516
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
log(
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
const coverageColor = stats.coverageScore >= 80 ? chalk5.green : stats.coverageScore >= 50 ? chalk5.yellow : chalk5.red;
|
|
1907
|
-
const coverageStr = coverageColor(`${stats.coverageScore}%`);
|
|
1908
|
-
const undocCount = stats.undocumentedExports.length;
|
|
1909
|
-
log(` ${stats.owner.padEnd(24)} ${coverageStr.padStart(12)} coverage (${stats.totalExports} exports${undocCount > 0 ? `, ${undocCount} undoc` : ""})`);
|
|
1910
|
-
}
|
|
1911
|
-
if (ownershipResult.unowned.length > 0) {
|
|
1912
|
-
log(chalk5.dim(` (${ownershipResult.unowned.length} exports have no owner in CODEOWNERS)`));
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
if (contributorResult) {
|
|
1916
|
-
log("");
|
|
1917
|
-
log(chalk5.bold(" Contributors:"));
|
|
1918
|
-
const sortedContributors = [...contributorResult.byContributor.values()].sort((a, b) => b.documentedExports - a.documentedExports);
|
|
1919
|
-
const displayLimit = 10;
|
|
1920
|
-
const topContributors = sortedContributors.slice(0, displayLimit);
|
|
1921
|
-
for (const stats of topContributors) {
|
|
1922
|
-
const name = stats.name.length > 20 ? stats.name.slice(0, 17) + "..." : stats.name;
|
|
1923
|
-
const lastDate = stats.lastContribution ? stats.lastContribution.toISOString().split("T")[0] : "unknown";
|
|
1924
|
-
log(` ${name.padEnd(22)} ${String(stats.documentedExports).padStart(4)} exports (last: ${lastDate})`);
|
|
1925
|
-
}
|
|
1926
|
-
if (sortedContributors.length > displayLimit) {
|
|
1927
|
-
log(chalk5.dim(` ... and ${sortedContributors.length - displayLimit} more contributors`));
|
|
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}"`));
|
|
1928
1522
|
}
|
|
1929
|
-
if (
|
|
1930
|
-
log(
|
|
1523
|
+
if (staleRefs.length > 5) {
|
|
1524
|
+
log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
|
|
1931
1525
|
}
|
|
1932
1526
|
}
|
|
1933
1527
|
log("");
|
|
1934
|
-
const failed = coverageFailed || driftFailed ||
|
|
1528
|
+
const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
|
|
1935
1529
|
if (!failed) {
|
|
1936
1530
|
const thresholdParts = [];
|
|
1937
|
-
|
|
1938
|
-
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1939
|
-
}
|
|
1531
|
+
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
1940
1532
|
if (maxDrift !== undefined) {
|
|
1941
1533
|
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1942
1534
|
}
|
|
1943
|
-
|
|
1944
|
-
thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
|
|
1945
|
-
}
|
|
1946
|
-
if (thresholdParts.length > 0) {
|
|
1947
|
-
log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1948
|
-
} else {
|
|
1949
|
-
log(chalk5.green("✓ Check passed"));
|
|
1950
|
-
log(chalk5.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
|
|
1951
|
-
}
|
|
1535
|
+
log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1952
1536
|
return;
|
|
1953
1537
|
}
|
|
1954
|
-
if (hasQualityErrors) {
|
|
1955
|
-
log(chalk5.red(`✗ ${errorCount} quality errors`));
|
|
1956
|
-
}
|
|
1957
1538
|
if (hasTypecheckErrors) {
|
|
1958
|
-
log(
|
|
1539
|
+
log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1959
1540
|
}
|
|
1960
|
-
if (
|
|
1961
|
-
log(
|
|
1541
|
+
if (hasStaleRefs) {
|
|
1542
|
+
log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
|
|
1962
1543
|
}
|
|
1963
1544
|
log("");
|
|
1964
|
-
log(
|
|
1545
|
+
log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
|
|
1965
1546
|
process.exit(1);
|
|
1966
1547
|
} catch (commandError) {
|
|
1967
|
-
error(
|
|
1548
|
+
error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1968
1549
|
process.exit(1);
|
|
1969
1550
|
}
|
|
1970
1551
|
});
|
|
@@ -1988,6 +1569,23 @@ function collectDrift(exportsList) {
|
|
|
1988
1569
|
}
|
|
1989
1570
|
return drifts;
|
|
1990
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
|
+
}
|
|
1991
1589
|
|
|
1992
1590
|
// src/commands/diff.ts
|
|
1993
1591
|
import * as fs3 from "node:fs";
|
|
@@ -1999,73 +1597,11 @@ import {
|
|
|
1999
1597
|
getDocsImpactSummary,
|
|
2000
1598
|
hasDocsImpact,
|
|
2001
1599
|
hashString,
|
|
2002
|
-
parseMarkdownFiles
|
|
1600
|
+
parseMarkdownFiles as parseMarkdownFiles2
|
|
2003
1601
|
} from "@doccov/sdk";
|
|
2004
1602
|
import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
|
|
2005
|
-
import
|
|
2006
|
-
import { glob } from "glob";
|
|
2007
|
-
|
|
2008
|
-
// src/utils/docs-impact-ai.ts
|
|
2009
|
-
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
2010
|
-
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
2011
|
-
import { generateObject as generateObject3, generateText } from "ai";
|
|
2012
|
-
import { z as z4 } from "zod";
|
|
2013
|
-
var CodeBlockUsageSchema = z4.object({
|
|
2014
|
-
isImpacted: z4.boolean().describe("Whether the code block is affected by the change"),
|
|
2015
|
-
reason: z4.string().describe("Explanation of why/why not the code is impacted"),
|
|
2016
|
-
usageType: z4.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
|
|
2017
|
-
suggestedFix: z4.string().optional().describe("If impacted, the suggested code change"),
|
|
2018
|
-
confidence: z4.enum(["high", "medium", "low"]).describe("Confidence level of the analysis")
|
|
2019
|
-
});
|
|
2020
|
-
var MultiBlockAnalysisSchema = z4.object({
|
|
2021
|
-
groups: z4.array(z4.object({
|
|
2022
|
-
blockIndices: z4.array(z4.number()).describe("Indices of blocks that should run together"),
|
|
2023
|
-
reason: z4.string().describe("Why these blocks are related")
|
|
2024
|
-
})).describe("Groups of related code blocks"),
|
|
2025
|
-
skippedBlocks: z4.array(z4.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
|
|
2026
|
-
});
|
|
2027
|
-
function getModel3() {
|
|
2028
|
-
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
2029
|
-
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
2030
|
-
const anthropic = createAnthropic3();
|
|
2031
|
-
return anthropic("claude-sonnet-4-20250514");
|
|
2032
|
-
}
|
|
2033
|
-
const openai = createOpenAI3();
|
|
2034
|
-
return openai("gpt-4o-mini");
|
|
2035
|
-
}
|
|
2036
|
-
function isAIDocsAnalysisAvailable() {
|
|
2037
|
-
return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
|
|
2038
|
-
}
|
|
2039
|
-
async function generateImpactSummary(impacts) {
|
|
2040
|
-
if (!isAIDocsAnalysisAvailable()) {
|
|
2041
|
-
return null;
|
|
2042
|
-
}
|
|
2043
|
-
if (impacts.length === 0) {
|
|
2044
|
-
return "No documentation impacts detected.";
|
|
2045
|
-
}
|
|
2046
|
-
try {
|
|
2047
|
-
const { text } = await generateText({
|
|
2048
|
-
model: getModel3(),
|
|
2049
|
-
prompt: `Summarize these documentation impacts for a GitHub PR comment.
|
|
2050
|
-
|
|
2051
|
-
Impacts:
|
|
2052
|
-
${impacts.map((i) => `- ${i.file}: ${i.exportName} (${i.changeType})`).join(`
|
|
2053
|
-
`)}
|
|
2054
|
-
|
|
2055
|
-
Write a brief, actionable summary (2-3 sentences) explaining:
|
|
2056
|
-
1. How many files/references are affected
|
|
2057
|
-
2. What type of updates are needed
|
|
2058
|
-
3. Priority recommendation
|
|
2059
|
-
|
|
2060
|
-
Keep it concise and developer-friendly.`
|
|
2061
|
-
});
|
|
2062
|
-
return text.trim();
|
|
2063
|
-
} catch {
|
|
2064
|
-
return null;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// src/commands/diff.ts
|
|
1603
|
+
import chalk5 from "chalk";
|
|
1604
|
+
import { glob as glob2 } from "glob";
|
|
2069
1605
|
var defaultDependencies2 = {
|
|
2070
1606
|
readFileSync: fs3.readFileSync,
|
|
2071
1607
|
log: console.log,
|
|
@@ -2086,11 +1622,11 @@ function getStrictChecks(preset) {
|
|
|
2086
1622
|
return checks;
|
|
2087
1623
|
}
|
|
2088
1624
|
function registerDiffCommand(program, dependencies = {}) {
|
|
2089
|
-
const { readFileSync:
|
|
1625
|
+
const { readFileSync: readFileSync3, log, error } = {
|
|
2090
1626
|
...defaultDependencies2,
|
|
2091
1627
|
...dependencies
|
|
2092
1628
|
};
|
|
2093
|
-
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",
|
|
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) => {
|
|
2094
1630
|
try {
|
|
2095
1631
|
const baseFile = options.base ?? baseArg;
|
|
2096
1632
|
const headFile = options.head ?? headArg;
|
|
@@ -2099,8 +1635,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2099
1635
|
` + `Usage: doccov diff <base> <head>
|
|
2100
1636
|
` + " or: doccov diff --base main.json --head feature.json");
|
|
2101
1637
|
}
|
|
2102
|
-
const baseSpec = loadSpec(baseFile,
|
|
2103
|
-
const headSpec = loadSpec(headFile,
|
|
1638
|
+
const baseSpec = loadSpec(baseFile, readFileSync3);
|
|
1639
|
+
const headSpec = loadSpec(headFile, readFileSync3);
|
|
2104
1640
|
const config = await loadDocCovConfig(options.cwd);
|
|
2105
1641
|
const baseHash = hashString(JSON.stringify(baseSpec));
|
|
2106
1642
|
const headHash = hashString(JSON.stringify(headSpec));
|
|
@@ -2137,9 +1673,9 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2137
1673
|
}, null, 2));
|
|
2138
1674
|
} else {
|
|
2139
1675
|
log("");
|
|
2140
|
-
log(
|
|
1676
|
+
log(chalk5.bold("Semver Recommendation"));
|
|
2141
1677
|
log(` Current version: ${currentVersion}`);
|
|
2142
|
-
log(` Recommended: ${
|
|
1678
|
+
log(` Recommended: ${chalk5.cyan(nextVersion)} (${chalk5.yellow(recommendation.bump.toUpperCase())})`);
|
|
2143
1679
|
log(` Reason: ${recommendation.reason}`);
|
|
2144
1680
|
}
|
|
2145
1681
|
return;
|
|
@@ -2157,9 +1693,6 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2157
1693
|
switch (format) {
|
|
2158
1694
|
case "text":
|
|
2159
1695
|
printSummary(diff, baseName, headName, fromCache, log);
|
|
2160
|
-
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
2161
|
-
await printAISummary(diff, log);
|
|
2162
|
-
}
|
|
2163
1696
|
if (!options.stdout) {
|
|
2164
1697
|
const jsonPath = getDiffReportPath(baseHash, headHash, "json");
|
|
2165
1698
|
if (!fromCache) {
|
|
@@ -2171,8 +1704,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2171
1704
|
silent: true
|
|
2172
1705
|
});
|
|
2173
1706
|
}
|
|
2174
|
-
const cacheNote = fromCache ?
|
|
2175
|
-
log(
|
|
1707
|
+
const cacheNote = fromCache ? chalk5.cyan(" (cached)") : "";
|
|
1708
|
+
log(chalk5.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
2176
1709
|
}
|
|
2177
1710
|
break;
|
|
2178
1711
|
case "json": {
|
|
@@ -2224,11 +1757,13 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2224
1757
|
printGitHubAnnotations(diff, log);
|
|
2225
1758
|
break;
|
|
2226
1759
|
case "pr-comment": {
|
|
1760
|
+
const semverRecommendation = recommendSemverBump(diff);
|
|
2227
1761
|
const content = renderPRComment({ diff, baseName, headName, headSpec }, {
|
|
2228
1762
|
repoUrl: options.repoUrl,
|
|
2229
1763
|
sha: options.sha,
|
|
2230
1764
|
minCoverage,
|
|
2231
|
-
limit
|
|
1765
|
+
limit,
|
|
1766
|
+
semverBump: { bump: semverRecommendation.bump, reason: semverRecommendation.reason }
|
|
2232
1767
|
});
|
|
2233
1768
|
log(content);
|
|
2234
1769
|
break;
|
|
@@ -2262,29 +1797,29 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
2262
1797
|
checks
|
|
2263
1798
|
});
|
|
2264
1799
|
if (failures.length > 0) {
|
|
2265
|
-
log(
|
|
1800
|
+
log(chalk5.red(`
|
|
2266
1801
|
✗ Check failed`));
|
|
2267
1802
|
for (const f of failures) {
|
|
2268
|
-
log(
|
|
1803
|
+
log(chalk5.red(` - ${f}`));
|
|
2269
1804
|
}
|
|
2270
1805
|
process.exitCode = 1;
|
|
2271
1806
|
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
2272
|
-
log(
|
|
1807
|
+
log(chalk5.green(`
|
|
2273
1808
|
✓ All checks passed`));
|
|
2274
1809
|
}
|
|
2275
1810
|
} catch (commandError) {
|
|
2276
|
-
error(
|
|
1811
|
+
error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2277
1812
|
process.exitCode = 1;
|
|
2278
1813
|
}
|
|
2279
1814
|
});
|
|
2280
1815
|
}
|
|
2281
|
-
function
|
|
1816
|
+
function collect2(value, previous) {
|
|
2282
1817
|
return previous.concat([value]);
|
|
2283
1818
|
}
|
|
2284
|
-
async function
|
|
1819
|
+
async function loadMarkdownFiles2(patterns) {
|
|
2285
1820
|
const files = [];
|
|
2286
1821
|
for (const pattern of patterns) {
|
|
2287
|
-
const matches = await
|
|
1822
|
+
const matches = await glob2(pattern, { nodir: true });
|
|
2288
1823
|
for (const filePath of matches) {
|
|
2289
1824
|
try {
|
|
2290
1825
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
@@ -2292,7 +1827,7 @@ async function loadMarkdownFiles(patterns) {
|
|
|
2292
1827
|
} catch {}
|
|
2293
1828
|
}
|
|
2294
1829
|
}
|
|
2295
|
-
return
|
|
1830
|
+
return parseMarkdownFiles2(files);
|
|
2296
1831
|
}
|
|
2297
1832
|
async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
2298
1833
|
let markdownFiles;
|
|
@@ -2300,21 +1835,21 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
2300
1835
|
if (!docsPatterns || docsPatterns.length === 0) {
|
|
2301
1836
|
if (config?.docs?.include) {
|
|
2302
1837
|
docsPatterns = config.docs.include;
|
|
2303
|
-
log(
|
|
1838
|
+
log(chalk5.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
2304
1839
|
}
|
|
2305
1840
|
}
|
|
2306
1841
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
2307
|
-
markdownFiles = await
|
|
1842
|
+
markdownFiles = await loadMarkdownFiles2(docsPatterns);
|
|
2308
1843
|
}
|
|
2309
1844
|
return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
|
|
2310
1845
|
}
|
|
2311
|
-
function loadSpec(filePath,
|
|
1846
|
+
function loadSpec(filePath, readFileSync3) {
|
|
2312
1847
|
const resolvedPath = path5.resolve(filePath);
|
|
2313
1848
|
if (!fs3.existsSync(resolvedPath)) {
|
|
2314
1849
|
throw new Error(`File not found: ${filePath}`);
|
|
2315
1850
|
}
|
|
2316
1851
|
try {
|
|
2317
|
-
const content =
|
|
1852
|
+
const content = readFileSync3(resolvedPath, "utf-8");
|
|
2318
1853
|
const spec = JSON.parse(content);
|
|
2319
1854
|
return ensureSpecCoverage(spec);
|
|
2320
1855
|
} catch (parseError) {
|
|
@@ -2323,60 +1858,40 @@ function loadSpec(filePath, readFileSync2) {
|
|
|
2323
1858
|
}
|
|
2324
1859
|
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
2325
1860
|
log("");
|
|
2326
|
-
const cacheIndicator = fromCache ?
|
|
2327
|
-
log(
|
|
1861
|
+
const cacheIndicator = fromCache ? chalk5.cyan(" (cached)") : "";
|
|
1862
|
+
log(chalk5.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
2328
1863
|
log("─".repeat(40));
|
|
2329
1864
|
log("");
|
|
2330
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
1865
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk5.green : diff.coverageDelta < 0 ? chalk5.red : chalk5.gray;
|
|
2331
1866
|
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
2332
1867
|
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
2333
1868
|
const breakingCount = diff.breaking.length;
|
|
2334
1869
|
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
2335
1870
|
if (breakingCount > 0) {
|
|
2336
|
-
const severityNote = highSeverity > 0 ?
|
|
2337
|
-
log(` Breaking: ${
|
|
1871
|
+
const severityNote = highSeverity > 0 ? chalk5.red(` (${highSeverity} high severity)`) : "";
|
|
1872
|
+
log(` Breaking: ${chalk5.red(breakingCount)} changes${severityNote}`);
|
|
2338
1873
|
} else {
|
|
2339
|
-
log(` Breaking: ${
|
|
1874
|
+
log(` Breaking: ${chalk5.green("0")} changes`);
|
|
2340
1875
|
}
|
|
2341
1876
|
const newCount = diff.nonBreaking.length;
|
|
2342
1877
|
const undocCount = diff.newUndocumented.length;
|
|
2343
1878
|
if (newCount > 0) {
|
|
2344
|
-
const undocNote = undocCount > 0 ?
|
|
2345
|
-
log(` New: ${
|
|
1879
|
+
const undocNote = undocCount > 0 ? chalk5.yellow(` (${undocCount} undocumented)`) : "";
|
|
1880
|
+
log(` New: ${chalk5.green(newCount)} exports${undocNote}`);
|
|
2346
1881
|
}
|
|
2347
1882
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
2348
1883
|
const parts = [];
|
|
2349
1884
|
if (diff.driftIntroduced > 0)
|
|
2350
|
-
parts.push(
|
|
1885
|
+
parts.push(chalk5.red(`+${diff.driftIntroduced}`));
|
|
2351
1886
|
if (diff.driftResolved > 0)
|
|
2352
|
-
parts.push(
|
|
1887
|
+
parts.push(chalk5.green(`-${diff.driftResolved}`));
|
|
2353
1888
|
log(` Drift: ${parts.join(", ")}`);
|
|
2354
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})`);
|
|
2355
1893
|
log("");
|
|
2356
1894
|
}
|
|
2357
|
-
async function printAISummary(diff, log) {
|
|
2358
|
-
if (!isAIDocsAnalysisAvailable()) {
|
|
2359
|
-
log(chalk6.yellow(`
|
|
2360
|
-
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2363
|
-
if (!diff.docsImpact)
|
|
2364
|
-
return;
|
|
2365
|
-
log(chalk6.gray(`
|
|
2366
|
-
Generating AI summary...`));
|
|
2367
|
-
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
2368
|
-
file: f.file,
|
|
2369
|
-
exportName: r.exportName,
|
|
2370
|
-
changeType: r.changeType,
|
|
2371
|
-
context: r.context
|
|
2372
|
-
})));
|
|
2373
|
-
const summary = await generateImpactSummary(impacts);
|
|
2374
|
-
if (summary) {
|
|
2375
|
-
log("");
|
|
2376
|
-
log(chalk6.bold("AI Summary"));
|
|
2377
|
-
log(chalk6.cyan(` ${summary}`));
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
1895
|
function validateDiff(diff, headSpec, options) {
|
|
2381
1896
|
const { minCoverage, maxDrift, checks } = options;
|
|
2382
1897
|
const failures = [];
|
|
@@ -2455,7 +1970,7 @@ function printGitHubAnnotations(diff, log) {
|
|
|
2455
1970
|
|
|
2456
1971
|
// src/commands/info.ts
|
|
2457
1972
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
2458
|
-
import
|
|
1973
|
+
import chalk6 from "chalk";
|
|
2459
1974
|
function registerInfoCommand(program) {
|
|
2460
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) => {
|
|
2461
1976
|
try {
|
|
@@ -2477,14 +1992,14 @@ function registerInfoCommand(program) {
|
|
|
2477
1992
|
const spec = enrichSpec2(specResult.spec);
|
|
2478
1993
|
const stats = computeStats(spec);
|
|
2479
1994
|
console.log("");
|
|
2480
|
-
console.log(
|
|
1995
|
+
console.log(chalk6.bold(`${stats.packageName}@${stats.version}`));
|
|
2481
1996
|
console.log("");
|
|
2482
|
-
console.log(` Exports: ${
|
|
2483
|
-
console.log(` Coverage: ${
|
|
2484
|
-
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}%`)}`);
|
|
2485
2000
|
console.log("");
|
|
2486
2001
|
} catch (err) {
|
|
2487
|
-
console.error(
|
|
2002
|
+
console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
|
|
2488
2003
|
process.exit(1);
|
|
2489
2004
|
}
|
|
2490
2005
|
});
|
|
@@ -2493,53 +2008,65 @@ function registerInfoCommand(program) {
|
|
|
2493
2008
|
// src/commands/init.ts
|
|
2494
2009
|
import * as fs4 from "node:fs";
|
|
2495
2010
|
import * as path6 from "node:path";
|
|
2496
|
-
import
|
|
2011
|
+
import chalk7 from "chalk";
|
|
2497
2012
|
var defaultDependencies3 = {
|
|
2498
2013
|
fileExists: fs4.existsSync,
|
|
2499
2014
|
writeFileSync: fs4.writeFileSync,
|
|
2500
2015
|
readFileSync: fs4.readFileSync,
|
|
2016
|
+
mkdirSync: fs4.mkdirSync,
|
|
2501
2017
|
log: console.log,
|
|
2502
2018
|
error: console.error
|
|
2503
2019
|
};
|
|
2504
2020
|
function registerInitCommand(program, dependencies = {}) {
|
|
2505
|
-
const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync:
|
|
2021
|
+
const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync4, mkdirSync: mkdirSync3, log, error } = {
|
|
2506
2022
|
...defaultDependencies3,
|
|
2507
2023
|
...dependencies
|
|
2508
2024
|
};
|
|
2509
|
-
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) => {
|
|
2510
2026
|
const cwd = path6.resolve(options.cwd);
|
|
2511
|
-
const formatOption = String(options.format ?? "auto").toLowerCase();
|
|
2512
|
-
if (!isValidFormat(formatOption)) {
|
|
2513
|
-
error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
|
|
2514
|
-
process.exitCode = 1;
|
|
2515
|
-
return;
|
|
2516
|
-
}
|
|
2517
2027
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
2518
2028
|
if (existing) {
|
|
2519
|
-
error(
|
|
2029
|
+
error(chalk7.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
2520
2030
|
process.exitCode = 1;
|
|
2521
2031
|
return;
|
|
2522
2032
|
}
|
|
2523
|
-
const packageType = detectPackageType(cwd, fileExists2,
|
|
2524
|
-
const targetFormat =
|
|
2525
|
-
|
|
2526
|
-
log(chalk8.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
|
|
2527
|
-
}
|
|
2528
|
-
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}`;
|
|
2529
2036
|
const outputPath = path6.join(cwd, fileName);
|
|
2530
2037
|
if (fileExists2(outputPath)) {
|
|
2531
|
-
error(
|
|
2038
|
+
error(chalk7.red(`Cannot create ${fileName}; file already exists.`));
|
|
2532
2039
|
process.exitCode = 1;
|
|
2533
2040
|
return;
|
|
2534
2041
|
}
|
|
2535
|
-
const template =
|
|
2042
|
+
const template = buildConfigTemplate();
|
|
2536
2043
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
2537
|
-
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"));
|
|
2538
2068
|
});
|
|
2539
2069
|
}
|
|
2540
|
-
var isValidFormat = (value) => {
|
|
2541
|
-
return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
|
|
2542
|
-
};
|
|
2543
2070
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
2544
2071
|
let current = path6.resolve(cwd);
|
|
2545
2072
|
const { root } = path6.parse(current);
|
|
@@ -2557,13 +2084,13 @@ var findExistingConfig = (cwd, fileExists2) => {
|
|
|
2557
2084
|
}
|
|
2558
2085
|
return null;
|
|
2559
2086
|
};
|
|
2560
|
-
var detectPackageType = (cwd, fileExists2,
|
|
2087
|
+
var detectPackageType = (cwd, fileExists2, readFileSync4) => {
|
|
2561
2088
|
const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
|
|
2562
2089
|
if (!packageJsonPath) {
|
|
2563
2090
|
return;
|
|
2564
2091
|
}
|
|
2565
2092
|
try {
|
|
2566
|
-
const raw =
|
|
2093
|
+
const raw = readFileSync4(packageJsonPath, "utf8");
|
|
2567
2094
|
const parsed = JSON.parse(raw);
|
|
2568
2095
|
if (parsed.type === "module") {
|
|
2569
2096
|
return "module";
|
|
@@ -2589,80 +2116,75 @@ var findNearestPackageJson = (cwd, fileExists2) => {
|
|
|
2589
2116
|
}
|
|
2590
2117
|
return null;
|
|
2591
2118
|
};
|
|
2592
|
-
var
|
|
2593
|
-
|
|
2594
|
-
return "yaml";
|
|
2595
|
-
if (format === "auto") {
|
|
2596
|
-
return packageType === "module" ? "js" : "mjs";
|
|
2597
|
-
}
|
|
2598
|
-
return format;
|
|
2599
|
-
};
|
|
2600
|
-
var buildTemplate = (format) => {
|
|
2601
|
-
if (format === "yaml") {
|
|
2602
|
-
return `# doccov.yml
|
|
2603
|
-
# include:
|
|
2604
|
-
# - "MyClass"
|
|
2605
|
-
# - "myFunction"
|
|
2606
|
-
# exclude:
|
|
2607
|
-
# - "internal*"
|
|
2608
|
-
|
|
2609
|
-
check:
|
|
2610
|
-
# minCoverage: 80
|
|
2611
|
-
# maxDrift: 20
|
|
2612
|
-
# examples: typecheck
|
|
2119
|
+
var buildConfigTemplate = () => {
|
|
2120
|
+
return `import { defineConfig } from '@doccov/cli/config';
|
|
2613
2121
|
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
# has-description: warn
|
|
2617
|
-
# has-params: off
|
|
2618
|
-
# has-returns: off
|
|
2619
|
-
`;
|
|
2620
|
-
}
|
|
2621
|
-
const configBody = `{
|
|
2622
|
-
// Filter which exports to analyze
|
|
2122
|
+
export default defineConfig({
|
|
2123
|
+
// Filter exports to analyze (optional)
|
|
2623
2124
|
// include: ['MyClass', 'myFunction'],
|
|
2624
2125
|
// exclude: ['internal*'],
|
|
2625
2126
|
|
|
2626
|
-
// Check command thresholds
|
|
2627
2127
|
check: {
|
|
2628
|
-
//
|
|
2629
|
-
|
|
2128
|
+
// Fail if coverage drops below threshold
|
|
2129
|
+
minCoverage: 80,
|
|
2630
2130
|
|
|
2631
|
-
//
|
|
2131
|
+
// Fail if drift exceeds threshold
|
|
2632
2132
|
// maxDrift: 20,
|
|
2633
|
-
|
|
2634
|
-
// Example validation: 'presence' | 'typecheck' | 'run'
|
|
2635
|
-
// examples: 'typecheck',
|
|
2636
2133
|
},
|
|
2134
|
+
});
|
|
2135
|
+
`;
|
|
2136
|
+
};
|
|
2137
|
+
var buildWorkflowTemplate = () => {
|
|
2138
|
+
return `name: DocCov
|
|
2637
2139
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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 {}
|
|
2658
2176
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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;
|
|
2666
2188
|
};
|
|
2667
2189
|
|
|
2668
2190
|
// src/commands/spec.ts
|
|
@@ -2670,9 +2192,9 @@ import * as fs5 from "node:fs";
|
|
|
2670
2192
|
import * as path7 from "node:path";
|
|
2671
2193
|
import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2672
2194
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2673
|
-
import
|
|
2195
|
+
import chalk8 from "chalk";
|
|
2674
2196
|
// package.json
|
|
2675
|
-
var version = "0.
|
|
2197
|
+
var version = "0.19.0";
|
|
2676
2198
|
|
|
2677
2199
|
// src/commands/spec.ts
|
|
2678
2200
|
var defaultDependencies4 = {
|
|
@@ -2687,7 +2209,7 @@ function getArrayLength(value) {
|
|
|
2687
2209
|
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2688
2210
|
const location = diagnostic.location;
|
|
2689
2211
|
const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
|
|
2690
|
-
const locationText = location && relativePath ?
|
|
2212
|
+
const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2691
2213
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2692
2214
|
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
2693
2215
|
}
|
|
@@ -2696,7 +2218,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2696
2218
|
...defaultDependencies4,
|
|
2697
2219
|
...dependencies
|
|
2698
2220
|
};
|
|
2699
|
-
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("--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) => {
|
|
2700
2222
|
try {
|
|
2701
2223
|
const steps = new StepProgress([
|
|
2702
2224
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
@@ -2718,7 +2240,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2718
2240
|
try {
|
|
2719
2241
|
config = await loadDocCovConfig(targetDir);
|
|
2720
2242
|
} catch (configError) {
|
|
2721
|
-
error(
|
|
2243
|
+
error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
2722
2244
|
process.exit(1);
|
|
2723
2245
|
}
|
|
2724
2246
|
steps.next();
|
|
@@ -2733,7 +2255,8 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2733
2255
|
resolveExternalTypes,
|
|
2734
2256
|
maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
|
|
2735
2257
|
useCache: options.cache !== false,
|
|
2736
|
-
cwd: options.cwd
|
|
2258
|
+
cwd: options.cwd,
|
|
2259
|
+
schemaExtraction: options.runtime ? "hybrid" : "static"
|
|
2737
2260
|
});
|
|
2738
2261
|
const generationInput = {
|
|
2739
2262
|
entryPoint: path7.relative(targetDir, entryFile),
|
|
@@ -2761,9 +2284,9 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2761
2284
|
const normalized = normalize(result.spec);
|
|
2762
2285
|
const validation = validateSpec(normalized);
|
|
2763
2286
|
if (!validation.ok) {
|
|
2764
|
-
error(
|
|
2287
|
+
error(chalk8.red("Spec failed schema validation"));
|
|
2765
2288
|
for (const err of validation.errors) {
|
|
2766
|
-
error(
|
|
2289
|
+
error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2767
2290
|
}
|
|
2768
2291
|
process.exit(1);
|
|
2769
2292
|
}
|
|
@@ -2778,55 +2301,62 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2778
2301
|
writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
|
|
2779
2302
|
steps.complete(`Generated ${options.output}`);
|
|
2780
2303
|
}
|
|
2781
|
-
log(
|
|
2782
|
-
log(
|
|
2304
|
+
log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
2305
|
+
log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
|
|
2783
2306
|
if (options.verbose && normalized.generation) {
|
|
2784
2307
|
const gen = normalized.generation;
|
|
2785
2308
|
log("");
|
|
2786
|
-
log(
|
|
2787
|
-
log(
|
|
2788
|
-
log(
|
|
2789
|
-
log(
|
|
2790
|
-
log(
|
|
2791
|
-
log(
|
|
2792
|
-
log(
|
|
2309
|
+
log(chalk8.bold("Generation Info"));
|
|
2310
|
+
log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
|
|
2311
|
+
log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
|
|
2312
|
+
log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
|
|
2313
|
+
log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
|
|
2314
|
+
log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
|
|
2315
|
+
log(chalk8.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
|
|
2793
2316
|
if (gen.analysis.maxTypeDepth) {
|
|
2794
|
-
log(
|
|
2317
|
+
log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
|
|
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
|
+
}
|
|
2795
2325
|
}
|
|
2796
2326
|
log("");
|
|
2797
|
-
log(
|
|
2798
|
-
log(
|
|
2327
|
+
log(chalk8.bold("Environment"));
|
|
2328
|
+
log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
2799
2329
|
if (gen.environment.packageManager) {
|
|
2800
|
-
log(
|
|
2330
|
+
log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
|
|
2801
2331
|
}
|
|
2802
2332
|
if (gen.environment.isMonorepo) {
|
|
2803
|
-
log(
|
|
2333
|
+
log(chalk8.gray(` Monorepo: yes`));
|
|
2804
2334
|
}
|
|
2805
2335
|
if (gen.environment.targetPackage) {
|
|
2806
|
-
log(
|
|
2336
|
+
log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
|
|
2807
2337
|
}
|
|
2808
2338
|
if (gen.issues.length > 0) {
|
|
2809
2339
|
log("");
|
|
2810
|
-
log(
|
|
2340
|
+
log(chalk8.bold("Issues"));
|
|
2811
2341
|
for (const issue of gen.issues) {
|
|
2812
|
-
const prefix = issue.severity === "error" ?
|
|
2342
|
+
const prefix = issue.severity === "error" ? chalk8.red(">") : issue.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
|
|
2813
2343
|
log(`${prefix} [${issue.code}] ${issue.message}`);
|
|
2814
2344
|
if (issue.suggestion) {
|
|
2815
|
-
log(
|
|
2345
|
+
log(chalk8.gray(` ${issue.suggestion}`));
|
|
2816
2346
|
}
|
|
2817
2347
|
}
|
|
2818
2348
|
}
|
|
2819
2349
|
}
|
|
2820
2350
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
2821
2351
|
log("");
|
|
2822
|
-
log(
|
|
2352
|
+
log(chalk8.bold("Diagnostics"));
|
|
2823
2353
|
for (const diagnostic of result.diagnostics) {
|
|
2824
|
-
const prefix = diagnostic.severity === "error" ?
|
|
2354
|
+
const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
|
|
2825
2355
|
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
2826
2356
|
}
|
|
2827
2357
|
}
|
|
2828
2358
|
} catch (commandError) {
|
|
2829
|
-
error(
|
|
2359
|
+
error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2830
2360
|
process.exit(1);
|
|
2831
2361
|
}
|
|
2832
2362
|
});
|
|
@@ -2846,7 +2376,7 @@ import {
|
|
|
2846
2376
|
RETENTION_DAYS,
|
|
2847
2377
|
saveSnapshot
|
|
2848
2378
|
} from "@doccov/sdk";
|
|
2849
|
-
import
|
|
2379
|
+
import chalk9 from "chalk";
|
|
2850
2380
|
function formatDate(timestamp) {
|
|
2851
2381
|
const date = new Date(timestamp);
|
|
2852
2382
|
return date.toLocaleDateString("en-US", {
|
|
@@ -2859,19 +2389,19 @@ function formatDate(timestamp) {
|
|
|
2859
2389
|
}
|
|
2860
2390
|
function getColorForScore(score) {
|
|
2861
2391
|
if (score >= 90)
|
|
2862
|
-
return
|
|
2392
|
+
return chalk9.green;
|
|
2863
2393
|
if (score >= 70)
|
|
2864
|
-
return
|
|
2394
|
+
return chalk9.yellow;
|
|
2865
2395
|
if (score >= 50)
|
|
2866
|
-
return
|
|
2867
|
-
return
|
|
2396
|
+
return chalk9.hex("#FFA500");
|
|
2397
|
+
return chalk9.red;
|
|
2868
2398
|
}
|
|
2869
2399
|
function formatSnapshot(snapshot) {
|
|
2870
2400
|
const color = getColorForScore(snapshot.coverageScore);
|
|
2871
2401
|
const date = formatDate(snapshot.timestamp);
|
|
2872
2402
|
const version2 = snapshot.version ? ` v${snapshot.version}` : "";
|
|
2873
|
-
const commit = snapshot.commit ?
|
|
2874
|
-
return `${
|
|
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}`;
|
|
2875
2405
|
}
|
|
2876
2406
|
function formatWeekDate(timestamp) {
|
|
2877
2407
|
const date = new Date(timestamp);
|
|
@@ -2879,10 +2409,10 @@ function formatWeekDate(timestamp) {
|
|
|
2879
2409
|
}
|
|
2880
2410
|
function formatVelocity(velocity) {
|
|
2881
2411
|
if (velocity > 0)
|
|
2882
|
-
return
|
|
2412
|
+
return chalk9.green(`+${velocity}%/day`);
|
|
2883
2413
|
if (velocity < 0)
|
|
2884
|
-
return
|
|
2885
|
-
return
|
|
2414
|
+
return chalk9.red(`${velocity}%/day`);
|
|
2415
|
+
return chalk9.gray("0%/day");
|
|
2886
2416
|
}
|
|
2887
2417
|
function registerTrendsCommand(program) {
|
|
2888
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) => {
|
|
@@ -2892,17 +2422,17 @@ function registerTrendsCommand(program) {
|
|
|
2892
2422
|
const keepCount = parseInt(options.prune, 10);
|
|
2893
2423
|
if (!isNaN(keepCount)) {
|
|
2894
2424
|
const deleted = pruneHistory(cwd, keepCount);
|
|
2895
|
-
console.log(
|
|
2425
|
+
console.log(chalk9.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
|
|
2896
2426
|
} else {
|
|
2897
2427
|
const deleted = pruneByTier(cwd, tier);
|
|
2898
|
-
console.log(
|
|
2428
|
+
console.log(chalk9.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
|
|
2899
2429
|
}
|
|
2900
2430
|
return;
|
|
2901
2431
|
}
|
|
2902
2432
|
if (options.record) {
|
|
2903
2433
|
const specPath = path8.resolve(cwd, "openpkg.json");
|
|
2904
2434
|
if (!fs6.existsSync(specPath)) {
|
|
2905
|
-
console.error(
|
|
2435
|
+
console.error(chalk9.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
|
|
2906
2436
|
process.exit(1);
|
|
2907
2437
|
}
|
|
2908
2438
|
try {
|
|
@@ -2910,24 +2440,24 @@ function registerTrendsCommand(program) {
|
|
|
2910
2440
|
const spec = JSON.parse(specContent);
|
|
2911
2441
|
const trend = getTrend(spec, cwd);
|
|
2912
2442
|
saveSnapshot(trend.current, cwd);
|
|
2913
|
-
console.log(
|
|
2443
|
+
console.log(chalk9.green("Recorded coverage snapshot:"));
|
|
2914
2444
|
console.log(formatSnapshot(trend.current));
|
|
2915
2445
|
if (trend.delta !== undefined) {
|
|
2916
2446
|
const deltaStr = formatDelta2(trend.delta);
|
|
2917
|
-
const deltaColor = trend.delta > 0 ?
|
|
2918
|
-
console.log(
|
|
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));
|
|
2919
2449
|
}
|
|
2920
2450
|
return;
|
|
2921
2451
|
} catch (error) {
|
|
2922
|
-
console.error(
|
|
2452
|
+
console.error(chalk9.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
|
|
2923
2453
|
process.exit(1);
|
|
2924
2454
|
}
|
|
2925
2455
|
}
|
|
2926
2456
|
const snapshots = loadSnapshots(cwd);
|
|
2927
2457
|
const limit = parseInt(options.limit ?? "10", 10);
|
|
2928
2458
|
if (snapshots.length === 0) {
|
|
2929
|
-
console.log(
|
|
2930
|
-
console.log(
|
|
2459
|
+
console.log(chalk9.yellow("No coverage history found."));
|
|
2460
|
+
console.log(chalk9.gray("Run `doccov trends --record` to save the current coverage."));
|
|
2931
2461
|
return;
|
|
2932
2462
|
}
|
|
2933
2463
|
if (options.json) {
|
|
@@ -2942,17 +2472,17 @@ function registerTrendsCommand(program) {
|
|
|
2942
2472
|
}
|
|
2943
2473
|
const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
|
|
2944
2474
|
const sparkline = renderSparkline(sparklineData);
|
|
2945
|
-
console.log(
|
|
2946
|
-
console.log(
|
|
2947
|
-
console.log(
|
|
2475
|
+
console.log(chalk9.bold("Coverage Trends"));
|
|
2476
|
+
console.log(chalk9.gray(`Package: ${snapshots[0].package}`));
|
|
2477
|
+
console.log(chalk9.gray(`Sparkline: ${sparkline}`));
|
|
2948
2478
|
console.log("");
|
|
2949
2479
|
if (snapshots.length >= 2) {
|
|
2950
2480
|
const oldest = snapshots[snapshots.length - 1];
|
|
2951
2481
|
const newest = snapshots[0];
|
|
2952
2482
|
const overallDelta = newest.coverageScore - oldest.coverageScore;
|
|
2953
2483
|
const deltaStr = formatDelta2(overallDelta);
|
|
2954
|
-
const deltaColor = overallDelta > 0 ?
|
|
2955
|
-
console.log(
|
|
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)`));
|
|
2956
2486
|
console.log("");
|
|
2957
2487
|
}
|
|
2958
2488
|
if (options.extended) {
|
|
@@ -2962,8 +2492,8 @@ function registerTrendsCommand(program) {
|
|
|
2962
2492
|
const specContent = fs6.readFileSync(specPath, "utf-8");
|
|
2963
2493
|
const spec = JSON.parse(specContent);
|
|
2964
2494
|
const extended = getExtendedTrend(spec, cwd, { tier });
|
|
2965
|
-
console.log(
|
|
2966
|
-
console.log(
|
|
2495
|
+
console.log(chalk9.bold("Extended Analysis"));
|
|
2496
|
+
console.log(chalk9.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
|
|
2967
2497
|
console.log("");
|
|
2968
2498
|
console.log(" Velocity:");
|
|
2969
2499
|
console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
|
|
@@ -2972,47 +2502,47 @@ function registerTrendsCommand(program) {
|
|
|
2972
2502
|
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
2973
2503
|
}
|
|
2974
2504
|
console.log("");
|
|
2975
|
-
const projColor = extended.projected30d >= extended.trend.current.coverageScore ?
|
|
2505
|
+
const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk9.green : chalk9.red;
|
|
2976
2506
|
console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
|
|
2977
|
-
console.log(` All-time high: ${
|
|
2978
|
-
console.log(` All-time low: ${
|
|
2507
|
+
console.log(` All-time high: ${chalk9.green(`${extended.allTimeHigh}%`)}`);
|
|
2508
|
+
console.log(` All-time low: ${chalk9.red(`${extended.allTimeLow}%`)}`);
|
|
2979
2509
|
if (extended.dataRange) {
|
|
2980
2510
|
const startDate = formatWeekDate(extended.dataRange.start);
|
|
2981
2511
|
const endDate = formatWeekDate(extended.dataRange.end);
|
|
2982
|
-
console.log(
|
|
2512
|
+
console.log(chalk9.gray(` Data range: ${startDate} - ${endDate}`));
|
|
2983
2513
|
}
|
|
2984
2514
|
console.log("");
|
|
2985
2515
|
if (options.weekly && extended.weeklySummaries.length > 0) {
|
|
2986
|
-
console.log(
|
|
2516
|
+
console.log(chalk9.bold("Weekly Summary"));
|
|
2987
2517
|
const weekLimit = Math.min(extended.weeklySummaries.length, 8);
|
|
2988
2518
|
for (let i = 0;i < weekLimit; i++) {
|
|
2989
2519
|
const week = extended.weeklySummaries[i];
|
|
2990
2520
|
const weekStart = formatWeekDate(week.weekStart);
|
|
2991
2521
|
const weekEnd = formatWeekDate(week.weekEnd);
|
|
2992
|
-
const deltaColor = week.delta > 0 ?
|
|
2522
|
+
const deltaColor = week.delta > 0 ? chalk9.green : week.delta < 0 ? chalk9.red : chalk9.gray;
|
|
2993
2523
|
const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
|
|
2994
2524
|
console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
|
|
2995
2525
|
}
|
|
2996
2526
|
if (extended.weeklySummaries.length > weekLimit) {
|
|
2997
|
-
console.log(
|
|
2527
|
+
console.log(chalk9.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
|
|
2998
2528
|
}
|
|
2999
2529
|
console.log("");
|
|
3000
2530
|
}
|
|
3001
2531
|
} catch {
|
|
3002
|
-
console.log(
|
|
2532
|
+
console.log(chalk9.yellow("Could not load openpkg.json for extended analysis"));
|
|
3003
2533
|
console.log("");
|
|
3004
2534
|
}
|
|
3005
2535
|
}
|
|
3006
2536
|
}
|
|
3007
|
-
console.log(
|
|
2537
|
+
console.log(chalk9.bold("History"));
|
|
3008
2538
|
const displaySnapshots = snapshots.slice(0, limit);
|
|
3009
2539
|
for (let i = 0;i < displaySnapshots.length; i++) {
|
|
3010
2540
|
const snapshot = displaySnapshots[i];
|
|
3011
|
-
const prefix = i === 0 ?
|
|
2541
|
+
const prefix = i === 0 ? chalk9.cyan("→") : " ";
|
|
3012
2542
|
console.log(`${prefix} ${formatSnapshot(snapshot)}`);
|
|
3013
2543
|
}
|
|
3014
2544
|
if (snapshots.length > limit) {
|
|
3015
|
-
console.log(
|
|
2545
|
+
console.log(chalk9.gray(` ... and ${snapshots.length - limit} more`));
|
|
3016
2546
|
}
|
|
3017
2547
|
});
|
|
3018
2548
|
}
|
|
@@ -3020,7 +2550,7 @@ function registerTrendsCommand(program) {
|
|
|
3020
2550
|
// src/cli.ts
|
|
3021
2551
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
3022
2552
|
var __dirname2 = path9.dirname(__filename2);
|
|
3023
|
-
var packageJson = JSON.parse(
|
|
2553
|
+
var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
|
|
3024
2554
|
var program = new Command;
|
|
3025
2555
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
3026
2556
|
registerCheckCommand(program);
|