@doccov/cli 0.8.0 → 0.10.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 +899 -739
- package/dist/config/index.d.ts +16 -12
- package/dist/config/index.js +25 -15
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -33,14 +33,24 @@ var docsConfigSchema = z.object({
|
|
|
33
33
|
include: stringList.optional(),
|
|
34
34
|
exclude: stringList.optional()
|
|
35
35
|
});
|
|
36
|
-
var
|
|
36
|
+
var severitySchema = z.enum(["error", "warn", "off"]);
|
|
37
|
+
var exampleModeSchema = z.enum([
|
|
38
|
+
"presence",
|
|
39
|
+
"typecheck",
|
|
40
|
+
"run"
|
|
41
|
+
]);
|
|
42
|
+
var exampleModesSchema = z.union([
|
|
43
|
+
exampleModeSchema,
|
|
44
|
+
z.array(exampleModeSchema),
|
|
45
|
+
z.string()
|
|
46
|
+
]);
|
|
37
47
|
var checkConfigSchema = z.object({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
examples: exampleModesSchema.optional(),
|
|
49
|
+
minCoverage: z.number().min(0).max(100).optional(),
|
|
50
|
+
maxDrift: z.number().min(0).max(100).optional()
|
|
41
51
|
});
|
|
42
|
-
var
|
|
43
|
-
rules: z.record(
|
|
52
|
+
var qualityConfigSchema = z.object({
|
|
53
|
+
rules: z.record(severitySchema).optional()
|
|
44
54
|
});
|
|
45
55
|
var docCovConfigSchema = z.object({
|
|
46
56
|
include: stringList.optional(),
|
|
@@ -48,7 +58,7 @@ var docCovConfigSchema = z.object({
|
|
|
48
58
|
plugins: z.array(z.unknown()).optional(),
|
|
49
59
|
docs: docsConfigSchema.optional(),
|
|
50
60
|
check: checkConfigSchema.optional(),
|
|
51
|
-
|
|
61
|
+
quality: qualityConfigSchema.optional()
|
|
52
62
|
});
|
|
53
63
|
var normalizeList = (value) => {
|
|
54
64
|
if (!value) {
|
|
@@ -75,15 +85,15 @@ var normalizeConfig = (input) => {
|
|
|
75
85
|
let check;
|
|
76
86
|
if (input.check) {
|
|
77
87
|
check = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
examples: input.check.examples,
|
|
89
|
+
minCoverage: input.check.minCoverage,
|
|
90
|
+
maxDrift: input.check.maxDrift
|
|
81
91
|
};
|
|
82
92
|
}
|
|
83
|
-
let
|
|
84
|
-
if (input.
|
|
85
|
-
|
|
86
|
-
rules: input.
|
|
93
|
+
let quality;
|
|
94
|
+
if (input.quality) {
|
|
95
|
+
quality = {
|
|
96
|
+
rules: input.quality.rules
|
|
87
97
|
};
|
|
88
98
|
}
|
|
89
99
|
return {
|
|
@@ -92,7 +102,7 @@ var normalizeConfig = (input) => {
|
|
|
92
102
|
plugins: input.plugins,
|
|
93
103
|
docs,
|
|
94
104
|
check,
|
|
95
|
-
|
|
105
|
+
quality
|
|
96
106
|
};
|
|
97
107
|
};
|
|
98
108
|
|
|
@@ -169,36 +179,330 @@ ${formatIssues(issues)}`);
|
|
|
169
179
|
var defineConfig = (config) => config;
|
|
170
180
|
// src/cli.ts
|
|
171
181
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
172
|
-
import * as
|
|
182
|
+
import * as path9 from "node:path";
|
|
173
183
|
import { fileURLToPath } from "node:url";
|
|
174
184
|
import { Command } from "commander";
|
|
175
185
|
|
|
176
186
|
// src/commands/check.ts
|
|
177
|
-
import * as
|
|
178
|
-
import * as
|
|
187
|
+
import * as fs2 from "node:fs";
|
|
188
|
+
import * as path3 from "node:path";
|
|
179
189
|
import {
|
|
180
190
|
applyEdits,
|
|
181
191
|
categorizeDrifts,
|
|
182
192
|
createSourceFile,
|
|
183
193
|
DocCov,
|
|
184
|
-
|
|
185
|
-
detectExampleRuntimeErrors,
|
|
194
|
+
enrichSpec,
|
|
186
195
|
findJSDocLocation,
|
|
187
196
|
generateFixesForExport,
|
|
188
|
-
|
|
189
|
-
hasNonAssertionComments,
|
|
190
|
-
lintExport,
|
|
197
|
+
generateReport,
|
|
191
198
|
mergeFixes,
|
|
192
199
|
NodeFileSystem,
|
|
193
|
-
|
|
200
|
+
parseExamplesFlag,
|
|
194
201
|
parseJSDocToPatch,
|
|
195
202
|
resolveTarget,
|
|
196
|
-
runExamplesWithPackage,
|
|
197
203
|
serializeJSDoc,
|
|
198
|
-
|
|
204
|
+
validateExamples
|
|
199
205
|
} from "@doccov/sdk";
|
|
200
|
-
import
|
|
206
|
+
import {
|
|
207
|
+
DRIFT_CATEGORIES as DRIFT_CATEGORIES2
|
|
208
|
+
} from "@openpkg-ts/spec";
|
|
209
|
+
import chalk2 from "chalk";
|
|
210
|
+
|
|
211
|
+
// src/reports/github.ts
|
|
212
|
+
function renderGithubSummary(stats, options = {}) {
|
|
213
|
+
const coverageScore = options.coverageScore ?? stats.coverageScore;
|
|
214
|
+
const driftCount = options.driftCount ?? stats.driftCount;
|
|
215
|
+
const qualityIssues = options.qualityIssues ?? 0;
|
|
216
|
+
let output = `## Documentation Coverage: ${coverageScore}%
|
|
217
|
+
|
|
218
|
+
`;
|
|
219
|
+
output += `| Metric | Value |
|
|
220
|
+
|--------|-------|
|
|
221
|
+
`;
|
|
222
|
+
output += `| Coverage Score | ${coverageScore}% |
|
|
223
|
+
`;
|
|
224
|
+
output += `| Total Exports | ${stats.totalExports} |
|
|
225
|
+
`;
|
|
226
|
+
output += `| Drift Issues | ${driftCount} |
|
|
227
|
+
`;
|
|
228
|
+
output += `| Quality Issues | ${qualityIssues} |
|
|
229
|
+
`;
|
|
230
|
+
const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
|
|
231
|
+
output += `
|
|
232
|
+
${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "needs improvement" : "failing"}
|
|
233
|
+
`;
|
|
234
|
+
return output;
|
|
235
|
+
}
|
|
236
|
+
// src/reports/markdown.ts
|
|
237
|
+
import { DRIFT_CATEGORY_LABELS } from "@openpkg-ts/spec";
|
|
238
|
+
function bar(pct, width = 10) {
|
|
239
|
+
const filled = Math.round(pct / 100 * width);
|
|
240
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
241
|
+
}
|
|
242
|
+
function renderMarkdown(stats, options = {}) {
|
|
243
|
+
const limit = options.limit ?? 20;
|
|
244
|
+
const lines = [];
|
|
245
|
+
lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
|
|
246
|
+
lines.push("");
|
|
247
|
+
lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push("| Metric | Value |");
|
|
250
|
+
lines.push("|--------|-------|");
|
|
251
|
+
lines.push(`| Exports | ${stats.totalExports} |`);
|
|
252
|
+
lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
|
|
253
|
+
lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
|
|
254
|
+
lines.push(`| Undocumented | ${stats.undocumented} |`);
|
|
255
|
+
lines.push(`| Drift issues | ${stats.driftCount} |`);
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push("## Coverage by Signal");
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push("| Signal | Coverage |");
|
|
260
|
+
lines.push("|--------|----------|");
|
|
261
|
+
for (const [sig, s] of Object.entries(stats.signalCoverage)) {
|
|
262
|
+
lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
|
|
263
|
+
}
|
|
264
|
+
if (stats.byKind.length > 0) {
|
|
265
|
+
lines.push("");
|
|
266
|
+
lines.push("## Coverage by Kind");
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push("| Kind | Count | Avg Score |");
|
|
269
|
+
lines.push("|------|-------|-----------|");
|
|
270
|
+
for (const k of stats.byKind) {
|
|
271
|
+
lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
|
|
275
|
+
if (lowExports.length > 0) {
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push("## Lowest Coverage Exports");
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push("| Export | Kind | Score | Missing |");
|
|
280
|
+
lines.push("|--------|------|-------|---------|");
|
|
281
|
+
for (const e of lowExports) {
|
|
282
|
+
lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
|
|
283
|
+
}
|
|
284
|
+
const totalLow = stats.exports.filter((e) => e.score < 100).length;
|
|
285
|
+
if (totalLow > limit) {
|
|
286
|
+
lines.push(`| ... | | | ${totalLow - limit} more |`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (stats.driftIssues.length > 0) {
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push("## Drift Issues");
|
|
292
|
+
lines.push("");
|
|
293
|
+
const { driftSummary } = stats;
|
|
294
|
+
const summaryParts = [];
|
|
295
|
+
if (driftSummary.byCategory.structural > 0) {
|
|
296
|
+
summaryParts.push(`${driftSummary.byCategory.structural} structural`);
|
|
297
|
+
}
|
|
298
|
+
if (driftSummary.byCategory.semantic > 0) {
|
|
299
|
+
summaryParts.push(`${driftSummary.byCategory.semantic} semantic`);
|
|
300
|
+
}
|
|
301
|
+
if (driftSummary.byCategory.example > 0) {
|
|
302
|
+
summaryParts.push(`${driftSummary.byCategory.example} example`);
|
|
303
|
+
}
|
|
304
|
+
const fixableNote = driftSummary.fixable > 0 ? ` (${driftSummary.fixable} auto-fixable)` : "";
|
|
305
|
+
lines.push(`**${driftSummary.total} issues** (${summaryParts.join(", ")})${fixableNote}`);
|
|
306
|
+
lines.push("");
|
|
307
|
+
const categories = ["structural", "semantic", "example"];
|
|
308
|
+
for (const category of categories) {
|
|
309
|
+
const issues = stats.driftByCategory[category];
|
|
310
|
+
if (issues.length === 0)
|
|
311
|
+
continue;
|
|
312
|
+
lines.push(`### ${DRIFT_CATEGORY_LABELS[category]}`);
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push("| Export | Issue |");
|
|
315
|
+
lines.push("|--------|-------|");
|
|
316
|
+
for (const d of issues.slice(0, Math.min(limit, 10))) {
|
|
317
|
+
const hint = d.suggestion ? ` → ${d.suggestion}` : "";
|
|
318
|
+
lines.push(`| \`${d.exportName}\` | ${d.issue}${hint} |`);
|
|
319
|
+
}
|
|
320
|
+
if (issues.length > 10) {
|
|
321
|
+
lines.push(`| ... | ${issues.length - 10} more ${category} issues |`);
|
|
322
|
+
}
|
|
323
|
+
lines.push("");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push("---");
|
|
328
|
+
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
329
|
+
return lines.join(`
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
201
332
|
|
|
333
|
+
// src/reports/html.ts
|
|
334
|
+
function escapeHtml(s) {
|
|
335
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
336
|
+
}
|
|
337
|
+
function renderHtml(stats, options = {}) {
|
|
338
|
+
const md = renderMarkdown(stats, options);
|
|
339
|
+
return `<!DOCTYPE html>
|
|
340
|
+
<html lang="en">
|
|
341
|
+
<head>
|
|
342
|
+
<meta charset="UTF-8">
|
|
343
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
344
|
+
<title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
|
|
345
|
+
<style>
|
|
346
|
+
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
347
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
|
|
348
|
+
h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
349
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
350
|
+
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
351
|
+
th { background: #161b22; }
|
|
352
|
+
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
353
|
+
a { color: var(--accent); }
|
|
354
|
+
</style>
|
|
355
|
+
</head>
|
|
356
|
+
<body>
|
|
357
|
+
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
|
|
358
|
+
</body>
|
|
359
|
+
</html>`;
|
|
360
|
+
}
|
|
361
|
+
// src/reports/stats.ts
|
|
362
|
+
import {
|
|
363
|
+
DRIFT_CATEGORIES
|
|
364
|
+
} from "@openpkg-ts/spec";
|
|
365
|
+
var FIXABLE_DRIFT_TYPES = new Set([
|
|
366
|
+
"param-mismatch",
|
|
367
|
+
"param-type-mismatch",
|
|
368
|
+
"optionality-mismatch",
|
|
369
|
+
"return-type-mismatch",
|
|
370
|
+
"generic-constraint-mismatch",
|
|
371
|
+
"example-assertion-failed",
|
|
372
|
+
"deprecated-mismatch",
|
|
373
|
+
"async-mismatch",
|
|
374
|
+
"property-type-drift"
|
|
375
|
+
]);
|
|
376
|
+
function computeStats(spec) {
|
|
377
|
+
const exports = spec.exports ?? [];
|
|
378
|
+
const signals = {
|
|
379
|
+
description: { covered: 0, total: 0 },
|
|
380
|
+
params: { covered: 0, total: 0 },
|
|
381
|
+
returns: { covered: 0, total: 0 },
|
|
382
|
+
examples: { covered: 0, total: 0 }
|
|
383
|
+
};
|
|
384
|
+
const kindMap = new Map;
|
|
385
|
+
const driftIssues = [];
|
|
386
|
+
const driftByCategory = {
|
|
387
|
+
structural: [],
|
|
388
|
+
semantic: [],
|
|
389
|
+
example: []
|
|
390
|
+
};
|
|
391
|
+
let fullyDocumented = 0;
|
|
392
|
+
let partiallyDocumented = 0;
|
|
393
|
+
let undocumented = 0;
|
|
394
|
+
for (const exp of exports) {
|
|
395
|
+
const score = exp.docs?.coverageScore ?? 0;
|
|
396
|
+
const missing = exp.docs?.missing ?? [];
|
|
397
|
+
for (const sig of ["description", "params", "returns", "examples"]) {
|
|
398
|
+
signals[sig].total++;
|
|
399
|
+
if (!missing.includes(sig))
|
|
400
|
+
signals[sig].covered++;
|
|
401
|
+
}
|
|
402
|
+
const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
|
|
403
|
+
kindEntry.count++;
|
|
404
|
+
kindEntry.totalScore += score;
|
|
405
|
+
kindMap.set(exp.kind, kindEntry);
|
|
406
|
+
if (score === 100)
|
|
407
|
+
fullyDocumented++;
|
|
408
|
+
else if (score > 0)
|
|
409
|
+
partiallyDocumented++;
|
|
410
|
+
else
|
|
411
|
+
undocumented++;
|
|
412
|
+
for (const d of exp.docs?.drift ?? []) {
|
|
413
|
+
const item = {
|
|
414
|
+
exportName: exp.name,
|
|
415
|
+
type: d.type,
|
|
416
|
+
issue: d.issue,
|
|
417
|
+
suggestion: d.suggestion
|
|
418
|
+
};
|
|
419
|
+
driftIssues.push(item);
|
|
420
|
+
const category = DRIFT_CATEGORIES[d.type] ?? "semantic";
|
|
421
|
+
driftByCategory[category].push(item);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
|
|
425
|
+
k,
|
|
426
|
+
{ ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
|
|
427
|
+
]));
|
|
428
|
+
const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
|
|
429
|
+
kind,
|
|
430
|
+
count,
|
|
431
|
+
avgScore: Math.round(totalScore / count)
|
|
432
|
+
})).sort((a, b) => b.count - a.count);
|
|
433
|
+
const sortedExports = exports.map((e) => ({
|
|
434
|
+
name: e.name,
|
|
435
|
+
kind: e.kind,
|
|
436
|
+
score: e.docs?.coverageScore ?? 0,
|
|
437
|
+
missing: e.docs?.missing ?? []
|
|
438
|
+
})).sort((a, b) => a.score - b.score);
|
|
439
|
+
const driftSummary = {
|
|
440
|
+
total: driftIssues.length,
|
|
441
|
+
byCategory: {
|
|
442
|
+
structural: driftByCategory.structural.length,
|
|
443
|
+
semantic: driftByCategory.semantic.length,
|
|
444
|
+
example: driftByCategory.example.length
|
|
445
|
+
},
|
|
446
|
+
fixable: driftIssues.filter((d) => FIXABLE_DRIFT_TYPES.has(d.type)).length
|
|
447
|
+
};
|
|
448
|
+
const exportsWithDrift = new Set(driftIssues.map((d) => d.exportName)).size;
|
|
449
|
+
const driftScore = exports.length === 0 ? 0 : Math.round(exportsWithDrift / exports.length * 100);
|
|
450
|
+
return {
|
|
451
|
+
packageName: spec.meta.name ?? "unknown",
|
|
452
|
+
version: spec.meta.version ?? "0.0.0",
|
|
453
|
+
coverageScore: spec.docs?.coverageScore ?? 0,
|
|
454
|
+
driftScore,
|
|
455
|
+
totalExports: exports.length,
|
|
456
|
+
fullyDocumented,
|
|
457
|
+
partiallyDocumented,
|
|
458
|
+
undocumented,
|
|
459
|
+
driftCount: driftIssues.length,
|
|
460
|
+
signalCoverage,
|
|
461
|
+
byKind,
|
|
462
|
+
exports: sortedExports,
|
|
463
|
+
driftIssues,
|
|
464
|
+
driftByCategory,
|
|
465
|
+
driftSummary
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// src/reports/writer.ts
|
|
469
|
+
import * as fs from "node:fs";
|
|
470
|
+
import * as path2 from "node:path";
|
|
471
|
+
import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
|
|
472
|
+
import chalk from "chalk";
|
|
473
|
+
function writeReport(options) {
|
|
474
|
+
const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
|
|
475
|
+
const reportPath = outputPath ? path2.resolve(cwd, outputPath) : path2.resolve(cwd, getReportPath(format));
|
|
476
|
+
const dir = path2.dirname(reportPath);
|
|
477
|
+
if (!fs.existsSync(dir)) {
|
|
478
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
fs.writeFileSync(reportPath, content);
|
|
481
|
+
const relativePath = path2.relative(cwd, reportPath);
|
|
482
|
+
if (!silent) {
|
|
483
|
+
console.log(chalk.green(`✓ Wrote ${format} report to ${relativePath}`));
|
|
484
|
+
}
|
|
485
|
+
return { path: reportPath, format, relativePath };
|
|
486
|
+
}
|
|
487
|
+
function writeReports(options) {
|
|
488
|
+
const { format, formatContent, jsonContent, outputPath, cwd = process.cwd() } = options;
|
|
489
|
+
const results = [];
|
|
490
|
+
if (format !== "json") {
|
|
491
|
+
results.push(writeReport({
|
|
492
|
+
format: "json",
|
|
493
|
+
content: jsonContent,
|
|
494
|
+
cwd,
|
|
495
|
+
silent: true
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
results.push(writeReport({
|
|
499
|
+
format,
|
|
500
|
+
content: format === "json" ? jsonContent : formatContent,
|
|
501
|
+
outputPath,
|
|
502
|
+
cwd
|
|
503
|
+
}));
|
|
504
|
+
return results;
|
|
505
|
+
}
|
|
202
506
|
// src/utils/llm-assertion-parser.ts
|
|
203
507
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
204
508
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -296,7 +600,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
296
600
|
...defaultDependencies,
|
|
297
601
|
...dependencies
|
|
298
602
|
};
|
|
299
|
-
program.command("check [entry]").description("
|
|
603
|
+
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) => {
|
|
300
604
|
try {
|
|
301
605
|
const fileSystem = new NodeFileSystem(options.cwd);
|
|
302
606
|
const resolved = await resolveTarget(fileSystem, {
|
|
@@ -306,220 +610,93 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
306
610
|
});
|
|
307
611
|
const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
|
|
308
612
|
if (packageInfo) {
|
|
309
|
-
log(
|
|
613
|
+
log(chalk2.gray(`Found package at ${packageInfo.path}`));
|
|
310
614
|
}
|
|
311
615
|
if (!entry) {
|
|
312
|
-
log(
|
|
616
|
+
log(chalk2.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
|
|
313
617
|
}
|
|
314
|
-
const
|
|
618
|
+
const config = await loadDocCovConfig(targetDir);
|
|
619
|
+
const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
|
|
620
|
+
const minCoverage = minCoverageRaw !== undefined ? clampCoverage(minCoverageRaw) : undefined;
|
|
621
|
+
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
622
|
+
const maxDrift = maxDriftRaw !== undefined ? clampCoverage(maxDriftRaw) : undefined;
|
|
315
623
|
const resolveExternalTypes = !options.skipResolve;
|
|
316
|
-
process.stdout.write(chalk.cyan(`> Analyzing documentation coverage...
|
|
317
|
-
`));
|
|
318
624
|
let specResult;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
625
|
+
const doccov = createDocCov({
|
|
626
|
+
resolveExternalTypes,
|
|
627
|
+
maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
|
|
628
|
+
useCache: options.cache !== false,
|
|
629
|
+
cwd: options.cwd
|
|
630
|
+
});
|
|
631
|
+
specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
632
|
+
if (specResult.fromCache) {
|
|
633
|
+
log(chalk2.gray("Using cached spec"));
|
|
328
634
|
}
|
|
329
635
|
if (!specResult) {
|
|
330
636
|
throw new Error("Failed to analyze documentation coverage.");
|
|
331
637
|
}
|
|
332
|
-
const spec = specResult.spec;
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
log("");
|
|
337
|
-
for (const diag of warnings) {
|
|
338
|
-
log(chalk.yellow(`⚠ ${diag.message}`));
|
|
339
|
-
if (diag.suggestion) {
|
|
340
|
-
log(chalk.gray(` ${diag.suggestion}`));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
for (const diag of infos) {
|
|
344
|
-
log(chalk.cyan(`ℹ ${diag.message}`));
|
|
345
|
-
if (diag.suggestion) {
|
|
346
|
-
log(chalk.gray(` ${diag.suggestion}`));
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
log("");
|
|
350
|
-
}
|
|
638
|
+
const spec = enrichSpec(specResult.spec);
|
|
639
|
+
const format = options.format ?? "text";
|
|
640
|
+
const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
|
|
641
|
+
const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
|
|
351
642
|
const shouldFix = options.fix || options.write;
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const lintConfig = getLintDefaultConfig();
|
|
357
|
-
for (const exp of spec.exports ?? []) {
|
|
358
|
-
const violations = lintExport(exp, undefined, lintConfig);
|
|
359
|
-
for (const violation of violations) {
|
|
360
|
-
lintViolations.push({ exportName: exp.name, violation });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (lintViolations.length === 0) {
|
|
364
|
-
process.stdout.write(chalk.green(`✓ No lint issues
|
|
365
|
-
`));
|
|
366
|
-
} else {
|
|
367
|
-
const errors = lintViolations.filter((v) => v.violation.severity === "error").length;
|
|
368
|
-
const warns = lintViolations.filter((v) => v.violation.severity === "warn").length;
|
|
369
|
-
process.stdout.write(chalk.yellow(`⚠ ${lintViolations.length} lint issue(s) (${errors} error, ${warns} warn)
|
|
370
|
-
`));
|
|
643
|
+
const violations = [];
|
|
644
|
+
for (const exp of spec.exports ?? []) {
|
|
645
|
+
for (const v of exp.docs?.violations ?? []) {
|
|
646
|
+
violations.push({ exportName: exp.name, violation: v });
|
|
371
647
|
}
|
|
372
648
|
}
|
|
649
|
+
const validations = parseExamplesFlag(options.examples);
|
|
650
|
+
let exampleResult;
|
|
373
651
|
const typecheckErrors = [];
|
|
374
|
-
if (options.typecheck !== false) {
|
|
375
|
-
const allExamplesForTypecheck = [];
|
|
376
|
-
for (const exp of spec.exports ?? []) {
|
|
377
|
-
if (exp.examples && exp.examples.length > 0) {
|
|
378
|
-
allExamplesForTypecheck.push({
|
|
379
|
-
exportName: exp.name,
|
|
380
|
-
examples: exp.examples
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (allExamplesForTypecheck.length > 0) {
|
|
385
|
-
process.stdout.write(chalk.cyan(`> Type-checking examples...
|
|
386
|
-
`));
|
|
387
|
-
for (const { exportName, examples } of allExamplesForTypecheck) {
|
|
388
|
-
const result = typecheckExamples(examples, targetDir);
|
|
389
|
-
for (const err of result.errors) {
|
|
390
|
-
typecheckErrors.push({ exportName, error: err });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (typecheckErrors.length === 0) {
|
|
394
|
-
process.stdout.write(chalk.green(`✓ All examples type-check
|
|
395
|
-
`));
|
|
396
|
-
} else {
|
|
397
|
-
process.stdout.write(chalk.red(`✗ ${typecheckErrors.length} type error(s)
|
|
398
|
-
`));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
652
|
const runtimeDrifts = [];
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
653
|
+
if (validations.length > 0) {
|
|
654
|
+
exampleResult = await validateExamples(spec.exports ?? [], {
|
|
655
|
+
validations,
|
|
656
|
+
packagePath: targetDir,
|
|
657
|
+
exportNames: (spec.exports ?? []).map((e) => e.name),
|
|
658
|
+
timeout: 5000,
|
|
659
|
+
installTimeout: 60000,
|
|
660
|
+
llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
|
|
661
|
+
const result = await parseAssertionsWithLLM(example);
|
|
662
|
+
return result;
|
|
663
|
+
} : undefined
|
|
664
|
+
});
|
|
665
|
+
if (exampleResult.typecheck) {
|
|
666
|
+
for (const err of exampleResult.typecheck.errors) {
|
|
667
|
+
typecheckErrors.push({
|
|
668
|
+
exportName: err.exportName,
|
|
669
|
+
error: err.error
|
|
670
|
+
});
|
|
408
671
|
}
|
|
409
672
|
}
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
installTimeout: 60000,
|
|
420
|
-
cwd: targetDir
|
|
421
|
-
});
|
|
422
|
-
if (!packageResult.installSuccess) {
|
|
423
|
-
process.stdout.write(chalk.red(`✗ Package install failed: ${packageResult.installError}
|
|
424
|
-
`));
|
|
425
|
-
log(chalk.yellow("Skipping example execution. Ensure the package is built."));
|
|
426
|
-
} else {
|
|
427
|
-
process.stdout.write(chalk.cyan(`> Running @example blocks...
|
|
428
|
-
`));
|
|
429
|
-
let examplesRun = 0;
|
|
430
|
-
let examplesFailed = 0;
|
|
431
|
-
let exampleIndex = 0;
|
|
432
|
-
for (const { exportName, examples } of allExamples) {
|
|
433
|
-
const entryResults = new Map;
|
|
434
|
-
for (let i = 0;i < examples.length; i++) {
|
|
435
|
-
const result = packageResult.results.get(exampleIndex);
|
|
436
|
-
if (result) {
|
|
437
|
-
entryResults.set(i, result);
|
|
438
|
-
examplesRun++;
|
|
439
|
-
if (!result.success)
|
|
440
|
-
examplesFailed++;
|
|
441
|
-
}
|
|
442
|
-
exampleIndex++;
|
|
443
|
-
}
|
|
444
|
-
const entry2 = (spec.exports ?? []).find((e) => e.name === exportName);
|
|
445
|
-
if (entry2) {
|
|
446
|
-
const runtimeErrorDrifts = detectExampleRuntimeErrors(entry2, entryResults);
|
|
447
|
-
for (const drift of runtimeErrorDrifts) {
|
|
448
|
-
runtimeDrifts.push({
|
|
449
|
-
name: entry2.name,
|
|
450
|
-
issue: drift.issue,
|
|
451
|
-
suggestion: drift.suggestion
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
const assertionDrifts = detectExampleAssertionFailures(entry2, entryResults);
|
|
455
|
-
for (const drift of assertionDrifts) {
|
|
456
|
-
runtimeDrifts.push({
|
|
457
|
-
name: entry2.name,
|
|
458
|
-
issue: drift.issue,
|
|
459
|
-
suggestion: drift.suggestion
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
if (isLLMAssertionParsingAvailable() && entry2.examples) {
|
|
463
|
-
for (let exIdx = 0;exIdx < entry2.examples.length; exIdx++) {
|
|
464
|
-
const example = entry2.examples[exIdx];
|
|
465
|
-
const result = entryResults.get(exIdx);
|
|
466
|
-
if (!result?.success || typeof example !== "string")
|
|
467
|
-
continue;
|
|
468
|
-
const regexAssertions = parseAssertions(example);
|
|
469
|
-
if (regexAssertions.length === 0 && hasNonAssertionComments(example)) {
|
|
470
|
-
const llmResult = await parseAssertionsWithLLM(example);
|
|
471
|
-
if (llmResult?.hasAssertions && llmResult.assertions.length > 0) {
|
|
472
|
-
const stdoutLines = result.stdout.split(`
|
|
473
|
-
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
474
|
-
for (let aIdx = 0;aIdx < llmResult.assertions.length; aIdx++) {
|
|
475
|
-
const assertion = llmResult.assertions[aIdx];
|
|
476
|
-
const actual = stdoutLines[aIdx];
|
|
477
|
-
if (actual === undefined) {
|
|
478
|
-
runtimeDrifts.push({
|
|
479
|
-
name: entry2.name,
|
|
480
|
-
issue: `Assertion expected "${assertion.expected}" but no output was produced`,
|
|
481
|
-
suggestion: `Consider using standard syntax: ${assertion.suggestedSyntax}`
|
|
482
|
-
});
|
|
483
|
-
} else if (assertion.expected.trim() !== actual.trim()) {
|
|
484
|
-
runtimeDrifts.push({
|
|
485
|
-
name: entry2.name,
|
|
486
|
-
issue: `Assertion failed: expected "${assertion.expected}" but got "${actual}"`,
|
|
487
|
-
suggestion: `Consider using standard syntax: ${assertion.suggestedSyntax}`
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (examplesFailed > 0) {
|
|
498
|
-
process.stdout.write(chalk.red(`✗ ${examplesFailed}/${examplesRun} example(s) failed
|
|
499
|
-
`));
|
|
500
|
-
} else {
|
|
501
|
-
process.stdout.write(chalk.green(`✓ ${examplesRun} example(s) passed
|
|
502
|
-
`));
|
|
503
|
-
}
|
|
673
|
+
if (exampleResult.run) {
|
|
674
|
+
for (const drift of exampleResult.run.drifts) {
|
|
675
|
+
runtimeDrifts.push({
|
|
676
|
+
name: drift.exportName,
|
|
677
|
+
type: "example-runtime-error",
|
|
678
|
+
issue: drift.issue,
|
|
679
|
+
suggestion: drift.suggestion,
|
|
680
|
+
category: "example"
|
|
681
|
+
});
|
|
504
682
|
}
|
|
505
683
|
}
|
|
506
684
|
}
|
|
507
685
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
let driftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
686
|
+
const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
|
|
687
|
+
let driftExports = validations.length > 0 ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
|
|
511
688
|
const fixedDriftKeys = new Set;
|
|
512
689
|
if (shouldFix && driftExports.length > 0) {
|
|
513
690
|
const allDrifts = collectDriftsFromExports(spec.exports ?? []);
|
|
514
691
|
if (allDrifts.length > 0) {
|
|
515
692
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
516
693
|
if (fixable.length === 0) {
|
|
517
|
-
log(
|
|
694
|
+
log(chalk2.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
518
695
|
} else {
|
|
519
696
|
log("");
|
|
520
|
-
log(
|
|
697
|
+
log(chalk2.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
521
698
|
if (nonFixable.length > 0) {
|
|
522
|
-
log(
|
|
699
|
+
log(chalk2.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
|
|
523
700
|
}
|
|
524
701
|
log("");
|
|
525
702
|
const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
|
|
@@ -527,29 +704,30 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
527
704
|
const editsByFile = new Map;
|
|
528
705
|
for (const [exp, drifts] of groupedDrifts) {
|
|
529
706
|
if (!exp.source?.file) {
|
|
530
|
-
log(
|
|
707
|
+
log(chalk2.gray(` Skipping ${exp.name}: no source location`));
|
|
531
708
|
continue;
|
|
532
709
|
}
|
|
533
710
|
if (exp.source.file.endsWith(".d.ts")) {
|
|
534
|
-
log(
|
|
711
|
+
log(chalk2.gray(` Skipping ${exp.name}: declaration file`));
|
|
535
712
|
continue;
|
|
536
713
|
}
|
|
537
|
-
const filePath =
|
|
538
|
-
if (!
|
|
539
|
-
log(
|
|
714
|
+
const filePath = path3.resolve(targetDir, exp.source.file);
|
|
715
|
+
if (!fs2.existsSync(filePath)) {
|
|
716
|
+
log(chalk2.gray(` Skipping ${exp.name}: file not found`));
|
|
540
717
|
continue;
|
|
541
718
|
}
|
|
542
719
|
const sourceFile = createSourceFile(filePath);
|
|
543
720
|
const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
|
|
544
721
|
if (!location) {
|
|
545
|
-
log(
|
|
722
|
+
log(chalk2.gray(` Skipping ${exp.name}: could not find declaration`));
|
|
546
723
|
continue;
|
|
547
724
|
}
|
|
548
725
|
let existingPatch = {};
|
|
549
726
|
if (location.hasExisting && location.existingJSDoc) {
|
|
550
727
|
existingPatch = parseJSDocToPatch(location.existingJSDoc);
|
|
551
728
|
}
|
|
552
|
-
const
|
|
729
|
+
const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
|
|
730
|
+
const fixes = generateFixesForExport(expWithDrift, existingPatch);
|
|
553
731
|
if (fixes.length === 0)
|
|
554
732
|
continue;
|
|
555
733
|
for (const drift of drifts) {
|
|
@@ -574,39 +752,27 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
574
752
|
}
|
|
575
753
|
if (edits.length > 0) {
|
|
576
754
|
if (options.dryRun) {
|
|
577
|
-
log(
|
|
755
|
+
log(chalk2.bold("Dry run - changes that would be made:"));
|
|
578
756
|
log("");
|
|
579
757
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
580
|
-
const relativePath =
|
|
581
|
-
log(
|
|
758
|
+
const relativePath = path3.relative(targetDir, filePath);
|
|
759
|
+
log(chalk2.cyan(` ${relativePath}:`));
|
|
582
760
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
583
761
|
const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
|
|
584
|
-
log(` ${
|
|
762
|
+
log(` ${chalk2.bold(exp.name)} [${lineInfo}]`);
|
|
585
763
|
for (const fix of fixes) {
|
|
586
|
-
log(
|
|
764
|
+
log(chalk2.green(` + ${fix.description}`));
|
|
587
765
|
}
|
|
588
766
|
}
|
|
589
767
|
log("");
|
|
590
768
|
}
|
|
591
|
-
log(
|
|
769
|
+
log(chalk2.gray("Run without --dry-run to apply these changes."));
|
|
592
770
|
} else {
|
|
593
|
-
process.stdout.write(chalk.cyan(`> Applying fixes...
|
|
594
|
-
`));
|
|
595
771
|
const applyResult = await applyEdits(edits);
|
|
596
772
|
if (applyResult.errors.length > 0) {
|
|
597
|
-
process.stdout.write(chalk.yellow(`⚠ Some fixes could not be applied
|
|
598
|
-
`));
|
|
599
773
|
for (const err of applyResult.errors) {
|
|
600
|
-
error(
|
|
774
|
+
error(chalk2.red(` ${err.file}: ${err.error}`));
|
|
601
775
|
}
|
|
602
|
-
} else {
|
|
603
|
-
process.stdout.write(chalk.green(`✓ Applied ${applyResult.editsApplied} fix(es) to ${applyResult.filesModified} file(s)
|
|
604
|
-
`));
|
|
605
|
-
}
|
|
606
|
-
log("");
|
|
607
|
-
for (const [filePath, fileEdits] of editsByFile) {
|
|
608
|
-
const relativePath = path2.relative(targetDir, filePath);
|
|
609
|
-
log(chalk.green(` ✓ ${relativePath}: ${fileEdits.length} fix(es)`));
|
|
610
776
|
}
|
|
611
777
|
}
|
|
612
778
|
}
|
|
@@ -616,68 +782,149 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
616
782
|
driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
617
783
|
}
|
|
618
784
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
785
|
+
if (format !== "text") {
|
|
786
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
787
|
+
const stats = computeStats(spec);
|
|
788
|
+
const report = generateReport(specResult.spec);
|
|
789
|
+
const jsonContent = JSON.stringify(report, null, 2);
|
|
790
|
+
let formatContent;
|
|
791
|
+
switch (format) {
|
|
792
|
+
case "json":
|
|
793
|
+
formatContent = jsonContent;
|
|
794
|
+
break;
|
|
795
|
+
case "markdown":
|
|
796
|
+
formatContent = renderMarkdown(stats, { limit });
|
|
797
|
+
break;
|
|
798
|
+
case "html":
|
|
799
|
+
formatContent = renderHtml(stats, { limit });
|
|
800
|
+
break;
|
|
801
|
+
case "github":
|
|
802
|
+
formatContent = renderGithubSummary(stats, {
|
|
803
|
+
coverageScore,
|
|
804
|
+
driftCount: driftExports.length,
|
|
805
|
+
qualityIssues: violations.length
|
|
806
|
+
});
|
|
807
|
+
break;
|
|
808
|
+
default:
|
|
809
|
+
throw new Error(`Unknown format: ${format}`);
|
|
810
|
+
}
|
|
811
|
+
if (options.stdout) {
|
|
812
|
+
log(formatContent);
|
|
813
|
+
} else {
|
|
814
|
+
writeReports({
|
|
815
|
+
format,
|
|
816
|
+
formatContent,
|
|
817
|
+
jsonContent,
|
|
818
|
+
outputPath: options.output,
|
|
819
|
+
cwd: options.cwd
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const totalExportsForDrift2 = spec.exports?.length ?? 0;
|
|
823
|
+
const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
|
|
824
|
+
const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
|
|
825
|
+
const coverageFailed2 = minCoverage !== undefined && coverageScore < minCoverage;
|
|
826
|
+
const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
|
|
827
|
+
const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
828
|
+
const hasTypecheckErrors2 = typecheckErrors.length > 0;
|
|
829
|
+
if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2) {
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const totalExportsForDrift = spec.exports?.length ?? 0;
|
|
835
|
+
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
836
|
+
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
837
|
+
const coverageFailed = minCoverage !== undefined && coverageScore < minCoverage;
|
|
838
|
+
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
839
|
+
const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
|
|
623
840
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
624
|
-
if (
|
|
625
|
-
log(
|
|
626
|
-
|
|
627
|
-
log(
|
|
628
|
-
|
|
629
|
-
log(
|
|
841
|
+
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
842
|
+
log("");
|
|
843
|
+
for (const diag of specWarnings) {
|
|
844
|
+
log(chalk2.yellow(`⚠ ${diag.message}`));
|
|
845
|
+
if (diag.suggestion) {
|
|
846
|
+
log(chalk2.gray(` ${diag.suggestion}`));
|
|
630
847
|
}
|
|
631
848
|
}
|
|
632
|
-
|
|
633
|
-
log(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
log(chalk.yellow(` • ${drift.name}: ${drift.issue}`));
|
|
637
|
-
if (drift.suggestion) {
|
|
638
|
-
log(chalk.gray(` Suggestion: ${drift.suggestion}`));
|
|
639
|
-
}
|
|
849
|
+
for (const diag of specInfos) {
|
|
850
|
+
log(chalk2.cyan(`ℹ ${diag.message}`));
|
|
851
|
+
if (diag.suggestion) {
|
|
852
|
+
log(chalk2.gray(` ${diag.suggestion}`));
|
|
640
853
|
}
|
|
641
854
|
}
|
|
642
|
-
return;
|
|
643
855
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
856
|
+
const pkgName = spec.meta?.name ?? "unknown";
|
|
857
|
+
const pkgVersion = spec.meta?.version ?? "";
|
|
858
|
+
const totalExports = spec.exports?.length ?? 0;
|
|
859
|
+
const errorCount = violations.filter((v) => v.violation.severity === "error").length;
|
|
860
|
+
const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
|
|
861
|
+
log("");
|
|
862
|
+
log(chalk2.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
|
|
863
|
+
log("");
|
|
864
|
+
log(` Exports: ${totalExports}`);
|
|
865
|
+
if (minCoverage !== undefined) {
|
|
866
|
+
if (coverageFailed) {
|
|
867
|
+
log(chalk2.red(` Coverage: ✗ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
|
|
868
|
+
} else {
|
|
869
|
+
log(chalk2.green(` Coverage: ✓ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
log(` Coverage: ${coverageScore}%`);
|
|
650
873
|
}
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
874
|
+
if (maxDrift !== undefined) {
|
|
875
|
+
if (driftFailed) {
|
|
876
|
+
log(chalk2.red(` Drift: ✗ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
|
|
877
|
+
} else {
|
|
878
|
+
log(chalk2.green(` Drift: ✓ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
|
|
656
879
|
}
|
|
880
|
+
} else {
|
|
881
|
+
log(` Drift: ${driftScore}%`);
|
|
657
882
|
}
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
883
|
+
if (exampleResult) {
|
|
884
|
+
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
885
|
+
if (typecheckCount > 0) {
|
|
886
|
+
log(` Examples: ${typecheckCount} type errors`);
|
|
887
|
+
} else {
|
|
888
|
+
log(chalk2.green(` Examples: ✓ validated`));
|
|
663
889
|
}
|
|
664
890
|
}
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
891
|
+
if (errorCount > 0 || warnCount > 0) {
|
|
892
|
+
const parts = [];
|
|
893
|
+
if (errorCount > 0)
|
|
894
|
+
parts.push(`${errorCount} errors`);
|
|
895
|
+
if (warnCount > 0)
|
|
896
|
+
parts.push(`${warnCount} warnings`);
|
|
897
|
+
log(` Quality: ${parts.join(", ")}`);
|
|
898
|
+
}
|
|
899
|
+
log("");
|
|
900
|
+
const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
|
|
901
|
+
if (!failed) {
|
|
902
|
+
const thresholdParts = [];
|
|
903
|
+
if (minCoverage !== undefined) {
|
|
904
|
+
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
670
905
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
906
|
+
if (maxDrift !== undefined) {
|
|
907
|
+
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
908
|
+
}
|
|
909
|
+
if (thresholdParts.length > 0) {
|
|
910
|
+
log(chalk2.green(`✓ Check passed (${thresholdParts.join(", ")})`));
|
|
911
|
+
} else {
|
|
912
|
+
log(chalk2.green("✓ Check passed"));
|
|
913
|
+
log(chalk2.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
|
|
676
914
|
}
|
|
915
|
+
return;
|
|
677
916
|
}
|
|
917
|
+
if (hasQualityErrors) {
|
|
918
|
+
log(chalk2.red(`✗ ${errorCount} quality errors`));
|
|
919
|
+
}
|
|
920
|
+
if (hasTypecheckErrors) {
|
|
921
|
+
log(chalk2.red(`✗ ${typecheckErrors.length} example type errors`));
|
|
922
|
+
}
|
|
923
|
+
log("");
|
|
924
|
+
log(chalk2.dim("Use --format json or --format markdown for detailed reports"));
|
|
678
925
|
process.exit(1);
|
|
679
926
|
} catch (commandError) {
|
|
680
|
-
error(
|
|
927
|
+
error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
681
928
|
process.exit(1);
|
|
682
929
|
}
|
|
683
930
|
});
|
|
@@ -688,20 +935,6 @@ function clampCoverage(value) {
|
|
|
688
935
|
}
|
|
689
936
|
return Math.min(100, Math.max(0, Math.round(value)));
|
|
690
937
|
}
|
|
691
|
-
function collectFailingExports(exportsList, minCoverage) {
|
|
692
|
-
const offenders = [];
|
|
693
|
-
for (const entry of exportsList) {
|
|
694
|
-
const exportScore = entry.docs?.coverageScore ?? 0;
|
|
695
|
-
const missing = entry.docs?.missing;
|
|
696
|
-
if (exportScore < minCoverage || missing && missing.length > 0) {
|
|
697
|
-
offenders.push({
|
|
698
|
-
name: entry.name,
|
|
699
|
-
missing
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return offenders;
|
|
704
|
-
}
|
|
705
938
|
function collectDrift(exportsList) {
|
|
706
939
|
const drifts = [];
|
|
707
940
|
for (const entry of exportsList) {
|
|
@@ -709,11 +942,13 @@ function collectDrift(exportsList) {
|
|
|
709
942
|
if (!drift || drift.length === 0) {
|
|
710
943
|
continue;
|
|
711
944
|
}
|
|
712
|
-
for (const
|
|
945
|
+
for (const d of drift) {
|
|
713
946
|
drifts.push({
|
|
714
947
|
name: entry.name,
|
|
715
|
-
|
|
716
|
-
|
|
948
|
+
type: d.type,
|
|
949
|
+
issue: d.issue ?? "Documentation drift detected.",
|
|
950
|
+
suggestion: d.suggestion,
|
|
951
|
+
category: DRIFT_CATEGORIES2[d.type]
|
|
717
952
|
});
|
|
718
953
|
}
|
|
719
954
|
}
|
|
@@ -721,15 +956,15 @@ function collectDrift(exportsList) {
|
|
|
721
956
|
}
|
|
722
957
|
|
|
723
958
|
// src/commands/diff.ts
|
|
724
|
-
import * as
|
|
725
|
-
import * as
|
|
959
|
+
import * as fs3 from "node:fs";
|
|
960
|
+
import * as path4 from "node:path";
|
|
726
961
|
import {
|
|
727
962
|
diffSpecWithDocs,
|
|
728
963
|
getDocsImpactSummary,
|
|
729
964
|
hasDocsImpact,
|
|
730
965
|
parseMarkdownFiles
|
|
731
966
|
} from "@doccov/sdk";
|
|
732
|
-
import
|
|
967
|
+
import chalk3 from "chalk";
|
|
733
968
|
import { glob } from "glob";
|
|
734
969
|
|
|
735
970
|
// src/utils/docs-impact-ai.ts
|
|
@@ -794,7 +1029,7 @@ Keep it concise and developer-friendly.`
|
|
|
794
1029
|
|
|
795
1030
|
// src/commands/diff.ts
|
|
796
1031
|
var defaultDependencies2 = {
|
|
797
|
-
readFileSync:
|
|
1032
|
+
readFileSync: fs3.readFileSync,
|
|
798
1033
|
log: console.log,
|
|
799
1034
|
error: console.error
|
|
800
1035
|
};
|
|
@@ -838,7 +1073,7 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
838
1073
|
const configResult = await loadDocCovConfig(process.cwd());
|
|
839
1074
|
if (configResult?.docs?.include) {
|
|
840
1075
|
docsPatterns = configResult.docs.include;
|
|
841
|
-
log(
|
|
1076
|
+
log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
842
1077
|
}
|
|
843
1078
|
}
|
|
844
1079
|
if (docsPatterns && docsPatterns.length > 0) {
|
|
@@ -867,10 +1102,10 @@ function registerDiffCommand(program, dependencies = {}) {
|
|
|
867
1102
|
printTextDiff(diff, log, error);
|
|
868
1103
|
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
869
1104
|
if (!isAIDocsAnalysisAvailable()) {
|
|
870
|
-
log(
|
|
1105
|
+
log(chalk3.yellow(`
|
|
871
1106
|
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
872
1107
|
} else {
|
|
873
|
-
log(
|
|
1108
|
+
log(chalk3.gray(`
|
|
874
1109
|
Generating AI summary...`));
|
|
875
1110
|
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
876
1111
|
file: f.file,
|
|
@@ -881,46 +1116,46 @@ Generating AI summary...`));
|
|
|
881
1116
|
const summary = await generateImpactSummary(impacts);
|
|
882
1117
|
if (summary) {
|
|
883
1118
|
log("");
|
|
884
|
-
log(
|
|
885
|
-
log(
|
|
1119
|
+
log(chalk3.bold("AI Summary"));
|
|
1120
|
+
log(chalk3.cyan(` ${summary}`));
|
|
886
1121
|
}
|
|
887
1122
|
}
|
|
888
1123
|
}
|
|
889
1124
|
break;
|
|
890
1125
|
}
|
|
891
1126
|
if (strictOptions.has("regression") && diff.coverageDelta < 0) {
|
|
892
|
-
error(
|
|
1127
|
+
error(chalk3.red(`
|
|
893
1128
|
Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
|
|
894
1129
|
process.exitCode = 1;
|
|
895
1130
|
return;
|
|
896
1131
|
}
|
|
897
1132
|
if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
|
|
898
|
-
error(
|
|
1133
|
+
error(chalk3.red(`
|
|
899
1134
|
${diff.driftIntroduced} new drift issue(s) introduced`));
|
|
900
1135
|
process.exitCode = 1;
|
|
901
1136
|
return;
|
|
902
1137
|
}
|
|
903
1138
|
if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
|
|
904
1139
|
const summary = getDocsImpactSummary(diff);
|
|
905
|
-
error(
|
|
1140
|
+
error(chalk3.red(`
|
|
906
1141
|
${summary.totalIssues} docs issue(s) require attention`));
|
|
907
1142
|
process.exitCode = 1;
|
|
908
1143
|
return;
|
|
909
1144
|
}
|
|
910
1145
|
if (strictOptions.has("breaking") && diff.breaking.length > 0) {
|
|
911
|
-
error(
|
|
1146
|
+
error(chalk3.red(`
|
|
912
1147
|
${diff.breaking.length} breaking change(s) detected`));
|
|
913
1148
|
process.exitCode = 1;
|
|
914
1149
|
return;
|
|
915
1150
|
}
|
|
916
1151
|
if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
|
|
917
|
-
error(
|
|
1152
|
+
error(chalk3.red(`
|
|
918
1153
|
${diff.newUndocumented.length} new undocumented export(s)`));
|
|
919
1154
|
process.exitCode = 1;
|
|
920
1155
|
return;
|
|
921
1156
|
}
|
|
922
1157
|
} catch (commandError) {
|
|
923
|
-
error(
|
|
1158
|
+
error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
924
1159
|
process.exitCode = 1;
|
|
925
1160
|
}
|
|
926
1161
|
});
|
|
@@ -934,7 +1169,7 @@ async function loadMarkdownFiles(patterns) {
|
|
|
934
1169
|
const matches = await glob(pattern, { nodir: true });
|
|
935
1170
|
for (const filePath of matches) {
|
|
936
1171
|
try {
|
|
937
|
-
const content =
|
|
1172
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
938
1173
|
files.push({ path: filePath, content });
|
|
939
1174
|
} catch {}
|
|
940
1175
|
}
|
|
@@ -942,8 +1177,8 @@ async function loadMarkdownFiles(patterns) {
|
|
|
942
1177
|
return parseMarkdownFiles(files);
|
|
943
1178
|
}
|
|
944
1179
|
function loadSpec(filePath, readFileSync2) {
|
|
945
|
-
const resolvedPath =
|
|
946
|
-
if (!
|
|
1180
|
+
const resolvedPath = path4.resolve(filePath);
|
|
1181
|
+
if (!fs3.existsSync(resolvedPath)) {
|
|
947
1182
|
throw new Error(`File not found: ${filePath}`);
|
|
948
1183
|
}
|
|
949
1184
|
try {
|
|
@@ -955,7 +1190,7 @@ function loadSpec(filePath, readFileSync2) {
|
|
|
955
1190
|
}
|
|
956
1191
|
function printTextDiff(diff, log, _error) {
|
|
957
1192
|
log("");
|
|
958
|
-
log(
|
|
1193
|
+
log(chalk3.bold("DocCov Diff Report"));
|
|
959
1194
|
log("─".repeat(40));
|
|
960
1195
|
printCoverage(diff, log);
|
|
961
1196
|
printAPIChanges(diff, log);
|
|
@@ -965,11 +1200,11 @@ function printTextDiff(diff, log, _error) {
|
|
|
965
1200
|
log("");
|
|
966
1201
|
}
|
|
967
1202
|
function printCoverage(diff, log) {
|
|
968
|
-
const coverageColor = diff.coverageDelta > 0 ?
|
|
1203
|
+
const coverageColor = diff.coverageDelta > 0 ? chalk3.green : diff.coverageDelta < 0 ? chalk3.red : chalk3.gray;
|
|
969
1204
|
const coverageSymbol = diff.coverageDelta > 0 ? "↑" : diff.coverageDelta < 0 ? "↓" : "→";
|
|
970
1205
|
const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
|
|
971
1206
|
log("");
|
|
972
|
-
log(
|
|
1207
|
+
log(chalk3.bold("Coverage"));
|
|
973
1208
|
log(` ${diff.oldCoverage}% ${coverageSymbol} ${diff.newCoverage}% ${coverageColor(`(${deltaStr}%)`)}`);
|
|
974
1209
|
}
|
|
975
1210
|
function printAPIChanges(diff, log) {
|
|
@@ -977,31 +1212,31 @@ function printAPIChanges(diff, log) {
|
|
|
977
1212
|
if (!hasChanges)
|
|
978
1213
|
return;
|
|
979
1214
|
log("");
|
|
980
|
-
log(
|
|
1215
|
+
log(chalk3.bold("API Changes"));
|
|
981
1216
|
const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
|
|
982
1217
|
const classesWithMembers = new Set(membersByClass.keys());
|
|
983
1218
|
for (const [className, changes] of membersByClass) {
|
|
984
1219
|
const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
|
|
985
1220
|
const isHighSeverity = categorized?.severity === "high";
|
|
986
|
-
const label = isHighSeverity ?
|
|
987
|
-
log(
|
|
1221
|
+
const label = isHighSeverity ? chalk3.red(" [BREAKING]") : chalk3.yellow(" [CHANGED]");
|
|
1222
|
+
log(chalk3.cyan(` ${className}`) + label);
|
|
988
1223
|
const removed = changes.filter((c) => c.changeType === "removed");
|
|
989
1224
|
for (const mc of removed) {
|
|
990
|
-
const suggestion = mc.suggestion ?
|
|
991
|
-
log(
|
|
1225
|
+
const suggestion = mc.suggestion ? chalk3.gray(` → ${mc.suggestion}`) : "";
|
|
1226
|
+
log(chalk3.red(` ✖ ${mc.memberName}()`) + suggestion);
|
|
992
1227
|
}
|
|
993
1228
|
const changed = changes.filter((c) => c.changeType === "signature-changed");
|
|
994
1229
|
for (const mc of changed) {
|
|
995
|
-
log(
|
|
1230
|
+
log(chalk3.yellow(` ~ ${mc.memberName}() signature changed`));
|
|
996
1231
|
if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
|
|
997
|
-
log(
|
|
998
|
-
log(
|
|
1232
|
+
log(chalk3.gray(` was: ${mc.oldSignature}`));
|
|
1233
|
+
log(chalk3.gray(` now: ${mc.newSignature}`));
|
|
999
1234
|
}
|
|
1000
1235
|
}
|
|
1001
1236
|
const added = changes.filter((c) => c.changeType === "added");
|
|
1002
1237
|
if (added.length > 0) {
|
|
1003
1238
|
const addedNames = added.map((a) => `${a.memberName}()`).join(", ");
|
|
1004
|
-
log(
|
|
1239
|
+
log(chalk3.green(` + ${addedNames}`));
|
|
1005
1240
|
}
|
|
1006
1241
|
}
|
|
1007
1242
|
const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
|
|
@@ -1010,43 +1245,43 @@ function printAPIChanges(diff, log) {
|
|
|
1010
1245
|
const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
|
|
1011
1246
|
if (functionChanges.length > 0) {
|
|
1012
1247
|
log("");
|
|
1013
|
-
log(
|
|
1248
|
+
log(chalk3.red(` Function Changes (${functionChanges.length}):`));
|
|
1014
1249
|
for (const fc of functionChanges.slice(0, 3)) {
|
|
1015
1250
|
const reason = fc.reason === "removed" ? "removed" : "signature changed";
|
|
1016
|
-
log(
|
|
1251
|
+
log(chalk3.red(` ✖ ${fc.name} (${reason})`));
|
|
1017
1252
|
}
|
|
1018
1253
|
if (functionChanges.length > 3) {
|
|
1019
|
-
log(
|
|
1254
|
+
log(chalk3.gray(` ... and ${functionChanges.length - 3} more`));
|
|
1020
1255
|
}
|
|
1021
1256
|
}
|
|
1022
1257
|
if (typeChanges.length > 0) {
|
|
1023
1258
|
log("");
|
|
1024
|
-
log(
|
|
1259
|
+
log(chalk3.yellow(` Type/Interface Changes (${typeChanges.length}):`));
|
|
1025
1260
|
const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
|
|
1026
|
-
log(
|
|
1261
|
+
log(chalk3.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
|
|
1027
1262
|
}
|
|
1028
1263
|
if (otherChanges.length > 0) {
|
|
1029
1264
|
log("");
|
|
1030
|
-
log(
|
|
1265
|
+
log(chalk3.gray(` Other Changes (${otherChanges.length}):`));
|
|
1031
1266
|
const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
|
|
1032
|
-
log(
|
|
1267
|
+
log(chalk3.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
|
|
1033
1268
|
}
|
|
1034
1269
|
if (diff.nonBreaking.length > 0) {
|
|
1035
1270
|
const undocCount = diff.newUndocumented.length;
|
|
1036
|
-
const undocSuffix = undocCount > 0 ?
|
|
1271
|
+
const undocSuffix = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
|
|
1037
1272
|
log("");
|
|
1038
|
-
log(
|
|
1273
|
+
log(chalk3.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
|
|
1039
1274
|
const exportNames = diff.nonBreaking.slice(0, 3);
|
|
1040
|
-
log(
|
|
1275
|
+
log(chalk3.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
|
|
1041
1276
|
}
|
|
1042
1277
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1043
1278
|
log("");
|
|
1044
1279
|
const parts = [];
|
|
1045
1280
|
if (diff.driftIntroduced > 0) {
|
|
1046
|
-
parts.push(
|
|
1281
|
+
parts.push(chalk3.red(`+${diff.driftIntroduced} drift`));
|
|
1047
1282
|
}
|
|
1048
1283
|
if (diff.driftResolved > 0) {
|
|
1049
|
-
parts.push(
|
|
1284
|
+
parts.push(chalk3.green(`-${diff.driftResolved} resolved`));
|
|
1050
1285
|
}
|
|
1051
1286
|
log(` Drift: ${parts.join(", ")}`);
|
|
1052
1287
|
}
|
|
@@ -1056,10 +1291,10 @@ function printDocsRequiringUpdates(diff, log) {
|
|
|
1056
1291
|
return;
|
|
1057
1292
|
const { impactedFiles, missingDocs, stats } = diff.docsImpact;
|
|
1058
1293
|
log("");
|
|
1059
|
-
log(
|
|
1060
|
-
log(
|
|
1294
|
+
log(chalk3.bold("Docs Requiring Updates"));
|
|
1295
|
+
log(chalk3.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
|
|
1061
1296
|
if (impactedFiles.length === 0 && missingDocs.length === 0) {
|
|
1062
|
-
log(
|
|
1297
|
+
log(chalk3.green(" ✓ No updates needed"));
|
|
1063
1298
|
return;
|
|
1064
1299
|
}
|
|
1065
1300
|
const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
|
|
@@ -1074,49 +1309,49 @@ function printDocsRequiringUpdates(diff, log) {
|
|
|
1074
1309
|
}
|
|
1075
1310
|
}
|
|
1076
1311
|
for (const file of actionableFiles.slice(0, 6)) {
|
|
1077
|
-
const filename =
|
|
1312
|
+
const filename = path4.basename(file.file);
|
|
1078
1313
|
const issueCount = file.references.length;
|
|
1079
1314
|
log("");
|
|
1080
|
-
log(
|
|
1315
|
+
log(chalk3.yellow(` ${filename}`) + chalk3.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
|
|
1081
1316
|
const actionableRefs = file.references.filter((r) => !r.isInstantiation);
|
|
1082
1317
|
for (const ref of actionableRefs.slice(0, 4)) {
|
|
1083
1318
|
if (ref.memberName) {
|
|
1084
1319
|
const action = ref.changeType === "method-removed" ? "→" : "~";
|
|
1085
1320
|
const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
|
|
1086
|
-
log(
|
|
1321
|
+
log(chalk3.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
|
|
1087
1322
|
} else {
|
|
1088
1323
|
const action = ref.changeType === "removed" ? "→" : "~";
|
|
1089
1324
|
const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
|
|
1090
|
-
log(
|
|
1325
|
+
log(chalk3.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
|
|
1091
1326
|
}
|
|
1092
1327
|
}
|
|
1093
1328
|
if (actionableRefs.length > 4) {
|
|
1094
|
-
log(
|
|
1329
|
+
log(chalk3.gray(` ... and ${actionableRefs.length - 4} more`));
|
|
1095
1330
|
}
|
|
1096
1331
|
}
|
|
1097
1332
|
if (actionableFiles.length > 6) {
|
|
1098
|
-
log(
|
|
1333
|
+
log(chalk3.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
|
|
1099
1334
|
}
|
|
1100
1335
|
if (instantiationOnlyFiles.length > 0) {
|
|
1101
1336
|
log("");
|
|
1102
|
-
const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) =>
|
|
1337
|
+
const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path4.basename(f.file));
|
|
1103
1338
|
const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
|
|
1104
|
-
log(
|
|
1105
|
-
log(
|
|
1339
|
+
log(chalk3.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
|
|
1340
|
+
log(chalk3.gray(` ${fileNames.join(", ")}${suffix}`));
|
|
1106
1341
|
}
|
|
1107
1342
|
const { allUndocumented } = diff.docsImpact;
|
|
1108
1343
|
if (missingDocs.length > 0) {
|
|
1109
1344
|
log("");
|
|
1110
|
-
log(
|
|
1345
|
+
log(chalk3.yellow(` New exports missing docs (${missingDocs.length}):`));
|
|
1111
1346
|
const names = missingDocs.slice(0, 4);
|
|
1112
|
-
log(
|
|
1347
|
+
log(chalk3.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
|
|
1113
1348
|
}
|
|
1114
1349
|
if (allUndocumented && allUndocumented.length > 0) {
|
|
1115
1350
|
const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
|
|
1116
1351
|
log("");
|
|
1117
|
-
log(
|
|
1352
|
+
log(chalk3.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
|
|
1118
1353
|
if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
|
|
1119
|
-
log(
|
|
1354
|
+
log(chalk3.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
|
|
1120
1355
|
}
|
|
1121
1356
|
}
|
|
1122
1357
|
}
|
|
@@ -1267,7 +1502,7 @@ function generateHTMLReport(diff) {
|
|
|
1267
1502
|
<h2>Documentation Impact</h2>
|
|
1268
1503
|
<div class="card">`;
|
|
1269
1504
|
for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
|
|
1270
|
-
const filename =
|
|
1505
|
+
const filename = path4.basename(file.file);
|
|
1271
1506
|
html += `
|
|
1272
1507
|
<div class="file-item">
|
|
1273
1508
|
<div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
|
|
@@ -1301,278 +1536,39 @@ function generateHTMLReport(diff) {
|
|
|
1301
1536
|
<ul>`;
|
|
1302
1537
|
for (const name of diff.newUndocumented.slice(0, 10)) {
|
|
1303
1538
|
html += `
|
|
1304
|
-
<li><code>${name}</code></li>`;
|
|
1305
|
-
}
|
|
1306
|
-
if (diff.newUndocumented.length > 10) {
|
|
1307
|
-
html += `
|
|
1308
|
-
<li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
|
|
1309
|
-
}
|
|
1310
|
-
html += `
|
|
1311
|
-
</ul>`;
|
|
1312
|
-
}
|
|
1313
|
-
if (diff.docsImpact?.stats) {
|
|
1314
|
-
const { stats, allUndocumented } = diff.docsImpact;
|
|
1315
|
-
const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
|
|
1316
|
-
html += `
|
|
1317
|
-
<div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
|
|
1318
|
-
<span class="metric-label">Total Documentation Coverage</span>
|
|
1319
|
-
<span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
|
|
1320
|
-
</div>`;
|
|
1321
|
-
}
|
|
1322
|
-
html += `
|
|
1323
|
-
</div>`;
|
|
1324
|
-
}
|
|
1325
|
-
html += `
|
|
1326
|
-
</div>
|
|
1327
|
-
</body>
|
|
1328
|
-
</html>`;
|
|
1329
|
-
return html;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// src/commands/generate.ts
|
|
1333
|
-
import * as fs3 from "node:fs";
|
|
1334
|
-
import * as path4 from "node:path";
|
|
1335
|
-
import {
|
|
1336
|
-
DocCov as DocCov2,
|
|
1337
|
-
NodeFileSystem as NodeFileSystem2,
|
|
1338
|
-
resolveTarget as resolveTarget2
|
|
1339
|
-
} from "@doccov/sdk";
|
|
1340
|
-
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
1341
|
-
import chalk4 from "chalk";
|
|
1342
|
-
|
|
1343
|
-
// src/reports/markdown.ts
|
|
1344
|
-
function bar(pct, width = 10) {
|
|
1345
|
-
const filled = Math.round(pct / 100 * width);
|
|
1346
|
-
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1347
|
-
}
|
|
1348
|
-
function renderMarkdown(stats, options = {}) {
|
|
1349
|
-
const limit = options.limit ?? 20;
|
|
1350
|
-
const lines = [];
|
|
1351
|
-
lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
|
|
1352
|
-
lines.push("");
|
|
1353
|
-
lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
|
|
1354
|
-
lines.push("");
|
|
1355
|
-
lines.push("| Metric | Value |");
|
|
1356
|
-
lines.push("|--------|-------|");
|
|
1357
|
-
lines.push(`| Exports | ${stats.totalExports} |`);
|
|
1358
|
-
lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
|
|
1359
|
-
lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
|
|
1360
|
-
lines.push(`| Undocumented | ${stats.undocumented} |`);
|
|
1361
|
-
lines.push(`| Drift issues | ${stats.driftCount} |`);
|
|
1362
|
-
lines.push("");
|
|
1363
|
-
lines.push("## Coverage by Signal");
|
|
1364
|
-
lines.push("");
|
|
1365
|
-
lines.push("| Signal | Coverage |");
|
|
1366
|
-
lines.push("|--------|----------|");
|
|
1367
|
-
for (const [sig, s] of Object.entries(stats.signalCoverage)) {
|
|
1368
|
-
lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
|
|
1369
|
-
}
|
|
1370
|
-
if (stats.byKind.length > 0) {
|
|
1371
|
-
lines.push("");
|
|
1372
|
-
lines.push("## Coverage by Kind");
|
|
1373
|
-
lines.push("");
|
|
1374
|
-
lines.push("| Kind | Count | Avg Score |");
|
|
1375
|
-
lines.push("|------|-------|-----------|");
|
|
1376
|
-
for (const k of stats.byKind) {
|
|
1377
|
-
lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
|
|
1381
|
-
if (lowExports.length > 0) {
|
|
1382
|
-
lines.push("");
|
|
1383
|
-
lines.push("## Lowest Coverage Exports");
|
|
1384
|
-
lines.push("");
|
|
1385
|
-
lines.push("| Export | Kind | Score | Missing |");
|
|
1386
|
-
lines.push("|--------|------|-------|---------|");
|
|
1387
|
-
for (const e of lowExports) {
|
|
1388
|
-
lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
|
|
1389
|
-
}
|
|
1390
|
-
const totalLow = stats.exports.filter((e) => e.score < 100).length;
|
|
1391
|
-
if (totalLow > limit) {
|
|
1392
|
-
lines.push(`| ... | | | ${totalLow - limit} more |`);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
if (stats.driftIssues.length > 0) {
|
|
1396
|
-
lines.push("");
|
|
1397
|
-
lines.push("## Drift Issues");
|
|
1398
|
-
lines.push("");
|
|
1399
|
-
lines.push("| Export | Type | Issue |");
|
|
1400
|
-
lines.push("|--------|------|-------|");
|
|
1401
|
-
for (const d of stats.driftIssues.slice(0, limit)) {
|
|
1402
|
-
const hint = d.suggestion ? ` → ${d.suggestion}` : "";
|
|
1403
|
-
lines.push(`| \`${d.exportName}\` | ${d.type} | ${d.issue}${hint} |`);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
lines.push("");
|
|
1407
|
-
lines.push("---");
|
|
1408
|
-
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
1409
|
-
return lines.join(`
|
|
1410
|
-
`);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// src/reports/html.ts
|
|
1414
|
-
function escapeHtml(s) {
|
|
1415
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1416
|
-
}
|
|
1417
|
-
function renderHtml(stats, options = {}) {
|
|
1418
|
-
const md = renderMarkdown(stats, options);
|
|
1419
|
-
return `<!DOCTYPE html>
|
|
1420
|
-
<html lang="en">
|
|
1421
|
-
<head>
|
|
1422
|
-
<meta charset="UTF-8">
|
|
1423
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1424
|
-
<title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
|
|
1425
|
-
<style>
|
|
1426
|
-
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
1427
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
|
|
1428
|
-
h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
1429
|
-
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
1430
|
-
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
1431
|
-
th { background: #161b22; }
|
|
1432
|
-
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
1433
|
-
a { color: var(--accent); }
|
|
1434
|
-
</style>
|
|
1435
|
-
</head>
|
|
1436
|
-
<body>
|
|
1437
|
-
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
|
|
1438
|
-
</body>
|
|
1439
|
-
</html>`;
|
|
1440
|
-
}
|
|
1441
|
-
// src/reports/stats.ts
|
|
1442
|
-
function computeStats(spec) {
|
|
1443
|
-
const exports = spec.exports ?? [];
|
|
1444
|
-
const signals = {
|
|
1445
|
-
description: { covered: 0, total: 0 },
|
|
1446
|
-
params: { covered: 0, total: 0 },
|
|
1447
|
-
returns: { covered: 0, total: 0 },
|
|
1448
|
-
examples: { covered: 0, total: 0 }
|
|
1449
|
-
};
|
|
1450
|
-
const kindMap = new Map;
|
|
1451
|
-
const driftIssues = [];
|
|
1452
|
-
let fullyDocumented = 0;
|
|
1453
|
-
let partiallyDocumented = 0;
|
|
1454
|
-
let undocumented = 0;
|
|
1455
|
-
for (const exp of exports) {
|
|
1456
|
-
const score = exp.docs?.coverageScore ?? 0;
|
|
1457
|
-
const missing = exp.docs?.missing ?? [];
|
|
1458
|
-
for (const sig of ["description", "params", "returns", "examples"]) {
|
|
1459
|
-
signals[sig].total++;
|
|
1460
|
-
if (!missing.includes(sig))
|
|
1461
|
-
signals[sig].covered++;
|
|
1462
|
-
}
|
|
1463
|
-
const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
|
|
1464
|
-
kindEntry.count++;
|
|
1465
|
-
kindEntry.totalScore += score;
|
|
1466
|
-
kindMap.set(exp.kind, kindEntry);
|
|
1467
|
-
if (score === 100)
|
|
1468
|
-
fullyDocumented++;
|
|
1469
|
-
else if (score > 0)
|
|
1470
|
-
partiallyDocumented++;
|
|
1471
|
-
else
|
|
1472
|
-
undocumented++;
|
|
1473
|
-
for (const d of exp.docs?.drift ?? []) {
|
|
1474
|
-
driftIssues.push({
|
|
1475
|
-
exportName: exp.name,
|
|
1476
|
-
type: d.type,
|
|
1477
|
-
issue: d.issue,
|
|
1478
|
-
suggestion: d.suggestion
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
|
|
1483
|
-
k,
|
|
1484
|
-
{ ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
|
|
1485
|
-
]));
|
|
1486
|
-
const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
|
|
1487
|
-
kind,
|
|
1488
|
-
count,
|
|
1489
|
-
avgScore: Math.round(totalScore / count)
|
|
1490
|
-
})).sort((a, b) => b.count - a.count);
|
|
1491
|
-
const sortedExports = exports.map((e) => ({
|
|
1492
|
-
name: e.name,
|
|
1493
|
-
kind: e.kind,
|
|
1494
|
-
score: e.docs?.coverageScore ?? 0,
|
|
1495
|
-
missing: e.docs?.missing ?? []
|
|
1496
|
-
})).sort((a, b) => a.score - b.score);
|
|
1497
|
-
return {
|
|
1498
|
-
packageName: spec.meta.name ?? "unknown",
|
|
1499
|
-
version: spec.meta.version ?? "0.0.0",
|
|
1500
|
-
coverageScore: spec.docs?.coverageScore ?? 0,
|
|
1501
|
-
totalExports: exports.length,
|
|
1502
|
-
fullyDocumented,
|
|
1503
|
-
partiallyDocumented,
|
|
1504
|
-
undocumented,
|
|
1505
|
-
driftCount: driftIssues.length,
|
|
1506
|
-
signalCoverage,
|
|
1507
|
-
byKind,
|
|
1508
|
-
exports: sortedExports,
|
|
1509
|
-
driftIssues
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
// src/utils/filter-options.ts
|
|
1513
|
-
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1514
|
-
import chalk3 from "chalk";
|
|
1515
|
-
var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
|
|
1516
|
-
var mergeFilterOptions = (config, cliOptions) => {
|
|
1517
|
-
const messages = [];
|
|
1518
|
-
if (config?.include) {
|
|
1519
|
-
messages.push(formatList("include filters from config", config.include));
|
|
1520
|
-
}
|
|
1521
|
-
if (config?.exclude) {
|
|
1522
|
-
messages.push(formatList("exclude filters from config", config.exclude));
|
|
1523
|
-
}
|
|
1524
|
-
if (cliOptions.include) {
|
|
1525
|
-
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
1526
|
-
}
|
|
1527
|
-
if (cliOptions.exclude) {
|
|
1528
|
-
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
1529
|
-
}
|
|
1530
|
-
const resolved = mergeFilters(config, cliOptions);
|
|
1531
|
-
if (!resolved.include && !resolved.exclude) {
|
|
1532
|
-
return { messages };
|
|
1539
|
+
<li><code>${name}</code></li>`;
|
|
1540
|
+
}
|
|
1541
|
+
if (diff.newUndocumented.length > 10) {
|
|
1542
|
+
html += `
|
|
1543
|
+
<li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
|
|
1544
|
+
}
|
|
1545
|
+
html += `
|
|
1546
|
+
</ul>`;
|
|
1547
|
+
}
|
|
1548
|
+
if (diff.docsImpact?.stats) {
|
|
1549
|
+
const { stats, allUndocumented } = diff.docsImpact;
|
|
1550
|
+
const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
|
|
1551
|
+
html += `
|
|
1552
|
+
<div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
|
|
1553
|
+
<span class="metric-label">Total Documentation Coverage</span>
|
|
1554
|
+
<span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
|
|
1555
|
+
</div>`;
|
|
1556
|
+
}
|
|
1557
|
+
html += `
|
|
1558
|
+
</div>`;
|
|
1533
1559
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
messages
|
|
1540
|
-
};
|
|
1541
|
-
};
|
|
1542
|
-
|
|
1543
|
-
// src/commands/generate.ts
|
|
1544
|
-
var defaultDependencies3 = {
|
|
1545
|
-
createDocCov: (options) => new DocCov2(options),
|
|
1546
|
-
writeFileSync: fs3.writeFileSync,
|
|
1547
|
-
log: console.log,
|
|
1548
|
-
error: console.error
|
|
1549
|
-
};
|
|
1550
|
-
function getArrayLength(value) {
|
|
1551
|
-
return Array.isArray(value) ? value.length : 0;
|
|
1552
|
-
}
|
|
1553
|
-
function stripDocsFields(spec) {
|
|
1554
|
-
const { docs: _rootDocs, ...rest } = spec;
|
|
1555
|
-
return {
|
|
1556
|
-
...rest,
|
|
1557
|
-
exports: spec.exports?.map((exp) => {
|
|
1558
|
-
const { docs: _expDocs, ...expRest } = exp;
|
|
1559
|
-
return expRest;
|
|
1560
|
-
})
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
1564
|
-
const location = diagnostic.location;
|
|
1565
|
-
const relativePath = location?.file ? path4.relative(baseDir, location.file) || location.file : undefined;
|
|
1566
|
-
const locationText = location && relativePath ? chalk4.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
1567
|
-
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
1568
|
-
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
1560
|
+
html += `
|
|
1561
|
+
</div>
|
|
1562
|
+
</body>
|
|
1563
|
+
</html>`;
|
|
1564
|
+
return html;
|
|
1569
1565
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
program.command("
|
|
1566
|
+
|
|
1567
|
+
// src/commands/info.ts
|
|
1568
|
+
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
1569
|
+
import chalk4 from "chalk";
|
|
1570
|
+
function registerInfoCommand(program) {
|
|
1571
|
+
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) => {
|
|
1576
1572
|
try {
|
|
1577
1573
|
const fileSystem = new NodeFileSystem2(options.cwd);
|
|
1578
1574
|
const resolved = await resolveTarget2(fileSystem, {
|
|
@@ -1580,94 +1576,26 @@ function registerGenerateCommand(program, dependencies = {}) {
|
|
|
1580
1576
|
package: options.package,
|
|
1581
1577
|
entry
|
|
1582
1578
|
});
|
|
1583
|
-
const {
|
|
1584
|
-
if (packageInfo) {
|
|
1585
|
-
log(chalk4.gray(`Found package at ${packageInfo.path}`));
|
|
1586
|
-
}
|
|
1587
|
-
if (!entry) {
|
|
1588
|
-
log(chalk4.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
|
|
1589
|
-
}
|
|
1579
|
+
const { entryFile } = resolved;
|
|
1590
1580
|
const resolveExternalTypes = !options.skipResolve;
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
config = await loadDocCovConfig(targetDir);
|
|
1598
|
-
if (config?.filePath) {
|
|
1599
|
-
log(chalk4.gray(`Loaded configuration from ${path4.relative(targetDir, config.filePath)}`));
|
|
1600
|
-
}
|
|
1601
|
-
} catch (configError) {
|
|
1602
|
-
error(chalk4.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
1603
|
-
process.exit(1);
|
|
1604
|
-
}
|
|
1605
|
-
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
1606
|
-
for (const message of resolvedFilters.messages) {
|
|
1607
|
-
log(chalk4.gray(`• ${message}`));
|
|
1608
|
-
}
|
|
1609
|
-
process.stdout.write(chalk4.cyan(`> Generating OpenPkg spec...
|
|
1610
|
-
`));
|
|
1611
|
-
let result;
|
|
1612
|
-
try {
|
|
1613
|
-
const doccov = createDocCov({
|
|
1614
|
-
resolveExternalTypes
|
|
1615
|
-
});
|
|
1616
|
-
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
|
|
1617
|
-
filters: {
|
|
1618
|
-
include: resolvedFilters.include,
|
|
1619
|
-
exclude: resolvedFilters.exclude
|
|
1620
|
-
}
|
|
1621
|
-
} : {};
|
|
1622
|
-
result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
1623
|
-
process.stdout.write(chalk4.green(`✓ Generated OpenPkg spec
|
|
1624
|
-
`));
|
|
1625
|
-
} catch (generationError) {
|
|
1626
|
-
process.stdout.write(chalk4.red(`✗ Failed to generate spec
|
|
1627
|
-
`));
|
|
1628
|
-
throw generationError;
|
|
1629
|
-
}
|
|
1630
|
-
if (!result) {
|
|
1631
|
-
throw new Error("Failed to produce an OpenPkg spec.");
|
|
1632
|
-
}
|
|
1633
|
-
let normalized = normalize(result.spec);
|
|
1634
|
-
if (options.docs === false) {
|
|
1635
|
-
normalized = stripDocsFields(normalized);
|
|
1636
|
-
}
|
|
1637
|
-
const validation = validateSpec(normalized);
|
|
1638
|
-
if (!validation.ok) {
|
|
1639
|
-
error(chalk4.red("Spec failed schema validation"));
|
|
1640
|
-
for (const err of validation.errors) {
|
|
1641
|
-
error(chalk4.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
1642
|
-
}
|
|
1643
|
-
process.exit(1);
|
|
1644
|
-
}
|
|
1645
|
-
const format = options.format ?? "json";
|
|
1646
|
-
const outputPath = path4.resolve(process.cwd(), options.output);
|
|
1647
|
-
if (format === "markdown" || format === "html") {
|
|
1648
|
-
const stats = computeStats(normalized);
|
|
1649
|
-
const limit = parseInt(options.limit, 10) || 20;
|
|
1650
|
-
const reportOutput = format === "html" ? renderHtml(stats, { limit }) : renderMarkdown(stats, { limit });
|
|
1651
|
-
writeFileSync2(outputPath, reportOutput);
|
|
1652
|
-
log(chalk4.green(`✓ Generated ${format} report: ${options.output}`));
|
|
1653
|
-
log(chalk4.gray(` Coverage: ${stats.coverageScore}%`));
|
|
1654
|
-
log(chalk4.gray(` ${stats.totalExports} exports, ${stats.driftCount} drift issues`));
|
|
1655
|
-
} else {
|
|
1656
|
-
writeFileSync2(outputPath, JSON.stringify(normalized, null, 2));
|
|
1657
|
-
log(chalk4.green(`✓ Generated ${options.output}`));
|
|
1658
|
-
log(chalk4.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
1659
|
-
log(chalk4.gray(` ${getArrayLength(normalized.types)} types`));
|
|
1660
|
-
}
|
|
1661
|
-
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
1662
|
-
log("");
|
|
1663
|
-
log(chalk4.bold("Diagnostics"));
|
|
1664
|
-
for (const diagnostic of result.diagnostics) {
|
|
1665
|
-
const prefix = diagnostic.severity === "error" ? chalk4.red("✖") : diagnostic.severity === "warning" ? chalk4.yellow("⚠") : chalk4.cyan("ℹ");
|
|
1666
|
-
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
1667
|
-
}
|
|
1581
|
+
const doccov = new DocCov2({
|
|
1582
|
+
resolveExternalTypes
|
|
1583
|
+
});
|
|
1584
|
+
const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
|
|
1585
|
+
if (!specResult) {
|
|
1586
|
+
throw new Error("Failed to analyze documentation coverage.");
|
|
1668
1587
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1588
|
+
const spec = enrichSpec2(specResult.spec);
|
|
1589
|
+
const stats = computeStats(spec);
|
|
1590
|
+
console.log("");
|
|
1591
|
+
console.log(chalk4.bold(`${stats.packageName}@${stats.version}`));
|
|
1592
|
+
console.log("");
|
|
1593
|
+
console.log(` Exports: ${chalk4.bold(stats.totalExports.toString())}`);
|
|
1594
|
+
console.log(` Coverage: ${chalk4.bold(`${stats.coverageScore}%`)}`);
|
|
1595
|
+
console.log(` Drift: ${chalk4.bold(`${stats.driftScore}%`)}`);
|
|
1596
|
+
console.log("");
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
console.error(chalk4.red("Error:"), err instanceof Error ? err.message : err);
|
|
1671
1599
|
process.exit(1);
|
|
1672
1600
|
}
|
|
1673
1601
|
});
|
|
@@ -1677,7 +1605,7 @@ function registerGenerateCommand(program, dependencies = {}) {
|
|
|
1677
1605
|
import * as fs4 from "node:fs";
|
|
1678
1606
|
import * as path5 from "node:path";
|
|
1679
1607
|
import chalk5 from "chalk";
|
|
1680
|
-
var
|
|
1608
|
+
var defaultDependencies3 = {
|
|
1681
1609
|
fileExists: fs4.existsSync,
|
|
1682
1610
|
writeFileSync: fs4.writeFileSync,
|
|
1683
1611
|
readFileSync: fs4.readFileSync,
|
|
@@ -1686,7 +1614,7 @@ var defaultDependencies4 = {
|
|
|
1686
1614
|
};
|
|
1687
1615
|
function registerInitCommand(program, dependencies = {}) {
|
|
1688
1616
|
const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync3, log, error } = {
|
|
1689
|
-
...
|
|
1617
|
+
...defaultDependencies3,
|
|
1690
1618
|
...dependencies
|
|
1691
1619
|
};
|
|
1692
1620
|
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) => {
|
|
@@ -1779,14 +1707,40 @@ var resolveFormat = (format, packageType) => {
|
|
|
1779
1707
|
return format;
|
|
1780
1708
|
};
|
|
1781
1709
|
var buildTemplate = (format) => {
|
|
1710
|
+
const configBody = `{
|
|
1711
|
+
// Filter which exports to analyze
|
|
1712
|
+
// include: ['MyClass', 'myFunction'],
|
|
1713
|
+
// exclude: ['internal*'],
|
|
1714
|
+
|
|
1715
|
+
// Check command thresholds
|
|
1716
|
+
check: {
|
|
1717
|
+
// Minimum documentation coverage percentage (0-100)
|
|
1718
|
+
// minCoverage: 80,
|
|
1719
|
+
|
|
1720
|
+
// Maximum drift percentage allowed (0-100)
|
|
1721
|
+
// maxDrift: 20,
|
|
1722
|
+
|
|
1723
|
+
// Example validation: 'presence' | 'typecheck' | 'run'
|
|
1724
|
+
// examples: 'typecheck',
|
|
1725
|
+
},
|
|
1726
|
+
|
|
1727
|
+
// Quality rule severities: 'error' | 'warn' | 'off'
|
|
1728
|
+
quality: {
|
|
1729
|
+
rules: {
|
|
1730
|
+
// 'has-description': 'warn',
|
|
1731
|
+
// 'has-params': 'off',
|
|
1732
|
+
// 'has-returns': 'off',
|
|
1733
|
+
// 'has-examples': 'off',
|
|
1734
|
+
// 'no-empty-returns': 'warn',
|
|
1735
|
+
// 'consistent-param-style': 'off',
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
}`;
|
|
1782
1739
|
if (format === "cjs") {
|
|
1783
1740
|
return [
|
|
1784
1741
|
"const { defineConfig } = require('@doccov/cli/config');",
|
|
1785
1742
|
"",
|
|
1786
|
-
|
|
1787
|
-
" include: [],",
|
|
1788
|
-
" exclude: [],",
|
|
1789
|
-
"});",
|
|
1743
|
+
`module.exports = defineConfig(${configBody});`,
|
|
1790
1744
|
""
|
|
1791
1745
|
].join(`
|
|
1792
1746
|
`);
|
|
@@ -1794,10 +1748,7 @@ var buildTemplate = (format) => {
|
|
|
1794
1748
|
return [
|
|
1795
1749
|
"import { defineConfig } from '@doccov/cli/config';",
|
|
1796
1750
|
"",
|
|
1797
|
-
|
|
1798
|
-
" include: [],",
|
|
1799
|
-
" exclude: [],",
|
|
1800
|
-
"});",
|
|
1751
|
+
`export default defineConfig(${configBody});`,
|
|
1801
1752
|
""
|
|
1802
1753
|
].join(`
|
|
1803
1754
|
`);
|
|
@@ -1809,9 +1760,9 @@ import * as fsPromises from "node:fs/promises";
|
|
|
1809
1760
|
import * as os from "node:os";
|
|
1810
1761
|
import * as path7 from "node:path";
|
|
1811
1762
|
import {
|
|
1812
|
-
DocCov as DocCov3,
|
|
1813
1763
|
buildCloneUrl,
|
|
1814
1764
|
buildDisplayUrl,
|
|
1765
|
+
DocCov as DocCov3,
|
|
1815
1766
|
detectBuildInfo,
|
|
1816
1767
|
detectEntryPoint,
|
|
1817
1768
|
detectMonorepo,
|
|
@@ -1823,6 +1774,10 @@ import {
|
|
|
1823
1774
|
NodeFileSystem as NodeFileSystem3,
|
|
1824
1775
|
parseGitHubUrl
|
|
1825
1776
|
} from "@doccov/sdk";
|
|
1777
|
+
import {
|
|
1778
|
+
DRIFT_CATEGORIES as DRIFT_CATEGORIES3,
|
|
1779
|
+
DRIFT_CATEGORY_LABELS as DRIFT_CATEGORY_LABELS2
|
|
1780
|
+
} from "@openpkg-ts/spec";
|
|
1826
1781
|
import chalk6 from "chalk";
|
|
1827
1782
|
import { simpleGit } from "simple-git";
|
|
1828
1783
|
|
|
@@ -1919,14 +1874,14 @@ async function generateBuildPlan(repoDir) {
|
|
|
1919
1874
|
}
|
|
1920
1875
|
|
|
1921
1876
|
// src/commands/scan.ts
|
|
1922
|
-
var
|
|
1877
|
+
var defaultDependencies4 = {
|
|
1923
1878
|
createDocCov: (options) => new DocCov3(options),
|
|
1924
1879
|
log: console.log,
|
|
1925
1880
|
error: console.error
|
|
1926
1881
|
};
|
|
1927
1882
|
function registerScanCommand(program, dependencies = {}) {
|
|
1928
1883
|
const { createDocCov, log, error } = {
|
|
1929
|
-
...
|
|
1884
|
+
...defaultDependencies4,
|
|
1930
1885
|
...dependencies
|
|
1931
1886
|
};
|
|
1932
1887
|
program.command("scan <url>").description("Analyze docs coverage for any public GitHub repository").option("--ref <branch>", "Branch or tag to analyze").option("--package <name>", "Target package in monorepo").option("--output <format>", "Output format: text or json", "text").option("--no-cleanup", "Keep cloned repo (for debugging)").option("--skip-install", "Skip dependency installation (faster, but may limit type resolution)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--save-spec <path>", "Save full OpenPkg spec to file").action(async (url, options) => {
|
|
@@ -2206,6 +2161,44 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2206
2161
|
}
|
|
2207
2162
|
});
|
|
2208
2163
|
}
|
|
2164
|
+
function categorizeDriftIssues(drift) {
|
|
2165
|
+
const byCategory = {
|
|
2166
|
+
structural: [],
|
|
2167
|
+
semantic: [],
|
|
2168
|
+
example: []
|
|
2169
|
+
};
|
|
2170
|
+
for (const d of drift) {
|
|
2171
|
+
const category = DRIFT_CATEGORIES3[d.type] ?? "semantic";
|
|
2172
|
+
byCategory[category].push(d);
|
|
2173
|
+
}
|
|
2174
|
+
return {
|
|
2175
|
+
summary: {
|
|
2176
|
+
total: drift.length,
|
|
2177
|
+
byCategory: {
|
|
2178
|
+
structural: byCategory.structural.length,
|
|
2179
|
+
semantic: byCategory.semantic.length,
|
|
2180
|
+
example: byCategory.example.length
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
byCategory
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function formatDriftSummary(summary) {
|
|
2187
|
+
if (summary.total === 0) {
|
|
2188
|
+
return "No drift detected";
|
|
2189
|
+
}
|
|
2190
|
+
const parts = [];
|
|
2191
|
+
if (summary.byCategory.structural > 0) {
|
|
2192
|
+
parts.push(`${summary.byCategory.structural} structural`);
|
|
2193
|
+
}
|
|
2194
|
+
if (summary.byCategory.semantic > 0) {
|
|
2195
|
+
parts.push(`${summary.byCategory.semantic} semantic`);
|
|
2196
|
+
}
|
|
2197
|
+
if (summary.byCategory.example > 0) {
|
|
2198
|
+
parts.push(`${summary.byCategory.example} example`);
|
|
2199
|
+
}
|
|
2200
|
+
return `${summary.total} issues (${parts.join(", ")})`;
|
|
2201
|
+
}
|
|
2209
2202
|
function printTextResult(result, log) {
|
|
2210
2203
|
log("");
|
|
2211
2204
|
log(chalk6.bold("DocCov Scan Results"));
|
|
@@ -2222,7 +2215,9 @@ function printTextResult(result, log) {
|
|
|
2222
2215
|
log(` ${result.exportCount} exports`);
|
|
2223
2216
|
log(` ${result.typeCount} types`);
|
|
2224
2217
|
log(` ${result.undocumented.length} undocumented`);
|
|
2225
|
-
|
|
2218
|
+
const categorized = categorizeDriftIssues(result.drift);
|
|
2219
|
+
const driftColor = result.driftCount > 0 ? chalk6.yellow : chalk6.green;
|
|
2220
|
+
log(` ${driftColor(formatDriftSummary(categorized.summary))}`);
|
|
2226
2221
|
if (result.undocumented.length > 0) {
|
|
2227
2222
|
log("");
|
|
2228
2223
|
log(chalk6.bold("Undocumented Exports"));
|
|
@@ -2236,24 +2231,189 @@ function printTextResult(result, log) {
|
|
|
2236
2231
|
if (result.drift.length > 0) {
|
|
2237
2232
|
log("");
|
|
2238
2233
|
log(chalk6.bold("Drift Issues"));
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2234
|
+
const categories = ["structural", "semantic", "example"];
|
|
2235
|
+
for (const category of categories) {
|
|
2236
|
+
const issues = categorized.byCategory[category];
|
|
2237
|
+
if (issues.length === 0)
|
|
2238
|
+
continue;
|
|
2239
|
+
const label = DRIFT_CATEGORY_LABELS2[category];
|
|
2240
|
+
log("");
|
|
2241
|
+
log(chalk6.dim(` ${label} (${issues.length})`));
|
|
2242
|
+
for (const d of issues.slice(0, 3)) {
|
|
2243
|
+
log(chalk6.red(` • ${d.export}: ${d.issue}`));
|
|
2244
|
+
}
|
|
2245
|
+
if (issues.length > 3) {
|
|
2246
|
+
log(chalk6.gray(` ... and ${issues.length - 3} more`));
|
|
2247
|
+
}
|
|
2244
2248
|
}
|
|
2245
2249
|
}
|
|
2246
2250
|
log("");
|
|
2247
2251
|
}
|
|
2248
2252
|
|
|
2253
|
+
// src/commands/spec.ts
|
|
2254
|
+
import * as fs7 from "node:fs";
|
|
2255
|
+
import * as path8 from "node:path";
|
|
2256
|
+
import { DocCov as DocCov4, NodeFileSystem as NodeFileSystem4, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2257
|
+
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2258
|
+
import chalk8 from "chalk";
|
|
2259
|
+
|
|
2260
|
+
// src/utils/filter-options.ts
|
|
2261
|
+
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
2262
|
+
import chalk7 from "chalk";
|
|
2263
|
+
var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
|
|
2264
|
+
var mergeFilterOptions = (config, cliOptions) => {
|
|
2265
|
+
const messages = [];
|
|
2266
|
+
if (config?.include) {
|
|
2267
|
+
messages.push(formatList("include filters from config", config.include));
|
|
2268
|
+
}
|
|
2269
|
+
if (config?.exclude) {
|
|
2270
|
+
messages.push(formatList("exclude filters from config", config.exclude));
|
|
2271
|
+
}
|
|
2272
|
+
if (cliOptions.include) {
|
|
2273
|
+
messages.push(formatList("apply include filters from CLI", cliOptions.include));
|
|
2274
|
+
}
|
|
2275
|
+
if (cliOptions.exclude) {
|
|
2276
|
+
messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
|
|
2277
|
+
}
|
|
2278
|
+
const resolved = mergeFilters(config, cliOptions);
|
|
2279
|
+
if (!resolved.include && !resolved.exclude) {
|
|
2280
|
+
return { messages };
|
|
2281
|
+
}
|
|
2282
|
+
const source = resolved.source === "override" ? "cli" : resolved.source;
|
|
2283
|
+
return {
|
|
2284
|
+
include: resolved.include,
|
|
2285
|
+
exclude: resolved.exclude,
|
|
2286
|
+
source,
|
|
2287
|
+
messages
|
|
2288
|
+
};
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
// src/commands/spec.ts
|
|
2292
|
+
var defaultDependencies5 = {
|
|
2293
|
+
createDocCov: (options) => new DocCov4(options),
|
|
2294
|
+
writeFileSync: fs7.writeFileSync,
|
|
2295
|
+
log: console.log,
|
|
2296
|
+
error: console.error
|
|
2297
|
+
};
|
|
2298
|
+
function getArrayLength(value) {
|
|
2299
|
+
return Array.isArray(value) ? value.length : 0;
|
|
2300
|
+
}
|
|
2301
|
+
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2302
|
+
const location = diagnostic.location;
|
|
2303
|
+
const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
|
|
2304
|
+
const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2305
|
+
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2306
|
+
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
2307
|
+
}
|
|
2308
|
+
function registerSpecCommand(program, dependencies = {}) {
|
|
2309
|
+
const { createDocCov, writeFileSync: writeFileSync5, log, error } = {
|
|
2310
|
+
...defaultDependencies5,
|
|
2311
|
+
...dependencies
|
|
2312
|
+
};
|
|
2313
|
+
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").action(async (entry, options) => {
|
|
2314
|
+
try {
|
|
2315
|
+
const fileSystem = new NodeFileSystem4(options.cwd);
|
|
2316
|
+
const resolved = await resolveTarget3(fileSystem, {
|
|
2317
|
+
cwd: options.cwd,
|
|
2318
|
+
package: options.package,
|
|
2319
|
+
entry
|
|
2320
|
+
});
|
|
2321
|
+
const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
|
|
2322
|
+
if (packageInfo) {
|
|
2323
|
+
log(chalk8.gray(`Found package at ${packageInfo.path}`));
|
|
2324
|
+
}
|
|
2325
|
+
if (!entry) {
|
|
2326
|
+
log(chalk8.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
|
|
2327
|
+
}
|
|
2328
|
+
let config = null;
|
|
2329
|
+
try {
|
|
2330
|
+
config = await loadDocCovConfig(targetDir);
|
|
2331
|
+
if (config?.filePath) {
|
|
2332
|
+
log(chalk8.gray(`Loaded configuration from ${path8.relative(targetDir, config.filePath)}`));
|
|
2333
|
+
}
|
|
2334
|
+
} catch (configError) {
|
|
2335
|
+
error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
2336
|
+
process.exit(1);
|
|
2337
|
+
}
|
|
2338
|
+
const cliFilters = {
|
|
2339
|
+
include: parseListFlag(options.include),
|
|
2340
|
+
exclude: parseListFlag(options.exclude)
|
|
2341
|
+
};
|
|
2342
|
+
const resolvedFilters = mergeFilterOptions(config, cliFilters);
|
|
2343
|
+
for (const message of resolvedFilters.messages) {
|
|
2344
|
+
log(chalk8.gray(`${message}`));
|
|
2345
|
+
}
|
|
2346
|
+
const resolveExternalTypes = !options.skipResolve;
|
|
2347
|
+
process.stdout.write(chalk8.cyan(`> Generating OpenPkg spec...
|
|
2348
|
+
`));
|
|
2349
|
+
let result;
|
|
2350
|
+
try {
|
|
2351
|
+
const doccov = createDocCov({
|
|
2352
|
+
resolveExternalTypes,
|
|
2353
|
+
maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
|
|
2354
|
+
useCache: options.cache !== false,
|
|
2355
|
+
cwd: options.cwd
|
|
2356
|
+
});
|
|
2357
|
+
const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
|
|
2358
|
+
filters: {
|
|
2359
|
+
include: resolvedFilters.include,
|
|
2360
|
+
exclude: resolvedFilters.exclude
|
|
2361
|
+
}
|
|
2362
|
+
} : {};
|
|
2363
|
+
result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
|
|
2364
|
+
if (result.fromCache) {
|
|
2365
|
+
process.stdout.write(chalk8.gray(`> Using cached spec
|
|
2366
|
+
`));
|
|
2367
|
+
} else {
|
|
2368
|
+
process.stdout.write(chalk8.green(`> Generated OpenPkg spec
|
|
2369
|
+
`));
|
|
2370
|
+
}
|
|
2371
|
+
} catch (generationError) {
|
|
2372
|
+
process.stdout.write(chalk8.red(`> Failed to generate spec
|
|
2373
|
+
`));
|
|
2374
|
+
throw generationError;
|
|
2375
|
+
}
|
|
2376
|
+
if (!result) {
|
|
2377
|
+
throw new Error("Failed to produce an OpenPkg spec.");
|
|
2378
|
+
}
|
|
2379
|
+
const normalized = normalize(result.spec);
|
|
2380
|
+
const validation = validateSpec(normalized);
|
|
2381
|
+
if (!validation.ok) {
|
|
2382
|
+
error(chalk8.red("Spec failed schema validation"));
|
|
2383
|
+
for (const err of validation.errors) {
|
|
2384
|
+
error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2385
|
+
}
|
|
2386
|
+
process.exit(1);
|
|
2387
|
+
}
|
|
2388
|
+
const outputPath = path8.resolve(process.cwd(), options.output);
|
|
2389
|
+
writeFileSync5(outputPath, JSON.stringify(normalized, null, 2));
|
|
2390
|
+
log(chalk8.green(`> Wrote ${options.output}`));
|
|
2391
|
+
log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
2392
|
+
log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
|
|
2393
|
+
if (options.showDiagnostics && result.diagnostics.length > 0) {
|
|
2394
|
+
log("");
|
|
2395
|
+
log(chalk8.bold("Diagnostics"));
|
|
2396
|
+
for (const diagnostic of result.diagnostics) {
|
|
2397
|
+
const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
|
|
2398
|
+
log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
} catch (commandError) {
|
|
2402
|
+
error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2249
2408
|
// src/cli.ts
|
|
2250
2409
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2251
|
-
var __dirname2 =
|
|
2252
|
-
var packageJson = JSON.parse(readFileSync4(
|
|
2410
|
+
var __dirname2 = path9.dirname(__filename2);
|
|
2411
|
+
var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
|
|
2253
2412
|
var program = new Command;
|
|
2254
2413
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2255
|
-
registerGenerateCommand(program);
|
|
2256
2414
|
registerCheckCommand(program);
|
|
2415
|
+
registerInfoCommand(program);
|
|
2416
|
+
registerSpecCommand(program);
|
|
2257
2417
|
registerDiffCommand(program);
|
|
2258
2418
|
registerInitCommand(program);
|
|
2259
2419
|
registerScanCommand(program);
|