@doccov/cli 0.16.0 → 0.18.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 +999 -185
- package/dist/config/index.d.ts +25 -15
- package/dist/config/index.js +21 -4
- package/package.json +4 -3
- package/dist/cli.d.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config/doccov-config.ts
|
|
4
|
-
import { access } from "node:fs/promises";
|
|
4
|
+
import { access, readFile } 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";
|
|
7
8
|
|
|
8
9
|
// src/config/schema.ts
|
|
9
10
|
import { z } from "zod";
|
|
@@ -34,13 +35,20 @@ var checkConfigSchema = z.object({
|
|
|
34
35
|
var qualityConfigSchema = z.object({
|
|
35
36
|
rules: z.record(severitySchema).optional()
|
|
36
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
|
+
});
|
|
37
44
|
var docCovConfigSchema = z.object({
|
|
38
45
|
include: stringList.optional(),
|
|
39
46
|
exclude: stringList.optional(),
|
|
40
47
|
plugins: z.array(z.unknown()).optional(),
|
|
41
48
|
docs: docsConfigSchema.optional(),
|
|
42
49
|
check: checkConfigSchema.optional(),
|
|
43
|
-
quality: qualityConfigSchema.optional()
|
|
50
|
+
quality: qualityConfigSchema.optional(),
|
|
51
|
+
policies: z.array(policyConfigSchema).optional()
|
|
44
52
|
});
|
|
45
53
|
var normalizeList = (value) => {
|
|
46
54
|
if (!value) {
|
|
@@ -78,13 +86,15 @@ var normalizeConfig = (input) => {
|
|
|
78
86
|
rules: input.quality.rules
|
|
79
87
|
};
|
|
80
88
|
}
|
|
89
|
+
const policies = input.policies;
|
|
81
90
|
return {
|
|
82
91
|
include,
|
|
83
92
|
exclude,
|
|
84
93
|
plugins: input.plugins,
|
|
85
94
|
docs,
|
|
86
95
|
check,
|
|
87
|
-
quality
|
|
96
|
+
quality,
|
|
97
|
+
policies
|
|
88
98
|
};
|
|
89
99
|
};
|
|
90
100
|
|
|
@@ -95,7 +105,9 @@ var DOCCOV_CONFIG_FILENAMES = [
|
|
|
95
105
|
"doccov.config.cts",
|
|
96
106
|
"doccov.config.js",
|
|
97
107
|
"doccov.config.mjs",
|
|
98
|
-
"doccov.config.cjs"
|
|
108
|
+
"doccov.config.cjs",
|
|
109
|
+
"doccov.yml",
|
|
110
|
+
"doccov.yaml"
|
|
99
111
|
];
|
|
100
112
|
var fileExists = async (filePath) => {
|
|
101
113
|
try {
|
|
@@ -122,6 +134,11 @@ var findConfigFile = async (cwd) => {
|
|
|
122
134
|
}
|
|
123
135
|
};
|
|
124
136
|
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
|
+
}
|
|
125
142
|
const fileUrl = pathToFileURL(absolutePath);
|
|
126
143
|
fileUrl.searchParams.set("t", Date.now().toString());
|
|
127
144
|
const module = await import(fileUrl.href);
|
|
@@ -160,8 +177,8 @@ ${formatIssues(issues)}`);
|
|
|
160
177
|
// src/config/index.ts
|
|
161
178
|
var defineConfig = (config) => config;
|
|
162
179
|
// src/cli.ts
|
|
163
|
-
import { readFileSync as
|
|
164
|
-
import * as
|
|
180
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
181
|
+
import * as path9 from "node:path";
|
|
165
182
|
import { fileURLToPath } from "node:url";
|
|
166
183
|
import { Command } from "commander";
|
|
167
184
|
|
|
@@ -169,11 +186,14 @@ import { Command } from "commander";
|
|
|
169
186
|
import * as fs2 from "node:fs";
|
|
170
187
|
import * as path4 from "node:path";
|
|
171
188
|
import {
|
|
189
|
+
analyzeSpecContributors,
|
|
190
|
+
analyzeSpecOwnership,
|
|
172
191
|
applyEdits,
|
|
173
192
|
categorizeDrifts,
|
|
174
193
|
createSourceFile,
|
|
175
194
|
DocCov,
|
|
176
195
|
enrichSpec,
|
|
196
|
+
evaluatePolicies,
|
|
177
197
|
findJSDocLocation,
|
|
178
198
|
generateFixesForExport,
|
|
179
199
|
generateReport,
|
|
@@ -188,8 +208,75 @@ import {
|
|
|
188
208
|
import {
|
|
189
209
|
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
190
210
|
} from "@openpkg-ts/spec";
|
|
191
|
-
import
|
|
211
|
+
import chalk5 from "chalk";
|
|
192
212
|
|
|
213
|
+
// src/reports/changelog-renderer.ts
|
|
214
|
+
function renderChangelog(data, options = {}) {
|
|
215
|
+
const { diff, categorizedBreaking } = data;
|
|
216
|
+
const lines = [];
|
|
217
|
+
const version = options.version ?? data.version ?? "Unreleased";
|
|
218
|
+
const date = options.date instanceof Date ? options.date.toISOString().split("T")[0] : options.date ?? new Date().toISOString().split("T")[0];
|
|
219
|
+
lines.push(`## [${version}] - ${date}`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
if (diff.breaking.length > 0) {
|
|
222
|
+
lines.push("### ⚠️ BREAKING CHANGES");
|
|
223
|
+
lines.push("");
|
|
224
|
+
if (categorizedBreaking && categorizedBreaking.length > 0) {
|
|
225
|
+
for (const breaking of categorizedBreaking) {
|
|
226
|
+
const severity = breaking.severity === "high" ? "**" : "";
|
|
227
|
+
lines.push(`- ${severity}${breaking.name}${severity}: ${breaking.reason}`);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
for (const id of diff.breaking) {
|
|
231
|
+
lines.push(`- \`${id}\` removed or changed`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
if (diff.nonBreaking.length > 0) {
|
|
237
|
+
lines.push("### Added");
|
|
238
|
+
lines.push("");
|
|
239
|
+
for (const id of diff.nonBreaking) {
|
|
240
|
+
lines.push(`- \`${id}\``);
|
|
241
|
+
}
|
|
242
|
+
lines.push("");
|
|
243
|
+
}
|
|
244
|
+
if (diff.docsOnly.length > 0) {
|
|
245
|
+
lines.push("### Documentation");
|
|
246
|
+
lines.push("");
|
|
247
|
+
for (const id of diff.docsOnly) {
|
|
248
|
+
lines.push(`- Updated documentation for \`${id}\``);
|
|
249
|
+
}
|
|
250
|
+
lines.push("");
|
|
251
|
+
}
|
|
252
|
+
if (diff.coverageDelta !== 0) {
|
|
253
|
+
lines.push("### Coverage");
|
|
254
|
+
lines.push("");
|
|
255
|
+
const arrow = diff.coverageDelta > 0 ? "↑" : "↓";
|
|
256
|
+
const sign = diff.coverageDelta > 0 ? "+" : "";
|
|
257
|
+
lines.push(`- Documentation coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% (${arrow} ${sign}${diff.coverageDelta}%)`);
|
|
258
|
+
lines.push("");
|
|
259
|
+
}
|
|
260
|
+
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
261
|
+
if (!lines.some((l) => l.startsWith("### Coverage"))) {
|
|
262
|
+
lines.push("### Coverage");
|
|
263
|
+
lines.push("");
|
|
264
|
+
}
|
|
265
|
+
if (diff.driftResolved > 0) {
|
|
266
|
+
lines.push(`- Fixed ${diff.driftResolved} drift issue${diff.driftResolved === 1 ? "" : "s"}`);
|
|
267
|
+
}
|
|
268
|
+
if (diff.driftIntroduced > 0) {
|
|
269
|
+
lines.push(`- ${diff.driftIntroduced} new drift issue${diff.driftIntroduced === 1 ? "" : "s"} detected`);
|
|
270
|
+
}
|
|
271
|
+
lines.push("");
|
|
272
|
+
}
|
|
273
|
+
if (options.compareUrl) {
|
|
274
|
+
lines.push(`**Full Changelog**: ${options.compareUrl}`);
|
|
275
|
+
lines.push("");
|
|
276
|
+
}
|
|
277
|
+
return lines.join(`
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
193
280
|
// src/reports/diff-markdown.ts
|
|
194
281
|
import * as path2 from "node:path";
|
|
195
282
|
function bar(pct, width = 10) {
|
|
@@ -891,19 +978,336 @@ function writeReports(options) {
|
|
|
891
978
|
}));
|
|
892
979
|
return results;
|
|
893
980
|
}
|
|
894
|
-
// src/utils/
|
|
981
|
+
// src/utils/ai-client.ts
|
|
982
|
+
import chalk2 from "chalk";
|
|
983
|
+
|
|
984
|
+
// src/utils/ai-generate.ts
|
|
895
985
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
896
986
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
897
987
|
import { generateObject } from "ai";
|
|
898
988
|
import { z as z2 } from "zod";
|
|
899
|
-
var
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
+
// src/utils/filter-options.ts
|
|
1248
|
+
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1249
|
+
import chalk3 from "chalk";
|
|
1250
|
+
var parseVisibilityFlag = (value) => {
|
|
1251
|
+
if (!value)
|
|
1252
|
+
return;
|
|
1253
|
+
const validTags = ["public", "beta", "alpha", "internal"];
|
|
1254
|
+
const parsed = parseListFlag(value);
|
|
1255
|
+
if (!parsed)
|
|
1256
|
+
return;
|
|
1257
|
+
const result = [];
|
|
1258
|
+
for (const tag of parsed) {
|
|
1259
|
+
const lower = tag.toLowerCase();
|
|
1260
|
+
if (validTags.includes(lower)) {
|
|
1261
|
+
result.push(lower);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return result.length > 0 ? result : undefined;
|
|
1265
|
+
};
|
|
1266
|
+
var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
|
|
1267
|
+
var mergeFilterOptions = (config, cliOptions) => {
|
|
1268
|
+
const messages = [];
|
|
1269
|
+
if (config?.include) {
|
|
1270
|
+
messages.push(formatList("include filters from config", config.include));
|
|
1271
|
+
}
|
|
1272
|
+
if (config?.exclude) {
|
|
1273
|
+
messages.push(formatList("exclude filters from config", config.exclude));
|
|
1274
|
+
}
|
|
1275
|
+
if (cliOptions.include) {
|
|
1276
|
+
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
1277
|
+
}
|
|
1278
|
+
if (cliOptions.exclude) {
|
|
1279
|
+
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
1280
|
+
}
|
|
1281
|
+
if (cliOptions.visibility) {
|
|
1282
|
+
messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
|
|
1283
|
+
}
|
|
1284
|
+
const resolved = mergeFilters(config, cliOptions);
|
|
1285
|
+
if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
|
|
1286
|
+
return { messages };
|
|
1287
|
+
}
|
|
1288
|
+
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
1289
|
+
return {
|
|
1290
|
+
include: resolved.include,
|
|
1291
|
+
exclude: resolved.exclude,
|
|
1292
|
+
visibility: cliOptions.visibility,
|
|
1293
|
+
source,
|
|
1294
|
+
messages
|
|
1295
|
+
};
|
|
1296
|
+
};
|
|
1297
|
+
|
|
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")
|
|
905
1309
|
})).describe("List of assertion-like comments found in the code"),
|
|
906
|
-
hasAssertions:
|
|
1310
|
+
hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
|
|
907
1311
|
});
|
|
908
1312
|
var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
|
|
909
1313
|
|
|
@@ -930,13 +1334,13 @@ Code:
|
|
|
930
1334
|
\`\`\`
|
|
931
1335
|
${code}
|
|
932
1336
|
\`\`\``;
|
|
933
|
-
function
|
|
1337
|
+
function getModel2() {
|
|
934
1338
|
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
935
1339
|
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
936
|
-
const anthropic =
|
|
1340
|
+
const anthropic = createAnthropic2();
|
|
937
1341
|
return anthropic("claude-sonnet-4-20250514");
|
|
938
1342
|
}
|
|
939
|
-
const openai =
|
|
1343
|
+
const openai = createOpenAI2();
|
|
940
1344
|
return openai("gpt-4o-mini");
|
|
941
1345
|
}
|
|
942
1346
|
function isLLMAssertionParsingAvailable() {
|
|
@@ -947,8 +1351,8 @@ async function parseAssertionsWithLLM(code) {
|
|
|
947
1351
|
return null;
|
|
948
1352
|
}
|
|
949
1353
|
try {
|
|
950
|
-
const model =
|
|
951
|
-
const { object } = await
|
|
1354
|
+
const model = getModel2();
|
|
1355
|
+
const { object } = await generateObject2({
|
|
952
1356
|
model,
|
|
953
1357
|
schema: AssertionParseSchema,
|
|
954
1358
|
prompt: ASSERTION_PARSE_PROMPT(code)
|
|
@@ -960,7 +1364,7 @@ async function parseAssertionsWithLLM(code) {
|
|
|
960
1364
|
}
|
|
961
1365
|
|
|
962
1366
|
// src/utils/progress.ts
|
|
963
|
-
import
|
|
1367
|
+
import chalk4 from "chalk";
|
|
964
1368
|
class StepProgress {
|
|
965
1369
|
steps;
|
|
966
1370
|
currentStep = 0;
|
|
@@ -988,7 +1392,7 @@ class StepProgress {
|
|
|
988
1392
|
this.completeCurrentStep();
|
|
989
1393
|
if (message) {
|
|
990
1394
|
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
991
|
-
console.log(`${
|
|
1395
|
+
console.log(`${chalk4.green("✓")} ${message} ${chalk4.dim(`(${elapsed}s)`)}`);
|
|
992
1396
|
}
|
|
993
1397
|
}
|
|
994
1398
|
render() {
|
|
@@ -996,17 +1400,17 @@ class StepProgress {
|
|
|
996
1400
|
if (!step)
|
|
997
1401
|
return;
|
|
998
1402
|
const label = step.activeLabel ?? step.label;
|
|
999
|
-
const prefix =
|
|
1000
|
-
process.stdout.write(`\r${prefix} ${
|
|
1403
|
+
const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1404
|
+
process.stdout.write(`\r${prefix} ${chalk4.cyan(label)}...`);
|
|
1001
1405
|
}
|
|
1002
1406
|
completeCurrentStep() {
|
|
1003
1407
|
const step = this.steps[this.currentStep];
|
|
1004
1408
|
if (!step)
|
|
1005
1409
|
return;
|
|
1006
1410
|
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
1007
|
-
const prefix =
|
|
1411
|
+
const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1008
1412
|
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
1009
|
-
console.log(`${prefix} ${step.label} ${
|
|
1413
|
+
console.log(`${prefix} ${step.label} ${chalk4.green("✓")} ${chalk4.dim(`(${elapsed}s)`)}`);
|
|
1010
1414
|
}
|
|
1011
1415
|
}
|
|
1012
1416
|
|
|
@@ -1050,7 +1454,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1050
1454
|
...defaultDependencies,
|
|
1051
1455
|
...dependencies
|
|
1052
1456
|
};
|
|
1053
|
-
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("--dry-run", "Preview fixes without writing (requires --fix)").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").action(async (entry, options) => {
|
|
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("--generate", "AI-generate missing JSDoc (requires --fix and API key)").option("--dry-run", "Preview fixes without writing (requires --fix)").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)").option("--owners", "Show coverage breakdown by CODEOWNERS").option("--contributors", "Show documentation contributors (git blame)").action(async (entry, options) => {
|
|
1054
1458
|
try {
|
|
1055
1459
|
const validations = parseExamplesFlag(options.examples);
|
|
1056
1460
|
const hasExamples = validations.length > 0;
|
|
@@ -1077,6 +1481,15 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1077
1481
|
const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
|
|
1078
1482
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
1079
1483
|
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
1484
|
+
const cliFilters = {
|
|
1485
|
+
include: undefined,
|
|
1486
|
+
exclude: undefined,
|
|
1487
|
+
visibility: parseVisibilityFlag(options.visibility)
|
|
1488
|
+
};
|
|
1489
|
+
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
1490
|
+
if (resolvedFilters.visibility) {
|
|
1491
|
+
log(chalk5.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
|
|
1492
|
+
}
|
|
1080
1493
|
steps.next();
|
|
1081
1494
|
const resolveExternalTypes = !options.skipResolve;
|
|
1082
1495
|
let specResult;
|
|
@@ -1086,7 +1499,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1086
1499
|
useCache: options.cache !== false,
|
|
1087
1500
|
cwd: options.cwd
|
|
1088
1501
|
});
|
|
1089
|
-
|
|
1502
|
+
const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
|
|
1503
|
+
specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1090
1504
|
if (!specResult) {
|
|
1091
1505
|
throw new Error("Failed to analyze documentation coverage.");
|
|
1092
1506
|
}
|
|
@@ -1103,6 +1517,24 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1103
1517
|
violations.push({ exportName: exp.name, violation: v });
|
|
1104
1518
|
}
|
|
1105
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
|
+
}
|
|
1106
1538
|
let exampleResult;
|
|
1107
1539
|
const typecheckErrors = [];
|
|
1108
1540
|
const runtimeDrifts = [];
|
|
@@ -1148,12 +1580,12 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1148
1580
|
if (allDrifts.length > 0) {
|
|
1149
1581
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1150
1582
|
if (fixable.length === 0) {
|
|
1151
|
-
log(
|
|
1583
|
+
log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1152
1584
|
} else {
|
|
1153
1585
|
log("");
|
|
1154
|
-
log(
|
|
1586
|
+
log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
1155
1587
|
if (nonFixable.length > 0) {
|
|
1156
|
-
log(
|
|
1588
|
+
log(chalk5.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
1157
1589
|
}
|
|
1158
1590
|
log("");
|
|
1159
1591
|
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
@@ -1161,22 +1593,22 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1161
1593
|
const editsByFile = new Map;
|
|
1162
1594
|
for (const [exp, drifts] of groupedDrifts) {
|
|
1163
1595
|
if (!exp.source?.file) {
|
|
1164
|
-
log(
|
|
1596
|
+
log(chalk5.gray(` Skipping ${exp.name}: no source location`));
|
|
1165
1597
|
continue;
|
|
1166
1598
|
}
|
|
1167
1599
|
if (exp.source.file.endsWith(".d.ts")) {
|
|
1168
|
-
log(
|
|
1600
|
+
log(chalk5.gray(` Skipping ${exp.name}: declaration file`));
|
|
1169
1601
|
continue;
|
|
1170
1602
|
}
|
|
1171
1603
|
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1172
1604
|
if (!fs2.existsSync(filePath)) {
|
|
1173
|
-
log(
|
|
1605
|
+
log(chalk5.gray(` Skipping ${exp.name}: file not found`));
|
|
1174
1606
|
continue;
|
|
1175
1607
|
}
|
|
1176
1608
|
const sourceFile = createSourceFile(filePath);
|
|
1177
1609
|
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1178
1610
|
if (!location) {
|
|
1179
|
-
log(
|
|
1611
|
+
log(chalk5.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
1180
1612
|
continue;
|
|
1181
1613
|
}
|
|
1182
1614
|
let existingPatch = {};
|
|
@@ -1209,26 +1641,26 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1209
1641
|
}
|
|
1210
1642
|
if (edits.length > 0) {
|
|
1211
1643
|
if (options.dryRun) {
|
|
1212
|
-
log(
|
|
1644
|
+
log(chalk5.bold("Dry run - changes that would be made:"));
|
|
1213
1645
|
log("");
|
|
1214
1646
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
1215
1647
|
const relativePath = path4.relative(targetDir, filePath);
|
|
1216
|
-
log(
|
|
1648
|
+
log(chalk5.cyan(` ${relativePath}:`));
|
|
1217
1649
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
1218
1650
|
const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
|
|
1219
|
-
log(` ${
|
|
1651
|
+
log(` ${chalk5.bold(exp.name)} [${lineInfo}]`);
|
|
1220
1652
|
for (const fix of fixes) {
|
|
1221
|
-
log(
|
|
1653
|
+
log(chalk5.green(` + ${fix.description}`));
|
|
1222
1654
|
}
|
|
1223
1655
|
}
|
|
1224
1656
|
log("");
|
|
1225
1657
|
}
|
|
1226
|
-
log(
|
|
1658
|
+
log(chalk5.gray("Run without --dry-run to apply these changes."));
|
|
1227
1659
|
} else {
|
|
1228
1660
|
const applyResult = await applyEdits(edits);
|
|
1229
1661
|
if (applyResult.errors.length > 0) {
|
|
1230
1662
|
for (const err of applyResult.errors) {
|
|
1231
|
-
error(
|
|
1663
|
+
error(chalk5.red(` ${err.file}: ${err.error}`));
|
|
1232
1664
|
}
|
|
1233
1665
|
}
|
|
1234
1666
|
}
|
|
@@ -1239,6 +1671,99 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1239
1671
|
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1240
1672
|
}
|
|
1241
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
|
+
}
|
|
1747
|
+
log("");
|
|
1748
|
+
log(chalk5.gray("Run without --dry-run to apply these changes."));
|
|
1749
|
+
} else {
|
|
1750
|
+
const applyResult = await applyEdits(edits);
|
|
1751
|
+
log("");
|
|
1752
|
+
log(chalk5.green(`✓ Generated JSDoc for ${edits.length} export(s)`));
|
|
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
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1242
1767
|
steps.complete("Check complete");
|
|
1243
1768
|
if (format !== "text") {
|
|
1244
1769
|
const limit = parseInt(options.limit, 10) || 20;
|
|
@@ -1284,7 +1809,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1284
1809
|
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
1285
1810
|
const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1286
1811
|
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
1287
|
-
|
|
1812
|
+
const policiesFailed2 = policyResult && !policyResult.allPassed;
|
|
1813
|
+
if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
|
|
1288
1814
|
process.exit(1);
|
|
1289
1815
|
}
|
|
1290
1816
|
return;
|
|
@@ -1296,18 +1822,19 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1296
1822
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1297
1823
|
const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1298
1824
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1825
|
+
const policiesFailed = policyResult && !policyResult.allPassed;
|
|
1299
1826
|
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
1300
1827
|
log("");
|
|
1301
1828
|
for (const diag of specWarnings) {
|
|
1302
|
-
log(
|
|
1829
|
+
log(chalk5.yellow(`⚠ ${diag.message}`));
|
|
1303
1830
|
if (diag.suggestion) {
|
|
1304
|
-
log(
|
|
1831
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1305
1832
|
}
|
|
1306
1833
|
}
|
|
1307
1834
|
for (const diag of specInfos) {
|
|
1308
|
-
log(
|
|
1835
|
+
log(chalk5.cyan(`ℹ ${diag.message}`));
|
|
1309
1836
|
if (diag.suggestion) {
|
|
1310
|
-
log(
|
|
1837
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1311
1838
|
}
|
|
1312
1839
|
}
|
|
1313
1840
|
}
|
|
@@ -1317,23 +1844,23 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1317
1844
|
const errorCount = violations.filter((v) => v.violation.severity === "error").length;
|
|
1318
1845
|
const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
|
|
1319
1846
|
log("");
|
|
1320
|
-
log(
|
|
1847
|
+
log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1321
1848
|
log("");
|
|
1322
1849
|
log(` Exports: ${totalExports}`);
|
|
1323
1850
|
if (minCoverage !== undefined) {
|
|
1324
1851
|
if (coverageFailed) {
|
|
1325
|
-
log(
|
|
1852
|
+
log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1326
1853
|
} else {
|
|
1327
|
-
log(
|
|
1854
|
+
log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1328
1855
|
}
|
|
1329
1856
|
} else {
|
|
1330
1857
|
log(` Coverage: ${coverageScore}%`);
|
|
1331
1858
|
}
|
|
1332
1859
|
if (maxDrift !== undefined) {
|
|
1333
1860
|
if (driftFailed) {
|
|
1334
|
-
log(
|
|
1861
|
+
log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1335
1862
|
} else {
|
|
1336
|
-
log(
|
|
1863
|
+
log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1337
1864
|
}
|
|
1338
1865
|
} else {
|
|
1339
1866
|
log(` Drift: ${driftScore}%`);
|
|
@@ -1343,7 +1870,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1343
1870
|
if (typecheckCount > 0) {
|
|
1344
1871
|
log(` Examples: ${typecheckCount} type errors`);
|
|
1345
1872
|
} else {
|
|
1346
|
-
log(
|
|
1873
|
+
log(chalk5.green(` Examples: ✓ validated`));
|
|
1347
1874
|
}
|
|
1348
1875
|
}
|
|
1349
1876
|
if (errorCount > 0 || warnCount > 0) {
|
|
@@ -1354,8 +1881,57 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1354
1881
|
parts.push(`${warnCount} warnings`);
|
|
1355
1882
|
log(` Quality: ${parts.join(", ")}`);
|
|
1356
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
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (ownershipResult) {
|
|
1902
|
+
log("");
|
|
1903
|
+
log(chalk5.bold(" Owners:"));
|
|
1904
|
+
const sortedOwners = [...ownershipResult.byOwner.values()].sort((a, b) => a.coverageScore - b.coverageScore);
|
|
1905
|
+
for (const stats of sortedOwners) {
|
|
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`));
|
|
1928
|
+
}
|
|
1929
|
+
if (contributorResult.unattributed.length > 0) {
|
|
1930
|
+
log(chalk5.dim(` (${contributorResult.unattributed.length} exports could not be attributed via git blame)`));
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1357
1933
|
log("");
|
|
1358
|
-
const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
|
|
1934
|
+
const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors || policiesFailed;
|
|
1359
1935
|
if (!failed) {
|
|
1360
1936
|
const thresholdParts = [];
|
|
1361
1937
|
if (minCoverage !== undefined) {
|
|
@@ -1364,25 +1940,31 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1364
1940
|
if (maxDrift !== undefined) {
|
|
1365
1941
|
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1366
1942
|
}
|
|
1943
|
+
if (policyResult) {
|
|
1944
|
+
thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
|
|
1945
|
+
}
|
|
1367
1946
|
if (thresholdParts.length > 0) {
|
|
1368
|
-
log(
|
|
1947
|
+
log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1369
1948
|
} else {
|
|
1370
|
-
log(
|
|
1371
|
-
log(
|
|
1949
|
+
log(chalk5.green("✓ Check passed"));
|
|
1950
|
+
log(chalk5.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
|
|
1372
1951
|
}
|
|
1373
1952
|
return;
|
|
1374
1953
|
}
|
|
1375
1954
|
if (hasQualityErrors) {
|
|
1376
|
-
log(
|
|
1955
|
+
log(chalk5.red(`✗ ${errorCount} quality errors`));
|
|
1377
1956
|
}
|
|
1378
1957
|
if (hasTypecheckErrors) {
|
|
1379
|
-
log(
|
|
1958
|
+
log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1959
|
+
}
|
|
1960
|
+
if (policiesFailed && policyResult) {
|
|
1961
|
+
log(chalk5.red(`✗ ${policyResult.failedCount} policy failures`));
|
|
1380
1962
|
}
|
|
1381
1963
|
log("");
|
|
1382
|
-
log(
|
|
1964
|
+
log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
|
|
1383
1965
|
process.exit(1);
|
|
1384
1966
|
} catch (commandError) {
|
|
1385
|
-
error(
|
|
1967
|
+
error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1386
1968
|
process.exit(1);
|
|
1387
1969
|
}
|
|
1388
1970
|
});
|
|
@@ -1419,35 +2001,36 @@ import {
|
|
|
1419
2001
|
hashString,
|
|
1420
2002
|
parseMarkdownFiles
|
|
1421
2003
|
} from "@doccov/sdk";
|
|
1422
|
-
import
|
|
2004
|
+
import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
|
|
2005
|
+
import chalk6 from "chalk";
|
|
1423
2006
|
import { glob } from "glob";
|
|
1424
2007
|
|
|
1425
2008
|
// src/utils/docs-impact-ai.ts
|
|
1426
|
-
import { createAnthropic as
|
|
1427
|
-
import { createOpenAI as
|
|
1428
|
-
import { generateObject as
|
|
1429
|
-
import { z as
|
|
1430
|
-
var CodeBlockUsageSchema =
|
|
1431
|
-
isImpacted:
|
|
1432
|
-
reason:
|
|
1433
|
-
usageType:
|
|
1434
|
-
suggestedFix:
|
|
1435
|
-
confidence:
|
|
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")
|
|
1436
2019
|
});
|
|
1437
|
-
var MultiBlockAnalysisSchema =
|
|
1438
|
-
groups:
|
|
1439
|
-
blockIndices:
|
|
1440
|
-
reason:
|
|
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")
|
|
1441
2024
|
})).describe("Groups of related code blocks"),
|
|
1442
|
-
skippedBlocks:
|
|
2025
|
+
skippedBlocks: z4.array(z4.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
|
|
1443
2026
|
});
|
|
1444
|
-
function
|
|
2027
|
+
function getModel3() {
|
|
1445
2028
|
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
1446
2029
|
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
1447
|
-
const anthropic =
|
|
2030
|
+
const anthropic = createAnthropic3();
|
|
1448
2031
|
return anthropic("claude-sonnet-4-20250514");
|
|
1449
2032
|
}
|
|
1450
|
-
const openai =
|
|
2033
|
+
const openai = createOpenAI3();
|
|
1451
2034
|
return openai("gpt-4o-mini");
|
|
1452
2035
|
}
|
|
1453
2036
|
function isAIDocsAnalysisAvailable() {
|
|
@@ -1462,7 +2045,7 @@ async function generateImpactSummary(impacts) {
|
|
|
1462
2045
|
}
|
|
1463
2046
|
try {
|
|
1464
2047
|
const { text } = await generateText({
|
|
1465
|
-
model:
|
|
2048
|
+
model: getModel3(),
|
|
1466
2049
|
prompt: `Summarize these documentation impacts for a GitHub PR comment.
|
|
1467
2050
|
|
|
1468
2051
|
Impacts:
|
|
@@ -1507,7 +2090,7 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1507
2090
|
...defaultDependencies2,
|
|
1508
2091
|
...dependencies
|
|
1509
2092
|
};
|
|
1510
|
-
program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
|
|
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", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").option("--recommend-version", "Output recommended semver version bump").action(async (baseArg, headArg, options) => {
|
|
1511
2094
|
try {
|
|
1512
2095
|
const baseFile = options.base ?? baseArg;
|
|
1513
2096
|
const headFile = options.head ?? headArg;
|
|
@@ -1538,6 +2121,29 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1538
2121
|
}
|
|
1539
2122
|
const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
|
|
1540
2123
|
const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
|
|
2124
|
+
if (options.recommendVersion) {
|
|
2125
|
+
const recommendation = recommendSemverBump(diff);
|
|
2126
|
+
const currentVersion = headSpec.meta?.version ?? "0.0.0";
|
|
2127
|
+
const nextVersion = calculateNextVersion(currentVersion, recommendation.bump);
|
|
2128
|
+
if (options.format === "json") {
|
|
2129
|
+
log(JSON.stringify({
|
|
2130
|
+
current: currentVersion,
|
|
2131
|
+
recommended: nextVersion,
|
|
2132
|
+
bump: recommendation.bump,
|
|
2133
|
+
reason: recommendation.reason,
|
|
2134
|
+
breakingCount: recommendation.breakingCount,
|
|
2135
|
+
additionCount: recommendation.additionCount,
|
|
2136
|
+
docsOnlyChanges: recommendation.docsOnlyChanges
|
|
2137
|
+
}, null, 2));
|
|
2138
|
+
} else {
|
|
2139
|
+
log("");
|
|
2140
|
+
log(chalk6.bold("Semver Recommendation"));
|
|
2141
|
+
log(` Current version: ${currentVersion}`);
|
|
2142
|
+
log(` Recommended: ${chalk6.cyan(nextVersion)} (${chalk6.yellow(recommendation.bump.toUpperCase())})`);
|
|
2143
|
+
log(` Reason: ${recommendation.reason}`);
|
|
2144
|
+
}
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
1541
2147
|
const format = options.format ?? "text";
|
|
1542
2148
|
const limit = parseInt(options.limit, 10) || 10;
|
|
1543
2149
|
const checks = getStrictChecks(options.strict);
|
|
@@ -1565,8 +2171,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1565
2171
|
silent: true
|
|
1566
2172
|
});
|
|
1567
2173
|
}
|
|
1568
|
-
const cacheNote = fromCache ?
|
|
1569
|
-
log(
|
|
2174
|
+
const cacheNote = fromCache ? chalk6.cyan(" (cached)") : "";
|
|
2175
|
+
log(chalk6.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
1570
2176
|
}
|
|
1571
2177
|
break;
|
|
1572
2178
|
case "json": {
|
|
@@ -1627,6 +2233,28 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1627
2233
|
log(content);
|
|
1628
2234
|
break;
|
|
1629
2235
|
}
|
|
2236
|
+
case "changelog": {
|
|
2237
|
+
const content = renderChangelog({
|
|
2238
|
+
diff,
|
|
2239
|
+
categorizedBreaking: diff.categorizedBreaking,
|
|
2240
|
+
version: headSpec.meta?.version
|
|
2241
|
+
}, {
|
|
2242
|
+
version: headSpec.meta?.version,
|
|
2243
|
+
compareUrl: options.repoUrl ? `${options.repoUrl}/compare/${baseSpec.meta?.version ?? "v0"}...${headSpec.meta?.version ?? "HEAD"}` : undefined
|
|
2244
|
+
});
|
|
2245
|
+
if (options.stdout) {
|
|
2246
|
+
log(content);
|
|
2247
|
+
} else {
|
|
2248
|
+
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "md");
|
|
2249
|
+
writeReport({
|
|
2250
|
+
format: "markdown",
|
|
2251
|
+
content,
|
|
2252
|
+
outputPath: outputPath.replace(/\.(json|html)$/, ".changelog.md"),
|
|
2253
|
+
cwd: options.cwd
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
1630
2258
|
}
|
|
1631
2259
|
const failures = validateDiff(diff, headSpec, {
|
|
1632
2260
|
minCoverage,
|
|
@@ -1634,18 +2262,18 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1634
2262
|
checks
|
|
1635
2263
|
});
|
|
1636
2264
|
if (failures.length > 0) {
|
|
1637
|
-
log(
|
|
2265
|
+
log(chalk6.red(`
|
|
1638
2266
|
✗ Check failed`));
|
|
1639
2267
|
for (const f of failures) {
|
|
1640
|
-
log(
|
|
2268
|
+
log(chalk6.red(` - ${f}`));
|
|
1641
2269
|
}
|
|
1642
2270
|
process.exitCode = 1;
|
|
1643
2271
|
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
1644
|
-
log(
|
|
2272
|
+
log(chalk6.green(`
|
|
1645
2273
|
✓ All checks passed`));
|
|
1646
2274
|
}
|
|
1647
2275
|
} catch (commandError) {
|
|
1648
|
-
error(
|
|
2276
|
+
error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1649
2277
|
process.exitCode = 1;
|
|
1650
2278
|
}
|
|
1651
2279
|
});
|
|
@@ -1672,7 +2300,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
1672
2300
|
if (!docsPatterns || docsPatterns.length === 0) {
|
|
1673
2301
|
if (config?.docs?.include) {
|
|
1674
2302
|
docsPatterns = config.docs.include;
|
|
1675
|
-
log(
|
|
2303
|
+
log(chalk6.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
1676
2304
|
}
|
|
1677
2305
|
}
|
|
1678
2306
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
@@ -1695,46 +2323,46 @@ function loadSpec(filePath, readFileSync2) {
|
|
|
1695
2323
|
}
|
|
1696
2324
|
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1697
2325
|
log("");
|
|
1698
|
-
const cacheIndicator = fromCache ?
|
|
1699
|
-
log(
|
|
2326
|
+
const cacheIndicator = fromCache ? chalk6.cyan(" (cached)") : "";
|
|
2327
|
+
log(chalk6.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
1700
2328
|
log("─".repeat(40));
|
|
1701
2329
|
log("");
|
|
1702
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
2330
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk6.green : diff.coverageDelta < 0 ? chalk6.red : chalk6.gray;
|
|
1703
2331
|
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1704
2332
|
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
1705
2333
|
const breakingCount = diff.breaking.length;
|
|
1706
2334
|
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
1707
2335
|
if (breakingCount > 0) {
|
|
1708
|
-
const severityNote = highSeverity > 0 ?
|
|
1709
|
-
log(` Breaking: ${
|
|
2336
|
+
const severityNote = highSeverity > 0 ? chalk6.red(` (${highSeverity} high severity)`) : "";
|
|
2337
|
+
log(` Breaking: ${chalk6.red(breakingCount)} changes${severityNote}`);
|
|
1710
2338
|
} else {
|
|
1711
|
-
log(` Breaking: ${
|
|
2339
|
+
log(` Breaking: ${chalk6.green("0")} changes`);
|
|
1712
2340
|
}
|
|
1713
2341
|
const newCount = diff.nonBreaking.length;
|
|
1714
2342
|
const undocCount = diff.newUndocumented.length;
|
|
1715
2343
|
if (newCount > 0) {
|
|
1716
|
-
const undocNote = undocCount > 0 ?
|
|
1717
|
-
log(` New: ${
|
|
2344
|
+
const undocNote = undocCount > 0 ? chalk6.yellow(` (${undocCount} undocumented)`) : "";
|
|
2345
|
+
log(` New: ${chalk6.green(newCount)} exports${undocNote}`);
|
|
1718
2346
|
}
|
|
1719
2347
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1720
2348
|
const parts = [];
|
|
1721
2349
|
if (diff.driftIntroduced > 0)
|
|
1722
|
-
parts.push(
|
|
2350
|
+
parts.push(chalk6.red(`+${diff.driftIntroduced}`));
|
|
1723
2351
|
if (diff.driftResolved > 0)
|
|
1724
|
-
parts.push(
|
|
2352
|
+
parts.push(chalk6.green(`-${diff.driftResolved}`));
|
|
1725
2353
|
log(` Drift: ${parts.join(", ")}`);
|
|
1726
2354
|
}
|
|
1727
2355
|
log("");
|
|
1728
2356
|
}
|
|
1729
2357
|
async function printAISummary(diff, log) {
|
|
1730
2358
|
if (!isAIDocsAnalysisAvailable()) {
|
|
1731
|
-
log(
|
|
2359
|
+
log(chalk6.yellow(`
|
|
1732
2360
|
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
1733
2361
|
return;
|
|
1734
2362
|
}
|
|
1735
2363
|
if (!diff.docsImpact)
|
|
1736
2364
|
return;
|
|
1737
|
-
log(
|
|
2365
|
+
log(chalk6.gray(`
|
|
1738
2366
|
Generating AI summary...`));
|
|
1739
2367
|
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
1740
2368
|
file: f.file,
|
|
@@ -1745,8 +2373,8 @@ Generating AI summary...`));
|
|
|
1745
2373
|
const summary = await generateImpactSummary(impacts);
|
|
1746
2374
|
if (summary) {
|
|
1747
2375
|
log("");
|
|
1748
|
-
log(
|
|
1749
|
-
log(
|
|
2376
|
+
log(chalk6.bold("AI Summary"));
|
|
2377
|
+
log(chalk6.cyan(` ${summary}`));
|
|
1750
2378
|
}
|
|
1751
2379
|
}
|
|
1752
2380
|
function validateDiff(diff, headSpec, options) {
|
|
@@ -1827,7 +2455,7 @@ function printGitHubAnnotations(diff, log) {
|
|
|
1827
2455
|
|
|
1828
2456
|
// src/commands/info.ts
|
|
1829
2457
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
1830
|
-
import
|
|
2458
|
+
import chalk7 from "chalk";
|
|
1831
2459
|
function registerInfoCommand(program) {
|
|
1832
2460
|
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) => {
|
|
1833
2461
|
try {
|
|
@@ -1849,14 +2477,14 @@ function registerInfoCommand(program) {
|
|
|
1849
2477
|
const spec = enrichSpec2(specResult.spec);
|
|
1850
2478
|
const stats = computeStats(spec);
|
|
1851
2479
|
console.log("");
|
|
1852
|
-
console.log(
|
|
2480
|
+
console.log(chalk7.bold(`${stats.packageName}@${stats.version}`));
|
|
1853
2481
|
console.log("");
|
|
1854
|
-
console.log(` Exports: ${
|
|
1855
|
-
console.log(` Coverage: ${
|
|
1856
|
-
console.log(` Drift: ${
|
|
2482
|
+
console.log(` Exports: ${chalk7.bold(stats.totalExports.toString())}`);
|
|
2483
|
+
console.log(` Coverage: ${chalk7.bold(`${stats.coverageScore}%`)}`);
|
|
2484
|
+
console.log(` Drift: ${chalk7.bold(`${stats.driftScore}%`)}`);
|
|
1857
2485
|
console.log("");
|
|
1858
2486
|
} catch (err) {
|
|
1859
|
-
console.error(
|
|
2487
|
+
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
1860
2488
|
process.exit(1);
|
|
1861
2489
|
}
|
|
1862
2490
|
});
|
|
@@ -1865,7 +2493,7 @@ function registerInfoCommand(program) {
|
|
|
1865
2493
|
// src/commands/init.ts
|
|
1866
2494
|
import * as fs4 from "node:fs";
|
|
1867
2495
|
import * as path6 from "node:path";
|
|
1868
|
-
import
|
|
2496
|
+
import chalk8 from "chalk";
|
|
1869
2497
|
var defaultDependencies3 = {
|
|
1870
2498
|
fileExists: fs4.existsSync,
|
|
1871
2499
|
writeFileSync: fs4.writeFileSync,
|
|
@@ -1878,39 +2506,39 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1878
2506
|
...defaultDependencies3,
|
|
1879
2507
|
...dependencies
|
|
1880
2508
|
};
|
|
1881
|
-
program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
|
|
2509
|
+
program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs, yaml", "auto").action((options) => {
|
|
1882
2510
|
const cwd = path6.resolve(options.cwd);
|
|
1883
2511
|
const formatOption = String(options.format ?? "auto").toLowerCase();
|
|
1884
2512
|
if (!isValidFormat(formatOption)) {
|
|
1885
|
-
error(
|
|
2513
|
+
error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
|
|
1886
2514
|
process.exitCode = 1;
|
|
1887
2515
|
return;
|
|
1888
2516
|
}
|
|
1889
2517
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
1890
2518
|
if (existing) {
|
|
1891
|
-
error(
|
|
2519
|
+
error(chalk8.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
1892
2520
|
process.exitCode = 1;
|
|
1893
2521
|
return;
|
|
1894
2522
|
}
|
|
1895
2523
|
const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
|
|
1896
2524
|
const targetFormat = resolveFormat(formatOption, packageType);
|
|
1897
2525
|
if (targetFormat === "js" && packageType !== "module") {
|
|
1898
|
-
log(
|
|
2526
|
+
log(chalk8.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
|
|
1899
2527
|
}
|
|
1900
|
-
const fileName = `doccov.config.${targetFormat}`;
|
|
2528
|
+
const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
|
|
1901
2529
|
const outputPath = path6.join(cwd, fileName);
|
|
1902
2530
|
if (fileExists2(outputPath)) {
|
|
1903
|
-
error(
|
|
2531
|
+
error(chalk8.red(`Cannot create ${fileName}; file already exists.`));
|
|
1904
2532
|
process.exitCode = 1;
|
|
1905
2533
|
return;
|
|
1906
2534
|
}
|
|
1907
2535
|
const template = buildTemplate(targetFormat);
|
|
1908
2536
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
1909
|
-
log(
|
|
2537
|
+
log(chalk8.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
|
|
1910
2538
|
});
|
|
1911
2539
|
}
|
|
1912
2540
|
var isValidFormat = (value) => {
|
|
1913
|
-
return
|
|
2541
|
+
return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
|
|
1914
2542
|
};
|
|
1915
2543
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
1916
2544
|
let current = path6.resolve(cwd);
|
|
@@ -1962,12 +2590,34 @@ var findNearestPackageJson = (cwd, fileExists2) => {
|
|
|
1962
2590
|
return null;
|
|
1963
2591
|
};
|
|
1964
2592
|
var resolveFormat = (format, packageType) => {
|
|
2593
|
+
if (format === "yaml")
|
|
2594
|
+
return "yaml";
|
|
1965
2595
|
if (format === "auto") {
|
|
1966
2596
|
return packageType === "module" ? "js" : "mjs";
|
|
1967
2597
|
}
|
|
1968
2598
|
return format;
|
|
1969
2599
|
};
|
|
1970
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
|
|
2613
|
+
|
|
2614
|
+
quality:
|
|
2615
|
+
rules:
|
|
2616
|
+
# has-description: warn
|
|
2617
|
+
# has-params: off
|
|
2618
|
+
# has-returns: off
|
|
2619
|
+
`;
|
|
2620
|
+
}
|
|
1971
2621
|
const configBody = `{
|
|
1972
2622
|
// Filter which exports to analyze
|
|
1973
2623
|
// include: ['MyClass', 'myFunction'],
|
|
@@ -2018,42 +2668,11 @@ var buildTemplate = (format) => {
|
|
|
2018
2668
|
// src/commands/spec.ts
|
|
2019
2669
|
import * as fs5 from "node:fs";
|
|
2020
2670
|
import * as path7 from "node:path";
|
|
2021
|
-
import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2671
|
+
import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2022
2672
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2023
|
-
import
|
|
2673
|
+
import chalk9 from "chalk";
|
|
2024
2674
|
// package.json
|
|
2025
|
-
var version = "0.
|
|
2026
|
-
|
|
2027
|
-
// src/utils/filter-options.ts
|
|
2028
|
-
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
2029
|
-
import chalk7 from "chalk";
|
|
2030
|
-
var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
|
|
2031
|
-
var mergeFilterOptions = (config, cliOptions) => {
|
|
2032
|
-
const messages = [];
|
|
2033
|
-
if (config?.include) {
|
|
2034
|
-
messages.push(formatList("include filters from config", config.include));
|
|
2035
|
-
}
|
|
2036
|
-
if (config?.exclude) {
|
|
2037
|
-
messages.push(formatList("exclude filters from config", config.exclude));
|
|
2038
|
-
}
|
|
2039
|
-
if (cliOptions.include) {
|
|
2040
|
-
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
2041
|
-
}
|
|
2042
|
-
if (cliOptions.exclude) {
|
|
2043
|
-
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
2044
|
-
}
|
|
2045
|
-
const resolved = mergeFilters(config, cliOptions);
|
|
2046
|
-
if (!resolved.include && !resolved.exclude) {
|
|
2047
|
-
return { messages };
|
|
2048
|
-
}
|
|
2049
|
-
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
2050
|
-
return {
|
|
2051
|
-
include: resolved.include,
|
|
2052
|
-
exclude: resolved.exclude,
|
|
2053
|
-
source,
|
|
2054
|
-
messages
|
|
2055
|
-
};
|
|
2056
|
-
};
|
|
2675
|
+
var version = "0.17.0";
|
|
2057
2676
|
|
|
2058
2677
|
// src/commands/spec.ts
|
|
2059
2678
|
var defaultDependencies4 = {
|
|
@@ -2068,7 +2687,7 @@ function getArrayLength(value) {
|
|
|
2068
2687
|
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2069
2688
|
const location = diagnostic.location;
|
|
2070
2689
|
const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
|
|
2071
|
-
const locationText = location && relativePath ?
|
|
2690
|
+
const locationText = location && relativePath ? chalk9.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2072
2691
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2073
2692
|
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
2074
2693
|
}
|
|
@@ -2077,7 +2696,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2077
2696
|
...defaultDependencies4,
|
|
2078
2697
|
...dependencies
|
|
2079
2698
|
};
|
|
2080
|
-
program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
|
|
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) => {
|
|
2081
2700
|
try {
|
|
2082
2701
|
const steps = new StepProgress([
|
|
2083
2702
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
@@ -2099,13 +2718,14 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2099
2718
|
try {
|
|
2100
2719
|
config = await loadDocCovConfig(targetDir);
|
|
2101
2720
|
} catch (configError) {
|
|
2102
|
-
error(
|
|
2721
|
+
error(chalk9.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
2103
2722
|
process.exit(1);
|
|
2104
2723
|
}
|
|
2105
2724
|
steps.next();
|
|
2106
2725
|
const cliFilters = {
|
|
2107
2726
|
include: parseListFlag(options.include),
|
|
2108
|
-
exclude: parseListFlag(options.exclude)
|
|
2727
|
+
exclude: parseListFlag(options.exclude),
|
|
2728
|
+
visibility: parseVisibilityFlag(options.visibility)
|
|
2109
2729
|
};
|
|
2110
2730
|
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
2111
2731
|
const resolveExternalTypes = !options.skipResolve;
|
|
@@ -2125,10 +2745,11 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2125
2745
|
isMonorepo: resolved.isMonorepo,
|
|
2126
2746
|
targetPackage: packageInfo?.name
|
|
2127
2747
|
};
|
|
2128
|
-
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
|
|
2748
|
+
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
|
|
2129
2749
|
filters: {
|
|
2130
2750
|
include: resolvedFilters.include,
|
|
2131
|
-
exclude: resolvedFilters.exclude
|
|
2751
|
+
exclude: resolvedFilters.exclude,
|
|
2752
|
+
visibility: resolvedFilters.visibility
|
|
2132
2753
|
},
|
|
2133
2754
|
generationInput
|
|
2134
2755
|
} : { generationInput };
|
|
@@ -2140,74 +2761,266 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2140
2761
|
const normalized = normalize(result.spec);
|
|
2141
2762
|
const validation = validateSpec(normalized);
|
|
2142
2763
|
if (!validation.ok) {
|
|
2143
|
-
error(
|
|
2764
|
+
error(chalk9.red("Spec failed schema validation"));
|
|
2144
2765
|
for (const err of validation.errors) {
|
|
2145
|
-
error(
|
|
2766
|
+
error(chalk9.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2146
2767
|
}
|
|
2147
2768
|
process.exit(1);
|
|
2148
2769
|
}
|
|
2149
2770
|
steps.next();
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2771
|
+
const format = options.format ?? "json";
|
|
2772
|
+
const outputPath = path7.resolve(options.cwd, options.output);
|
|
2773
|
+
if (format === "api-surface") {
|
|
2774
|
+
const apiSurface = renderApiSurface(normalized);
|
|
2775
|
+
writeFileSync4(outputPath, apiSurface);
|
|
2776
|
+
steps.complete(`Generated ${options.output} (API surface)`);
|
|
2777
|
+
} else {
|
|
2778
|
+
writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
|
|
2779
|
+
steps.complete(`Generated ${options.output}`);
|
|
2780
|
+
}
|
|
2781
|
+
log(chalk9.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
2782
|
+
log(chalk9.gray(` ${getArrayLength(normalized.types)} types`));
|
|
2155
2783
|
if (options.verbose && normalized.generation) {
|
|
2156
2784
|
const gen = normalized.generation;
|
|
2157
2785
|
log("");
|
|
2158
|
-
log(
|
|
2159
|
-
log(
|
|
2160
|
-
log(
|
|
2161
|
-
log(
|
|
2162
|
-
log(
|
|
2163
|
-
log(
|
|
2164
|
-
log(
|
|
2786
|
+
log(chalk9.bold("Generation Info"));
|
|
2787
|
+
log(chalk9.gray(` Timestamp: ${gen.timestamp}`));
|
|
2788
|
+
log(chalk9.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
|
|
2789
|
+
log(chalk9.gray(` Entry point: ${gen.analysis.entryPoint}`));
|
|
2790
|
+
log(chalk9.gray(` Detected via: ${gen.analysis.entryPointSource}`));
|
|
2791
|
+
log(chalk9.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
|
|
2792
|
+
log(chalk9.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
|
|
2165
2793
|
if (gen.analysis.maxTypeDepth) {
|
|
2166
|
-
log(
|
|
2794
|
+
log(chalk9.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
|
|
2167
2795
|
}
|
|
2168
2796
|
log("");
|
|
2169
|
-
log(
|
|
2170
|
-
log(
|
|
2797
|
+
log(chalk9.bold("Environment"));
|
|
2798
|
+
log(chalk9.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
2171
2799
|
if (gen.environment.packageManager) {
|
|
2172
|
-
log(
|
|
2800
|
+
log(chalk9.gray(` Package manager: ${gen.environment.packageManager}`));
|
|
2173
2801
|
}
|
|
2174
2802
|
if (gen.environment.isMonorepo) {
|
|
2175
|
-
log(
|
|
2803
|
+
log(chalk9.gray(` Monorepo: yes`));
|
|
2176
2804
|
}
|
|
2177
2805
|
if (gen.environment.targetPackage) {
|
|
2178
|
-
log(
|
|
2806
|
+
log(chalk9.gray(` Target package: ${gen.environment.targetPackage}`));
|
|
2179
2807
|
}
|
|
2180
2808
|
if (gen.issues.length > 0) {
|
|
2181
2809
|
log("");
|
|
2182
|
-
log(
|
|
2810
|
+
log(chalk9.bold("Issues"));
|
|
2183
2811
|
for (const issue of gen.issues) {
|
|
2184
|
-
const prefix = issue.severity === "error" ?
|
|
2812
|
+
const prefix = issue.severity === "error" ? chalk9.red(">") : issue.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
|
|
2185
2813
|
log(`${prefix} [${issue.code}] ${issue.message}`);
|
|
2186
2814
|
if (issue.suggestion) {
|
|
2187
|
-
log(
|
|
2815
|
+
log(chalk9.gray(` ${issue.suggestion}`));
|
|
2188
2816
|
}
|
|
2189
2817
|
}
|
|
2190
2818
|
}
|
|
2191
2819
|
}
|
|
2192
2820
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
2193
2821
|
log("");
|
|
2194
|
-
log(
|
|
2822
|
+
log(chalk9.bold("Diagnostics"));
|
|
2195
2823
|
for (const diagnostic of result.diagnostics) {
|
|
2196
|
-
const prefix = diagnostic.severity === "error" ?
|
|
2824
|
+
const prefix = diagnostic.severity === "error" ? chalk9.red(">") : diagnostic.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
|
|
2197
2825
|
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
2198
2826
|
}
|
|
2199
2827
|
}
|
|
2200
2828
|
} catch (commandError) {
|
|
2201
|
-
error(
|
|
2829
|
+
error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2202
2830
|
process.exit(1);
|
|
2203
2831
|
}
|
|
2204
2832
|
});
|
|
2205
2833
|
}
|
|
2206
2834
|
|
|
2835
|
+
// src/commands/trends.ts
|
|
2836
|
+
import * as fs6 from "node:fs";
|
|
2837
|
+
import * as path8 from "node:path";
|
|
2838
|
+
import {
|
|
2839
|
+
formatDelta as formatDelta2,
|
|
2840
|
+
getExtendedTrend,
|
|
2841
|
+
getTrend,
|
|
2842
|
+
loadSnapshots,
|
|
2843
|
+
pruneByTier,
|
|
2844
|
+
pruneHistory,
|
|
2845
|
+
renderSparkline,
|
|
2846
|
+
RETENTION_DAYS,
|
|
2847
|
+
saveSnapshot
|
|
2848
|
+
} from "@doccov/sdk";
|
|
2849
|
+
import chalk10 from "chalk";
|
|
2850
|
+
function formatDate(timestamp) {
|
|
2851
|
+
const date = new Date(timestamp);
|
|
2852
|
+
return date.toLocaleDateString("en-US", {
|
|
2853
|
+
month: "short",
|
|
2854
|
+
day: "numeric",
|
|
2855
|
+
year: "numeric",
|
|
2856
|
+
hour: "2-digit",
|
|
2857
|
+
minute: "2-digit"
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
function getColorForScore(score) {
|
|
2861
|
+
if (score >= 90)
|
|
2862
|
+
return chalk10.green;
|
|
2863
|
+
if (score >= 70)
|
|
2864
|
+
return chalk10.yellow;
|
|
2865
|
+
if (score >= 50)
|
|
2866
|
+
return chalk10.hex("#FFA500");
|
|
2867
|
+
return chalk10.red;
|
|
2868
|
+
}
|
|
2869
|
+
function formatSnapshot(snapshot) {
|
|
2870
|
+
const color = getColorForScore(snapshot.coverageScore);
|
|
2871
|
+
const date = formatDate(snapshot.timestamp);
|
|
2872
|
+
const version2 = snapshot.version ? ` v${snapshot.version}` : "";
|
|
2873
|
+
const commit = snapshot.commit ? chalk10.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
|
|
2874
|
+
return `${chalk10.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
|
|
2875
|
+
}
|
|
2876
|
+
function formatWeekDate(timestamp) {
|
|
2877
|
+
const date = new Date(timestamp);
|
|
2878
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
2879
|
+
}
|
|
2880
|
+
function formatVelocity(velocity) {
|
|
2881
|
+
if (velocity > 0)
|
|
2882
|
+
return chalk10.green(`+${velocity}%/day`);
|
|
2883
|
+
if (velocity < 0)
|
|
2884
|
+
return chalk10.red(`${velocity}%/day`);
|
|
2885
|
+
return chalk10.gray("0%/day");
|
|
2886
|
+
}
|
|
2887
|
+
function registerTrendsCommand(program) {
|
|
2888
|
+
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) => {
|
|
2889
|
+
const cwd = path8.resolve(options.cwd);
|
|
2890
|
+
const tier = options.tier ?? "pro";
|
|
2891
|
+
if (options.prune) {
|
|
2892
|
+
const keepCount = parseInt(options.prune, 10);
|
|
2893
|
+
if (!isNaN(keepCount)) {
|
|
2894
|
+
const deleted = pruneHistory(cwd, keepCount);
|
|
2895
|
+
console.log(chalk10.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
|
|
2896
|
+
} else {
|
|
2897
|
+
const deleted = pruneByTier(cwd, tier);
|
|
2898
|
+
console.log(chalk10.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
|
|
2899
|
+
}
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (options.record) {
|
|
2903
|
+
const specPath = path8.resolve(cwd, "openpkg.json");
|
|
2904
|
+
if (!fs6.existsSync(specPath)) {
|
|
2905
|
+
console.error(chalk10.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
|
|
2906
|
+
process.exit(1);
|
|
2907
|
+
}
|
|
2908
|
+
try {
|
|
2909
|
+
const specContent = fs6.readFileSync(specPath, "utf-8");
|
|
2910
|
+
const spec = JSON.parse(specContent);
|
|
2911
|
+
const trend = getTrend(spec, cwd);
|
|
2912
|
+
saveSnapshot(trend.current, cwd);
|
|
2913
|
+
console.log(chalk10.green("Recorded coverage snapshot:"));
|
|
2914
|
+
console.log(formatSnapshot(trend.current));
|
|
2915
|
+
if (trend.delta !== undefined) {
|
|
2916
|
+
const deltaStr = formatDelta2(trend.delta);
|
|
2917
|
+
const deltaColor = trend.delta > 0 ? chalk10.green : trend.delta < 0 ? chalk10.red : chalk10.gray;
|
|
2918
|
+
console.log(chalk10.gray("Change from previous:"), deltaColor(deltaStr));
|
|
2919
|
+
}
|
|
2920
|
+
return;
|
|
2921
|
+
} catch (error) {
|
|
2922
|
+
console.error(chalk10.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
|
|
2923
|
+
process.exit(1);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
const snapshots = loadSnapshots(cwd);
|
|
2927
|
+
const limit = parseInt(options.limit ?? "10", 10);
|
|
2928
|
+
if (snapshots.length === 0) {
|
|
2929
|
+
console.log(chalk10.yellow("No coverage history found."));
|
|
2930
|
+
console.log(chalk10.gray("Run `doccov trends --record` to save the current coverage."));
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
if (options.json) {
|
|
2934
|
+
const output = {
|
|
2935
|
+
current: snapshots[0],
|
|
2936
|
+
history: snapshots.slice(1),
|
|
2937
|
+
delta: snapshots.length > 1 ? snapshots[0].coverageScore - snapshots[1].coverageScore : undefined,
|
|
2938
|
+
sparkline: snapshots.slice(0, 10).map((s) => s.coverageScore).reverse()
|
|
2939
|
+
};
|
|
2940
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
|
|
2944
|
+
const sparkline = renderSparkline(sparklineData);
|
|
2945
|
+
console.log(chalk10.bold("Coverage Trends"));
|
|
2946
|
+
console.log(chalk10.gray(`Package: ${snapshots[0].package}`));
|
|
2947
|
+
console.log(chalk10.gray(`Sparkline: ${sparkline}`));
|
|
2948
|
+
console.log("");
|
|
2949
|
+
if (snapshots.length >= 2) {
|
|
2950
|
+
const oldest = snapshots[snapshots.length - 1];
|
|
2951
|
+
const newest = snapshots[0];
|
|
2952
|
+
const overallDelta = newest.coverageScore - oldest.coverageScore;
|
|
2953
|
+
const deltaStr = formatDelta2(overallDelta);
|
|
2954
|
+
const deltaColor = overallDelta > 0 ? chalk10.green : overallDelta < 0 ? chalk10.red : chalk10.gray;
|
|
2955
|
+
console.log(chalk10.gray("Overall trend:"), deltaColor(deltaStr), chalk10.gray(`(${snapshots.length} snapshots)`));
|
|
2956
|
+
console.log("");
|
|
2957
|
+
}
|
|
2958
|
+
if (options.extended) {
|
|
2959
|
+
const specPath = path8.resolve(cwd, "openpkg.json");
|
|
2960
|
+
if (fs6.existsSync(specPath)) {
|
|
2961
|
+
try {
|
|
2962
|
+
const specContent = fs6.readFileSync(specPath, "utf-8");
|
|
2963
|
+
const spec = JSON.parse(specContent);
|
|
2964
|
+
const extended = getExtendedTrend(spec, cwd, { tier });
|
|
2965
|
+
console.log(chalk10.bold("Extended Analysis"));
|
|
2966
|
+
console.log(chalk10.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
|
|
2967
|
+
console.log("");
|
|
2968
|
+
console.log(" Velocity:");
|
|
2969
|
+
console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
|
|
2970
|
+
console.log(` 30-day: ${formatVelocity(extended.velocity30d)}`);
|
|
2971
|
+
if (extended.velocity90d !== undefined) {
|
|
2972
|
+
console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
|
|
2973
|
+
}
|
|
2974
|
+
console.log("");
|
|
2975
|
+
const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk10.green : chalk10.red;
|
|
2976
|
+
console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
|
|
2977
|
+
console.log(` All-time high: ${chalk10.green(`${extended.allTimeHigh}%`)}`);
|
|
2978
|
+
console.log(` All-time low: ${chalk10.red(`${extended.allTimeLow}%`)}`);
|
|
2979
|
+
if (extended.dataRange) {
|
|
2980
|
+
const startDate = formatWeekDate(extended.dataRange.start);
|
|
2981
|
+
const endDate = formatWeekDate(extended.dataRange.end);
|
|
2982
|
+
console.log(chalk10.gray(` Data range: ${startDate} - ${endDate}`));
|
|
2983
|
+
}
|
|
2984
|
+
console.log("");
|
|
2985
|
+
if (options.weekly && extended.weeklySummaries.length > 0) {
|
|
2986
|
+
console.log(chalk10.bold("Weekly Summary"));
|
|
2987
|
+
const weekLimit = Math.min(extended.weeklySummaries.length, 8);
|
|
2988
|
+
for (let i = 0;i < weekLimit; i++) {
|
|
2989
|
+
const week = extended.weeklySummaries[i];
|
|
2990
|
+
const weekStart = formatWeekDate(week.weekStart);
|
|
2991
|
+
const weekEnd = formatWeekDate(week.weekEnd);
|
|
2992
|
+
const deltaColor = week.delta > 0 ? chalk10.green : week.delta < 0 ? chalk10.red : chalk10.gray;
|
|
2993
|
+
const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
|
|
2994
|
+
console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
|
|
2995
|
+
}
|
|
2996
|
+
if (extended.weeklySummaries.length > weekLimit) {
|
|
2997
|
+
console.log(chalk10.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
|
|
2998
|
+
}
|
|
2999
|
+
console.log("");
|
|
3000
|
+
}
|
|
3001
|
+
} catch {
|
|
3002
|
+
console.log(chalk10.yellow("Could not load openpkg.json for extended analysis"));
|
|
3003
|
+
console.log("");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
console.log(chalk10.bold("History"));
|
|
3008
|
+
const displaySnapshots = snapshots.slice(0, limit);
|
|
3009
|
+
for (let i = 0;i < displaySnapshots.length; i++) {
|
|
3010
|
+
const snapshot = displaySnapshots[i];
|
|
3011
|
+
const prefix = i === 0 ? chalk10.cyan("→") : " ";
|
|
3012
|
+
console.log(`${prefix} ${formatSnapshot(snapshot)}`);
|
|
3013
|
+
}
|
|
3014
|
+
if (snapshots.length > limit) {
|
|
3015
|
+
console.log(chalk10.gray(` ... and ${snapshots.length - limit} more`));
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
2207
3020
|
// src/cli.ts
|
|
2208
3021
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2209
|
-
var __dirname2 =
|
|
2210
|
-
var packageJson = JSON.parse(
|
|
3022
|
+
var __dirname2 = path9.dirname(__filename2);
|
|
3023
|
+
var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
|
|
2211
3024
|
var program = new Command;
|
|
2212
3025
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2213
3026
|
registerCheckCommand(program);
|
|
@@ -2215,6 +3028,7 @@ registerInfoCommand(program);
|
|
|
2215
3028
|
registerSpecCommand(program);
|
|
2216
3029
|
registerDiffCommand(program);
|
|
2217
3030
|
registerInitCommand(program);
|
|
3031
|
+
registerTrendsCommand(program);
|
|
2218
3032
|
program.command("*", { hidden: true }).action(() => {
|
|
2219
3033
|
program.outputHelp();
|
|
2220
3034
|
});
|