@doccov/cli 0.17.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 +964 -180
- package/dist/config/index.d.ts +24 -14
- package/dist/config/index.js +11 -2
- package/package.json +3 -3
- package/dist/cli.d.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -35,13 +35,20 @@ var checkConfigSchema = z.object({
|
|
|
35
35
|
var qualityConfigSchema = z.object({
|
|
36
36
|
rules: z.record(severitySchema).optional()
|
|
37
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
|
+
});
|
|
38
44
|
var docCovConfigSchema = z.object({
|
|
39
45
|
include: stringList.optional(),
|
|
40
46
|
exclude: stringList.optional(),
|
|
41
47
|
plugins: z.array(z.unknown()).optional(),
|
|
42
48
|
docs: docsConfigSchema.optional(),
|
|
43
49
|
check: checkConfigSchema.optional(),
|
|
44
|
-
quality: qualityConfigSchema.optional()
|
|
50
|
+
quality: qualityConfigSchema.optional(),
|
|
51
|
+
policies: z.array(policyConfigSchema).optional()
|
|
45
52
|
});
|
|
46
53
|
var normalizeList = (value) => {
|
|
47
54
|
if (!value) {
|
|
@@ -79,13 +86,15 @@ var normalizeConfig = (input) => {
|
|
|
79
86
|
rules: input.quality.rules
|
|
80
87
|
};
|
|
81
88
|
}
|
|
89
|
+
const policies = input.policies;
|
|
82
90
|
return {
|
|
83
91
|
include,
|
|
84
92
|
exclude,
|
|
85
93
|
plugins: input.plugins,
|
|
86
94
|
docs,
|
|
87
95
|
check,
|
|
88
|
-
quality
|
|
96
|
+
quality,
|
|
97
|
+
policies
|
|
89
98
|
};
|
|
90
99
|
};
|
|
91
100
|
|
|
@@ -168,8 +177,8 @@ ${formatIssues(issues)}`);
|
|
|
168
177
|
// src/config/index.ts
|
|
169
178
|
var defineConfig = (config) => config;
|
|
170
179
|
// src/cli.ts
|
|
171
|
-
import { readFileSync as
|
|
172
|
-
import * as
|
|
180
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
181
|
+
import * as path9 from "node:path";
|
|
173
182
|
import { fileURLToPath } from "node:url";
|
|
174
183
|
import { Command } from "commander";
|
|
175
184
|
|
|
@@ -177,11 +186,14 @@ import { Command } from "commander";
|
|
|
177
186
|
import * as fs2 from "node:fs";
|
|
178
187
|
import * as path4 from "node:path";
|
|
179
188
|
import {
|
|
189
|
+
analyzeSpecContributors,
|
|
190
|
+
analyzeSpecOwnership,
|
|
180
191
|
applyEdits,
|
|
181
192
|
categorizeDrifts,
|
|
182
193
|
createSourceFile,
|
|
183
194
|
DocCov,
|
|
184
195
|
enrichSpec,
|
|
196
|
+
evaluatePolicies,
|
|
185
197
|
findJSDocLocation,
|
|
186
198
|
generateFixesForExport,
|
|
187
199
|
generateReport,
|
|
@@ -196,8 +208,75 @@ import {
|
|
|
196
208
|
import {
|
|
197
209
|
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
198
210
|
} from "@openpkg-ts/spec";
|
|
199
|
-
import
|
|
211
|
+
import chalk5 from "chalk";
|
|
200
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
|
+
}
|
|
201
280
|
// src/reports/diff-markdown.ts
|
|
202
281
|
import * as path2 from "node:path";
|
|
203
282
|
function bar(pct, width = 10) {
|
|
@@ -899,19 +978,336 @@ function writeReports(options) {
|
|
|
899
978
|
}));
|
|
900
979
|
return results;
|
|
901
980
|
}
|
|
902
|
-
// src/utils/
|
|
981
|
+
// src/utils/ai-client.ts
|
|
982
|
+
import chalk2 from "chalk";
|
|
983
|
+
|
|
984
|
+
// src/utils/ai-generate.ts
|
|
903
985
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
904
986
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
905
987
|
import { generateObject } from "ai";
|
|
906
988
|
import { z as z2 } from "zod";
|
|
907
|
-
var
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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")
|
|
913
1309
|
})).describe("List of assertion-like comments found in the code"),
|
|
914
|
-
hasAssertions:
|
|
1310
|
+
hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
|
|
915
1311
|
});
|
|
916
1312
|
var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
|
|
917
1313
|
|
|
@@ -938,13 +1334,13 @@ Code:
|
|
|
938
1334
|
\`\`\`
|
|
939
1335
|
${code}
|
|
940
1336
|
\`\`\``;
|
|
941
|
-
function
|
|
1337
|
+
function getModel2() {
|
|
942
1338
|
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
943
1339
|
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
944
|
-
const anthropic =
|
|
1340
|
+
const anthropic = createAnthropic2();
|
|
945
1341
|
return anthropic("claude-sonnet-4-20250514");
|
|
946
1342
|
}
|
|
947
|
-
const openai =
|
|
1343
|
+
const openai = createOpenAI2();
|
|
948
1344
|
return openai("gpt-4o-mini");
|
|
949
1345
|
}
|
|
950
1346
|
function isLLMAssertionParsingAvailable() {
|
|
@@ -955,8 +1351,8 @@ async function parseAssertionsWithLLM(code) {
|
|
|
955
1351
|
return null;
|
|
956
1352
|
}
|
|
957
1353
|
try {
|
|
958
|
-
const model =
|
|
959
|
-
const { object } = await
|
|
1354
|
+
const model = getModel2();
|
|
1355
|
+
const { object } = await generateObject2({
|
|
960
1356
|
model,
|
|
961
1357
|
schema: AssertionParseSchema,
|
|
962
1358
|
prompt: ASSERTION_PARSE_PROMPT(code)
|
|
@@ -968,7 +1364,7 @@ async function parseAssertionsWithLLM(code) {
|
|
|
968
1364
|
}
|
|
969
1365
|
|
|
970
1366
|
// src/utils/progress.ts
|
|
971
|
-
import
|
|
1367
|
+
import chalk4 from "chalk";
|
|
972
1368
|
class StepProgress {
|
|
973
1369
|
steps;
|
|
974
1370
|
currentStep = 0;
|
|
@@ -996,7 +1392,7 @@ class StepProgress {
|
|
|
996
1392
|
this.completeCurrentStep();
|
|
997
1393
|
if (message) {
|
|
998
1394
|
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
999
|
-
console.log(`${
|
|
1395
|
+
console.log(`${chalk4.green("✓")} ${message} ${chalk4.dim(`(${elapsed}s)`)}`);
|
|
1000
1396
|
}
|
|
1001
1397
|
}
|
|
1002
1398
|
render() {
|
|
@@ -1004,17 +1400,17 @@ class StepProgress {
|
|
|
1004
1400
|
if (!step)
|
|
1005
1401
|
return;
|
|
1006
1402
|
const label = step.activeLabel ?? step.label;
|
|
1007
|
-
const prefix =
|
|
1008
|
-
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)}...`);
|
|
1009
1405
|
}
|
|
1010
1406
|
completeCurrentStep() {
|
|
1011
1407
|
const step = this.steps[this.currentStep];
|
|
1012
1408
|
if (!step)
|
|
1013
1409
|
return;
|
|
1014
1410
|
const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
|
|
1015
|
-
const prefix =
|
|
1411
|
+
const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
|
|
1016
1412
|
process.stdout.write(`\r${" ".repeat(80)}\r`);
|
|
1017
|
-
console.log(`${prefix} ${step.label} ${
|
|
1413
|
+
console.log(`${prefix} ${step.label} ${chalk4.green("✓")} ${chalk4.dim(`(${elapsed}s)`)}`);
|
|
1018
1414
|
}
|
|
1019
1415
|
}
|
|
1020
1416
|
|
|
@@ -1058,7 +1454,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1058
1454
|
...defaultDependencies,
|
|
1059
1455
|
...dependencies
|
|
1060
1456
|
};
|
|
1061
|
-
program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--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) => {
|
|
1062
1458
|
try {
|
|
1063
1459
|
const validations = parseExamplesFlag(options.examples);
|
|
1064
1460
|
const hasExamples = validations.length > 0;
|
|
@@ -1085,6 +1481,15 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1085
1481
|
const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
|
|
1086
1482
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
1087
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
|
+
}
|
|
1088
1493
|
steps.next();
|
|
1089
1494
|
const resolveExternalTypes = !options.skipResolve;
|
|
1090
1495
|
let specResult;
|
|
@@ -1094,7 +1499,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1094
1499
|
useCache: options.cache !== false,
|
|
1095
1500
|
cwd: options.cwd
|
|
1096
1501
|
});
|
|
1097
|
-
|
|
1502
|
+
const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
|
|
1503
|
+
specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1098
1504
|
if (!specResult) {
|
|
1099
1505
|
throw new Error("Failed to analyze documentation coverage.");
|
|
1100
1506
|
}
|
|
@@ -1111,6 +1517,24 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1111
1517
|
violations.push({ exportName: exp.name, violation: v });
|
|
1112
1518
|
}
|
|
1113
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
|
+
}
|
|
1114
1538
|
let exampleResult;
|
|
1115
1539
|
const typecheckErrors = [];
|
|
1116
1540
|
const runtimeDrifts = [];
|
|
@@ -1156,12 +1580,12 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1156
1580
|
if (allDrifts.length > 0) {
|
|
1157
1581
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1158
1582
|
if (fixable.length === 0) {
|
|
1159
|
-
log(
|
|
1583
|
+
log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1160
1584
|
} else {
|
|
1161
1585
|
log("");
|
|
1162
|
-
log(
|
|
1586
|
+
log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
1163
1587
|
if (nonFixable.length > 0) {
|
|
1164
|
-
log(
|
|
1588
|
+
log(chalk5.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
1165
1589
|
}
|
|
1166
1590
|
log("");
|
|
1167
1591
|
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
@@ -1169,22 +1593,22 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1169
1593
|
const editsByFile = new Map;
|
|
1170
1594
|
for (const [exp, drifts] of groupedDrifts) {
|
|
1171
1595
|
if (!exp.source?.file) {
|
|
1172
|
-
log(
|
|
1596
|
+
log(chalk5.gray(` Skipping ${exp.name}: no source location`));
|
|
1173
1597
|
continue;
|
|
1174
1598
|
}
|
|
1175
1599
|
if (exp.source.file.endsWith(".d.ts")) {
|
|
1176
|
-
log(
|
|
1600
|
+
log(chalk5.gray(` Skipping ${exp.name}: declaration file`));
|
|
1177
1601
|
continue;
|
|
1178
1602
|
}
|
|
1179
1603
|
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
1180
1604
|
if (!fs2.existsSync(filePath)) {
|
|
1181
|
-
log(
|
|
1605
|
+
log(chalk5.gray(` Skipping ${exp.name}: file not found`));
|
|
1182
1606
|
continue;
|
|
1183
1607
|
}
|
|
1184
1608
|
const sourceFile = createSourceFile(filePath);
|
|
1185
1609
|
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
1186
1610
|
if (!location) {
|
|
1187
|
-
log(
|
|
1611
|
+
log(chalk5.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
1188
1612
|
continue;
|
|
1189
1613
|
}
|
|
1190
1614
|
let existingPatch = {};
|
|
@@ -1217,26 +1641,26 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1217
1641
|
}
|
|
1218
1642
|
if (edits.length > 0) {
|
|
1219
1643
|
if (options.dryRun) {
|
|
1220
|
-
log(
|
|
1644
|
+
log(chalk5.bold("Dry run - changes that would be made:"));
|
|
1221
1645
|
log("");
|
|
1222
1646
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
1223
1647
|
const relativePath = path4.relative(targetDir, filePath);
|
|
1224
|
-
log(
|
|
1648
|
+
log(chalk5.cyan(` ${relativePath}:`));
|
|
1225
1649
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
1226
1650
|
const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
|
|
1227
|
-
log(` ${
|
|
1651
|
+
log(` ${chalk5.bold(exp.name)} [${lineInfo}]`);
|
|
1228
1652
|
for (const fix of fixes) {
|
|
1229
|
-
log(
|
|
1653
|
+
log(chalk5.green(` + ${fix.description}`));
|
|
1230
1654
|
}
|
|
1231
1655
|
}
|
|
1232
1656
|
log("");
|
|
1233
1657
|
}
|
|
1234
|
-
log(
|
|
1658
|
+
log(chalk5.gray("Run without --dry-run to apply these changes."));
|
|
1235
1659
|
} else {
|
|
1236
1660
|
const applyResult = await applyEdits(edits);
|
|
1237
1661
|
if (applyResult.errors.length > 0) {
|
|
1238
1662
|
for (const err of applyResult.errors) {
|
|
1239
|
-
error(
|
|
1663
|
+
error(chalk5.red(` ${err.file}: ${err.error}`));
|
|
1240
1664
|
}
|
|
1241
1665
|
}
|
|
1242
1666
|
}
|
|
@@ -1247,6 +1671,99 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1247
1671
|
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
1248
1672
|
}
|
|
1249
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
|
+
}
|
|
1250
1767
|
steps.complete("Check complete");
|
|
1251
1768
|
if (format !== "text") {
|
|
1252
1769
|
const limit = parseInt(options.limit, 10) || 20;
|
|
@@ -1292,7 +1809,8 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1292
1809
|
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
1293
1810
|
const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1294
1811
|
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
1295
|
-
|
|
1812
|
+
const policiesFailed2 = policyResult && !policyResult.allPassed;
|
|
1813
|
+
if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
|
|
1296
1814
|
process.exit(1);
|
|
1297
1815
|
}
|
|
1298
1816
|
return;
|
|
@@ -1304,18 +1822,19 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1304
1822
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
1305
1823
|
const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
1306
1824
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
1825
|
+
const policiesFailed = policyResult && !policyResult.allPassed;
|
|
1307
1826
|
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
1308
1827
|
log("");
|
|
1309
1828
|
for (const diag of specWarnings) {
|
|
1310
|
-
log(
|
|
1829
|
+
log(chalk5.yellow(`⚠ ${diag.message}`));
|
|
1311
1830
|
if (diag.suggestion) {
|
|
1312
|
-
log(
|
|
1831
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1313
1832
|
}
|
|
1314
1833
|
}
|
|
1315
1834
|
for (const diag of specInfos) {
|
|
1316
|
-
log(
|
|
1835
|
+
log(chalk5.cyan(`ℹ ${diag.message}`));
|
|
1317
1836
|
if (diag.suggestion) {
|
|
1318
|
-
log(
|
|
1837
|
+
log(chalk5.gray(` ${diag.suggestion}`));
|
|
1319
1838
|
}
|
|
1320
1839
|
}
|
|
1321
1840
|
}
|
|
@@ -1325,23 +1844,23 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1325
1844
|
const errorCount = violations.filter((v) => v.violation.severity === "error").length;
|
|
1326
1845
|
const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
|
|
1327
1846
|
log("");
|
|
1328
|
-
log(
|
|
1847
|
+
log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
1329
1848
|
log("");
|
|
1330
1849
|
log(` Exports: ${totalExports}`);
|
|
1331
1850
|
if (minCoverage !== undefined) {
|
|
1332
1851
|
if (coverageFailed) {
|
|
1333
|
-
log(
|
|
1852
|
+
log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1334
1853
|
} else {
|
|
1335
|
-
log(
|
|
1854
|
+
log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
|
|
1336
1855
|
}
|
|
1337
1856
|
} else {
|
|
1338
1857
|
log(` Coverage: ${coverageScore}%`);
|
|
1339
1858
|
}
|
|
1340
1859
|
if (maxDrift !== undefined) {
|
|
1341
1860
|
if (driftFailed) {
|
|
1342
|
-
log(
|
|
1861
|
+
log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1343
1862
|
} else {
|
|
1344
|
-
log(
|
|
1863
|
+
log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
|
|
1345
1864
|
}
|
|
1346
1865
|
} else {
|
|
1347
1866
|
log(` Drift: ${driftScore}%`);
|
|
@@ -1351,7 +1870,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1351
1870
|
if (typecheckCount > 0) {
|
|
1352
1871
|
log(` Examples: ${typecheckCount} type errors`);
|
|
1353
1872
|
} else {
|
|
1354
|
-
log(
|
|
1873
|
+
log(chalk5.green(` Examples: ✓ validated`));
|
|
1355
1874
|
}
|
|
1356
1875
|
}
|
|
1357
1876
|
if (errorCount > 0 || warnCount > 0) {
|
|
@@ -1362,8 +1881,57 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1362
1881
|
parts.push(`${warnCount} warnings`);
|
|
1363
1882
|
log(` Quality: ${parts.join(", ")}`);
|
|
1364
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
|
+
}
|
|
1365
1933
|
log("");
|
|
1366
|
-
const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
|
|
1934
|
+
const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors || policiesFailed;
|
|
1367
1935
|
if (!failed) {
|
|
1368
1936
|
const thresholdParts = [];
|
|
1369
1937
|
if (minCoverage !== undefined) {
|
|
@@ -1372,25 +1940,31 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
1372
1940
|
if (maxDrift !== undefined) {
|
|
1373
1941
|
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
1374
1942
|
}
|
|
1943
|
+
if (policyResult) {
|
|
1944
|
+
thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
|
|
1945
|
+
}
|
|
1375
1946
|
if (thresholdParts.length > 0) {
|
|
1376
|
-
log(
|
|
1947
|
+
log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
1377
1948
|
} else {
|
|
1378
|
-
log(
|
|
1379
|
-
log(
|
|
1949
|
+
log(chalk5.green("✓ Check passed"));
|
|
1950
|
+
log(chalk5.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
|
|
1380
1951
|
}
|
|
1381
1952
|
return;
|
|
1382
1953
|
}
|
|
1383
1954
|
if (hasQualityErrors) {
|
|
1384
|
-
log(
|
|
1955
|
+
log(chalk5.red(`✗ ${errorCount} quality errors`));
|
|
1385
1956
|
}
|
|
1386
1957
|
if (hasTypecheckErrors) {
|
|
1387
|
-
log(
|
|
1958
|
+
log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
1959
|
+
}
|
|
1960
|
+
if (policiesFailed && policyResult) {
|
|
1961
|
+
log(chalk5.red(`✗ ${policyResult.failedCount} policy failures`));
|
|
1388
1962
|
}
|
|
1389
1963
|
log("");
|
|
1390
|
-
log(
|
|
1964
|
+
log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
|
|
1391
1965
|
process.exit(1);
|
|
1392
1966
|
} catch (commandError) {
|
|
1393
|
-
error(
|
|
1967
|
+
error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1394
1968
|
process.exit(1);
|
|
1395
1969
|
}
|
|
1396
1970
|
});
|
|
@@ -1427,35 +2001,36 @@ import {
|
|
|
1427
2001
|
hashString,
|
|
1428
2002
|
parseMarkdownFiles
|
|
1429
2003
|
} from "@doccov/sdk";
|
|
1430
|
-
import
|
|
2004
|
+
import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
|
|
2005
|
+
import chalk6 from "chalk";
|
|
1431
2006
|
import { glob } from "glob";
|
|
1432
2007
|
|
|
1433
2008
|
// src/utils/docs-impact-ai.ts
|
|
1434
|
-
import { createAnthropic as
|
|
1435
|
-
import { createOpenAI as
|
|
1436
|
-
import { generateObject as
|
|
1437
|
-
import { z as
|
|
1438
|
-
var CodeBlockUsageSchema =
|
|
1439
|
-
isImpacted:
|
|
1440
|
-
reason:
|
|
1441
|
-
usageType:
|
|
1442
|
-
suggestedFix:
|
|
1443
|
-
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")
|
|
1444
2019
|
});
|
|
1445
|
-
var MultiBlockAnalysisSchema =
|
|
1446
|
-
groups:
|
|
1447
|
-
blockIndices:
|
|
1448
|
-
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")
|
|
1449
2024
|
})).describe("Groups of related code blocks"),
|
|
1450
|
-
skippedBlocks:
|
|
2025
|
+
skippedBlocks: z4.array(z4.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
|
|
1451
2026
|
});
|
|
1452
|
-
function
|
|
2027
|
+
function getModel3() {
|
|
1453
2028
|
const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
|
|
1454
2029
|
if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
|
|
1455
|
-
const anthropic =
|
|
2030
|
+
const anthropic = createAnthropic3();
|
|
1456
2031
|
return anthropic("claude-sonnet-4-20250514");
|
|
1457
2032
|
}
|
|
1458
|
-
const openai =
|
|
2033
|
+
const openai = createOpenAI3();
|
|
1459
2034
|
return openai("gpt-4o-mini");
|
|
1460
2035
|
}
|
|
1461
2036
|
function isAIDocsAnalysisAvailable() {
|
|
@@ -1470,7 +2045,7 @@ async function generateImpactSummary(impacts) {
|
|
|
1470
2045
|
}
|
|
1471
2046
|
try {
|
|
1472
2047
|
const { text } = await generateText({
|
|
1473
|
-
model:
|
|
2048
|
+
model: getModel3(),
|
|
1474
2049
|
prompt: `Summarize these documentation impacts for a GitHub PR comment.
|
|
1475
2050
|
|
|
1476
2051
|
Impacts:
|
|
@@ -1515,7 +2090,7 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1515
2090
|
...defaultDependencies2,
|
|
1516
2091
|
...dependencies
|
|
1517
2092
|
};
|
|
1518
|
-
program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", 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) => {
|
|
1519
2094
|
try {
|
|
1520
2095
|
const baseFile = options.base ?? baseArg;
|
|
1521
2096
|
const headFile = options.head ?? headArg;
|
|
@@ -1546,6 +2121,29 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1546
2121
|
}
|
|
1547
2122
|
const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
|
|
1548
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
|
+
}
|
|
1549
2147
|
const format = options.format ?? "text";
|
|
1550
2148
|
const limit = parseInt(options.limit, 10) || 10;
|
|
1551
2149
|
const checks = getStrictChecks(options.strict);
|
|
@@ -1573,8 +2171,8 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1573
2171
|
silent: true
|
|
1574
2172
|
});
|
|
1575
2173
|
}
|
|
1576
|
-
const cacheNote = fromCache ?
|
|
1577
|
-
log(
|
|
2174
|
+
const cacheNote = fromCache ? chalk6.cyan(" (cached)") : "";
|
|
2175
|
+
log(chalk6.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
1578
2176
|
}
|
|
1579
2177
|
break;
|
|
1580
2178
|
case "json": {
|
|
@@ -1635,6 +2233,28 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1635
2233
|
log(content);
|
|
1636
2234
|
break;
|
|
1637
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
|
+
}
|
|
1638
2258
|
}
|
|
1639
2259
|
const failures = validateDiff(diff, headSpec, {
|
|
1640
2260
|
minCoverage,
|
|
@@ -1642,18 +2262,18 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
1642
2262
|
checks
|
|
1643
2263
|
});
|
|
1644
2264
|
if (failures.length > 0) {
|
|
1645
|
-
log(
|
|
2265
|
+
log(chalk6.red(`
|
|
1646
2266
|
✗ Check failed`));
|
|
1647
2267
|
for (const f of failures) {
|
|
1648
|
-
log(
|
|
2268
|
+
log(chalk6.red(` - ${f}`));
|
|
1649
2269
|
}
|
|
1650
2270
|
process.exitCode = 1;
|
|
1651
2271
|
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
1652
|
-
log(
|
|
2272
|
+
log(chalk6.green(`
|
|
1653
2273
|
✓ All checks passed`));
|
|
1654
2274
|
}
|
|
1655
2275
|
} catch (commandError) {
|
|
1656
|
-
error(
|
|
2276
|
+
error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
1657
2277
|
process.exitCode = 1;
|
|
1658
2278
|
}
|
|
1659
2279
|
});
|
|
@@ -1680,7 +2300,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
|
1680
2300
|
if (!docsPatterns || docsPatterns.length === 0) {
|
|
1681
2301
|
if (config?.docs?.include) {
|
|
1682
2302
|
docsPatterns = config.docs.include;
|
|
1683
|
-
log(
|
|
2303
|
+
log(chalk6.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
1684
2304
|
}
|
|
1685
2305
|
}
|
|
1686
2306
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
@@ -1703,46 +2323,46 @@ function loadSpec(filePath, readFileSync2) {
|
|
|
1703
2323
|
}
|
|
1704
2324
|
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1705
2325
|
log("");
|
|
1706
|
-
const cacheIndicator = fromCache ?
|
|
1707
|
-
log(
|
|
2326
|
+
const cacheIndicator = fromCache ? chalk6.cyan(" (cached)") : "";
|
|
2327
|
+
log(chalk6.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
1708
2328
|
log("─".repeat(40));
|
|
1709
2329
|
log("");
|
|
1710
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
2330
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk6.green : diff.coverageDelta < 0 ? chalk6.red : chalk6.gray;
|
|
1711
2331
|
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1712
2332
|
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
1713
2333
|
const breakingCount = diff.breaking.length;
|
|
1714
2334
|
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
1715
2335
|
if (breakingCount > 0) {
|
|
1716
|
-
const severityNote = highSeverity > 0 ?
|
|
1717
|
-
log(` Breaking: ${
|
|
2336
|
+
const severityNote = highSeverity > 0 ? chalk6.red(` (${highSeverity} high severity)`) : "";
|
|
2337
|
+
log(` Breaking: ${chalk6.red(breakingCount)} changes${severityNote}`);
|
|
1718
2338
|
} else {
|
|
1719
|
-
log(` Breaking: ${
|
|
2339
|
+
log(` Breaking: ${chalk6.green("0")} changes`);
|
|
1720
2340
|
}
|
|
1721
2341
|
const newCount = diff.nonBreaking.length;
|
|
1722
2342
|
const undocCount = diff.newUndocumented.length;
|
|
1723
2343
|
if (newCount > 0) {
|
|
1724
|
-
const undocNote = undocCount > 0 ?
|
|
1725
|
-
log(` New: ${
|
|
2344
|
+
const undocNote = undocCount > 0 ? chalk6.yellow(` (${undocCount} undocumented)`) : "";
|
|
2345
|
+
log(` New: ${chalk6.green(newCount)} exports${undocNote}`);
|
|
1726
2346
|
}
|
|
1727
2347
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1728
2348
|
const parts = [];
|
|
1729
2349
|
if (diff.driftIntroduced > 0)
|
|
1730
|
-
parts.push(
|
|
2350
|
+
parts.push(chalk6.red(`+${diff.driftIntroduced}`));
|
|
1731
2351
|
if (diff.driftResolved > 0)
|
|
1732
|
-
parts.push(
|
|
2352
|
+
parts.push(chalk6.green(`-${diff.driftResolved}`));
|
|
1733
2353
|
log(` Drift: ${parts.join(", ")}`);
|
|
1734
2354
|
}
|
|
1735
2355
|
log("");
|
|
1736
2356
|
}
|
|
1737
2357
|
async function printAISummary(diff, log) {
|
|
1738
2358
|
if (!isAIDocsAnalysisAvailable()) {
|
|
1739
|
-
log(
|
|
2359
|
+
log(chalk6.yellow(`
|
|
1740
2360
|
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
1741
2361
|
return;
|
|
1742
2362
|
}
|
|
1743
2363
|
if (!diff.docsImpact)
|
|
1744
2364
|
return;
|
|
1745
|
-
log(
|
|
2365
|
+
log(chalk6.gray(`
|
|
1746
2366
|
Generating AI summary...`));
|
|
1747
2367
|
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
1748
2368
|
file: f.file,
|
|
@@ -1753,8 +2373,8 @@ Generating AI summary...`));
|
|
|
1753
2373
|
const summary = await generateImpactSummary(impacts);
|
|
1754
2374
|
if (summary) {
|
|
1755
2375
|
log("");
|
|
1756
|
-
log(
|
|
1757
|
-
log(
|
|
2376
|
+
log(chalk6.bold("AI Summary"));
|
|
2377
|
+
log(chalk6.cyan(` ${summary}`));
|
|
1758
2378
|
}
|
|
1759
2379
|
}
|
|
1760
2380
|
function validateDiff(diff, headSpec, options) {
|
|
@@ -1835,7 +2455,7 @@ function printGitHubAnnotations(diff, log) {
|
|
|
1835
2455
|
|
|
1836
2456
|
// src/commands/info.ts
|
|
1837
2457
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
1838
|
-
import
|
|
2458
|
+
import chalk7 from "chalk";
|
|
1839
2459
|
function registerInfoCommand(program) {
|
|
1840
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) => {
|
|
1841
2461
|
try {
|
|
@@ -1857,14 +2477,14 @@ function registerInfoCommand(program) {
|
|
|
1857
2477
|
const spec = enrichSpec2(specResult.spec);
|
|
1858
2478
|
const stats = computeStats(spec);
|
|
1859
2479
|
console.log("");
|
|
1860
|
-
console.log(
|
|
2480
|
+
console.log(chalk7.bold(`${stats.packageName}@${stats.version}`));
|
|
1861
2481
|
console.log("");
|
|
1862
|
-
console.log(` Exports: ${
|
|
1863
|
-
console.log(` Coverage: ${
|
|
1864
|
-
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}%`)}`);
|
|
1865
2485
|
console.log("");
|
|
1866
2486
|
} catch (err) {
|
|
1867
|
-
console.error(
|
|
2487
|
+
console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
|
|
1868
2488
|
process.exit(1);
|
|
1869
2489
|
}
|
|
1870
2490
|
});
|
|
@@ -1873,7 +2493,7 @@ function registerInfoCommand(program) {
|
|
|
1873
2493
|
// src/commands/init.ts
|
|
1874
2494
|
import * as fs4 from "node:fs";
|
|
1875
2495
|
import * as path6 from "node:path";
|
|
1876
|
-
import
|
|
2496
|
+
import chalk8 from "chalk";
|
|
1877
2497
|
var defaultDependencies3 = {
|
|
1878
2498
|
fileExists: fs4.existsSync,
|
|
1879
2499
|
writeFileSync: fs4.writeFileSync,
|
|
@@ -1890,31 +2510,31 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1890
2510
|
const cwd = path6.resolve(options.cwd);
|
|
1891
2511
|
const formatOption = String(options.format ?? "auto").toLowerCase();
|
|
1892
2512
|
if (!isValidFormat(formatOption)) {
|
|
1893
|
-
error(
|
|
2513
|
+
error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
|
|
1894
2514
|
process.exitCode = 1;
|
|
1895
2515
|
return;
|
|
1896
2516
|
}
|
|
1897
2517
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
1898
2518
|
if (existing) {
|
|
1899
|
-
error(
|
|
2519
|
+
error(chalk8.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
1900
2520
|
process.exitCode = 1;
|
|
1901
2521
|
return;
|
|
1902
2522
|
}
|
|
1903
2523
|
const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
|
|
1904
2524
|
const targetFormat = resolveFormat(formatOption, packageType);
|
|
1905
2525
|
if (targetFormat === "js" && packageType !== "module") {
|
|
1906
|
-
log(
|
|
2526
|
+
log(chalk8.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
|
|
1907
2527
|
}
|
|
1908
2528
|
const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
|
|
1909
2529
|
const outputPath = path6.join(cwd, fileName);
|
|
1910
2530
|
if (fileExists2(outputPath)) {
|
|
1911
|
-
error(
|
|
2531
|
+
error(chalk8.red(`Cannot create ${fileName}; file already exists.`));
|
|
1912
2532
|
process.exitCode = 1;
|
|
1913
2533
|
return;
|
|
1914
2534
|
}
|
|
1915
2535
|
const template = buildTemplate(targetFormat);
|
|
1916
2536
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
1917
|
-
log(
|
|
2537
|
+
log(chalk8.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
|
|
1918
2538
|
});
|
|
1919
2539
|
}
|
|
1920
2540
|
var isValidFormat = (value) => {
|
|
@@ -2048,42 +2668,11 @@ quality:
|
|
|
2048
2668
|
// src/commands/spec.ts
|
|
2049
2669
|
import * as fs5 from "node:fs";
|
|
2050
2670
|
import * as path7 from "node:path";
|
|
2051
|
-
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";
|
|
2052
2672
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2053
|
-
import
|
|
2673
|
+
import chalk9 from "chalk";
|
|
2054
2674
|
// package.json
|
|
2055
|
-
var version = "0.
|
|
2056
|
-
|
|
2057
|
-
// src/utils/filter-options.ts
|
|
2058
|
-
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
2059
|
-
import chalk7 from "chalk";
|
|
2060
|
-
var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
|
|
2061
|
-
var mergeFilterOptions = (config, cliOptions) => {
|
|
2062
|
-
const messages = [];
|
|
2063
|
-
if (config?.include) {
|
|
2064
|
-
messages.push(formatList("include filters from config", config.include));
|
|
2065
|
-
}
|
|
2066
|
-
if (config?.exclude) {
|
|
2067
|
-
messages.push(formatList("exclude filters from config", config.exclude));
|
|
2068
|
-
}
|
|
2069
|
-
if (cliOptions.include) {
|
|
2070
|
-
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
2071
|
-
}
|
|
2072
|
-
if (cliOptions.exclude) {
|
|
2073
|
-
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
2074
|
-
}
|
|
2075
|
-
const resolved = mergeFilters(config, cliOptions);
|
|
2076
|
-
if (!resolved.include && !resolved.exclude) {
|
|
2077
|
-
return { messages };
|
|
2078
|
-
}
|
|
2079
|
-
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
2080
|
-
return {
|
|
2081
|
-
include: resolved.include,
|
|
2082
|
-
exclude: resolved.exclude,
|
|
2083
|
-
source,
|
|
2084
|
-
messages
|
|
2085
|
-
};
|
|
2086
|
-
};
|
|
2675
|
+
var version = "0.17.0";
|
|
2087
2676
|
|
|
2088
2677
|
// src/commands/spec.ts
|
|
2089
2678
|
var defaultDependencies4 = {
|
|
@@ -2098,7 +2687,7 @@ function getArrayLength(value) {
|
|
|
2098
2687
|
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2099
2688
|
const location = diagnostic.location;
|
|
2100
2689
|
const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
|
|
2101
|
-
const locationText = location && relativePath ?
|
|
2690
|
+
const locationText = location && relativePath ? chalk9.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2102
2691
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2103
2692
|
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
2104
2693
|
}
|
|
@@ -2107,7 +2696,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2107
2696
|
...defaultDependencies4,
|
|
2108
2697
|
...dependencies
|
|
2109
2698
|
};
|
|
2110
|
-
program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
|
|
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) => {
|
|
2111
2700
|
try {
|
|
2112
2701
|
const steps = new StepProgress([
|
|
2113
2702
|
{ label: "Resolved target", activeLabel: "Resolving target" },
|
|
@@ -2129,13 +2718,14 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2129
2718
|
try {
|
|
2130
2719
|
config = await loadDocCovConfig(targetDir);
|
|
2131
2720
|
} catch (configError) {
|
|
2132
|
-
error(
|
|
2721
|
+
error(chalk9.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
2133
2722
|
process.exit(1);
|
|
2134
2723
|
}
|
|
2135
2724
|
steps.next();
|
|
2136
2725
|
const cliFilters = {
|
|
2137
2726
|
include: parseListFlag(options.include),
|
|
2138
|
-
exclude: parseListFlag(options.exclude)
|
|
2727
|
+
exclude: parseListFlag(options.exclude),
|
|
2728
|
+
visibility: parseVisibilityFlag(options.visibility)
|
|
2139
2729
|
};
|
|
2140
2730
|
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
2141
2731
|
const resolveExternalTypes = !options.skipResolve;
|
|
@@ -2155,10 +2745,11 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2155
2745
|
isMonorepo: resolved.isMonorepo,
|
|
2156
2746
|
targetPackage: packageInfo?.name
|
|
2157
2747
|
};
|
|
2158
|
-
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
|
|
2748
|
+
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
|
|
2159
2749
|
filters: {
|
|
2160
2750
|
include: resolvedFilters.include,
|
|
2161
|
-
exclude: resolvedFilters.exclude
|
|
2751
|
+
exclude: resolvedFilters.exclude,
|
|
2752
|
+
visibility: resolvedFilters.visibility
|
|
2162
2753
|
},
|
|
2163
2754
|
generationInput
|
|
2164
2755
|
} : { generationInput };
|
|
@@ -2170,74 +2761,266 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2170
2761
|
const normalized = normalize(result.spec);
|
|
2171
2762
|
const validation = validateSpec(normalized);
|
|
2172
2763
|
if (!validation.ok) {
|
|
2173
|
-
error(
|
|
2764
|
+
error(chalk9.red("Spec failed schema validation"));
|
|
2174
2765
|
for (const err of validation.errors) {
|
|
2175
|
-
error(
|
|
2766
|
+
error(chalk9.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2176
2767
|
}
|
|
2177
2768
|
process.exit(1);
|
|
2178
2769
|
}
|
|
2179
2770
|
steps.next();
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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`));
|
|
2185
2783
|
if (options.verbose && normalized.generation) {
|
|
2186
2784
|
const gen = normalized.generation;
|
|
2187
2785
|
log("");
|
|
2188
|
-
log(
|
|
2189
|
-
log(
|
|
2190
|
-
log(
|
|
2191
|
-
log(
|
|
2192
|
-
log(
|
|
2193
|
-
log(
|
|
2194
|
-
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"}`));
|
|
2195
2793
|
if (gen.analysis.maxTypeDepth) {
|
|
2196
|
-
log(
|
|
2794
|
+
log(chalk9.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
|
|
2197
2795
|
}
|
|
2198
2796
|
log("");
|
|
2199
|
-
log(
|
|
2200
|
-
log(
|
|
2797
|
+
log(chalk9.bold("Environment"));
|
|
2798
|
+
log(chalk9.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
|
|
2201
2799
|
if (gen.environment.packageManager) {
|
|
2202
|
-
log(
|
|
2800
|
+
log(chalk9.gray(` Package manager: ${gen.environment.packageManager}`));
|
|
2203
2801
|
}
|
|
2204
2802
|
if (gen.environment.isMonorepo) {
|
|
2205
|
-
log(
|
|
2803
|
+
log(chalk9.gray(` Monorepo: yes`));
|
|
2206
2804
|
}
|
|
2207
2805
|
if (gen.environment.targetPackage) {
|
|
2208
|
-
log(
|
|
2806
|
+
log(chalk9.gray(` Target package: ${gen.environment.targetPackage}`));
|
|
2209
2807
|
}
|
|
2210
2808
|
if (gen.issues.length > 0) {
|
|
2211
2809
|
log("");
|
|
2212
|
-
log(
|
|
2810
|
+
log(chalk9.bold("Issues"));
|
|
2213
2811
|
for (const issue of gen.issues) {
|
|
2214
|
-
const prefix = issue.severity === "error" ?
|
|
2812
|
+
const prefix = issue.severity === "error" ? chalk9.red(">") : issue.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
|
|
2215
2813
|
log(`${prefix} [${issue.code}] ${issue.message}`);
|
|
2216
2814
|
if (issue.suggestion) {
|
|
2217
|
-
log(
|
|
2815
|
+
log(chalk9.gray(` ${issue.suggestion}`));
|
|
2218
2816
|
}
|
|
2219
2817
|
}
|
|
2220
2818
|
}
|
|
2221
2819
|
}
|
|
2222
2820
|
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
2223
2821
|
log("");
|
|
2224
|
-
log(
|
|
2822
|
+
log(chalk9.bold("Diagnostics"));
|
|
2225
2823
|
for (const diagnostic of result.diagnostics) {
|
|
2226
|
-
const prefix = diagnostic.severity === "error" ?
|
|
2824
|
+
const prefix = diagnostic.severity === "error" ? chalk9.red(">") : diagnostic.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
|
|
2227
2825
|
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
2228
2826
|
}
|
|
2229
2827
|
}
|
|
2230
2828
|
} catch (commandError) {
|
|
2231
|
-
error(
|
|
2829
|
+
error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2232
2830
|
process.exit(1);
|
|
2233
2831
|
}
|
|
2234
2832
|
});
|
|
2235
2833
|
}
|
|
2236
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
|
+
|
|
2237
3020
|
// src/cli.ts
|
|
2238
3021
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2239
|
-
var __dirname2 =
|
|
2240
|
-
var packageJson = JSON.parse(
|
|
3022
|
+
var __dirname2 = path9.dirname(__filename2);
|
|
3023
|
+
var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
|
|
2241
3024
|
var program = new Command;
|
|
2242
3025
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2243
3026
|
registerCheckCommand(program);
|
|
@@ -2245,6 +3028,7 @@ registerInfoCommand(program);
|
|
|
2245
3028
|
registerSpecCommand(program);
|
|
2246
3029
|
registerDiffCommand(program);
|
|
2247
3030
|
registerInitCommand(program);
|
|
3031
|
+
registerTrendsCommand(program);
|
|
2248
3032
|
program.command("*", { hidden: true }).action(() => {
|
|
2249
3033
|
program.outputHelp();
|
|
2250
3034
|
});
|