@doccov/cli 0.25.9 → 0.26.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 +260 -36
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +11 -2
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -25,10 +25,17 @@ var exampleModesSchema = z.union([
|
|
|
25
25
|
z.array(exampleModeSchema),
|
|
26
26
|
z.string()
|
|
27
27
|
]);
|
|
28
|
+
var apiSurfaceConfigSchema = z.object({
|
|
29
|
+
minCompleteness: z.number().min(0).max(100).optional(),
|
|
30
|
+
warnBelow: z.number().min(0).max(100).optional(),
|
|
31
|
+
ignore: z.array(z.string()).optional()
|
|
32
|
+
});
|
|
28
33
|
var checkConfigSchema = z.object({
|
|
29
34
|
examples: exampleModesSchema.optional(),
|
|
30
35
|
minCoverage: z.number().min(0).max(100).optional(),
|
|
31
|
-
maxDrift: z.number().min(0).max(100).optional()
|
|
36
|
+
maxDrift: z.number().min(0).max(100).optional(),
|
|
37
|
+
minApiSurface: z.number().min(0).max(100).optional(),
|
|
38
|
+
apiSurface: apiSurfaceConfigSchema.optional()
|
|
32
39
|
});
|
|
33
40
|
var docCovConfigSchema = z.object({
|
|
34
41
|
include: stringList.optional(),
|
|
@@ -64,7 +71,9 @@ var normalizeConfig = (input) => {
|
|
|
64
71
|
check = {
|
|
65
72
|
examples: input.check.examples,
|
|
66
73
|
minCoverage: input.check.minCoverage,
|
|
67
|
-
maxDrift: input.check.maxDrift
|
|
74
|
+
maxDrift: input.check.maxDrift,
|
|
75
|
+
minApiSurface: input.check.minApiSurface,
|
|
76
|
+
apiSurface: input.check.apiSurface
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
return {
|
|
@@ -151,9 +160,20 @@ import * as path10 from "node:path";
|
|
|
151
160
|
import { fileURLToPath } from "node:url";
|
|
152
161
|
import { Command } from "commander";
|
|
153
162
|
|
|
163
|
+
// src/commands/check/index.ts
|
|
164
|
+
import {
|
|
165
|
+
buildDocCovSpec,
|
|
166
|
+
DocCov,
|
|
167
|
+
NodeFileSystem,
|
|
168
|
+
parseExamplesFlag,
|
|
169
|
+
resolveTarget
|
|
170
|
+
} from "@doccov/sdk";
|
|
171
|
+
import chalk7 from "chalk";
|
|
172
|
+
|
|
154
173
|
// ../cli-utils/dist/index.js
|
|
155
174
|
import chalk from "chalk";
|
|
156
175
|
import chalk2 from "chalk";
|
|
176
|
+
import { Worker } from "node:worker_threads";
|
|
157
177
|
var colors = {
|
|
158
178
|
success: chalk.green,
|
|
159
179
|
error: chalk.red,
|
|
@@ -312,8 +332,8 @@ class MultiProgress {
|
|
|
312
332
|
hideCursor();
|
|
313
333
|
this.setupSignalHandler();
|
|
314
334
|
this.timer = setInterval(() => {
|
|
315
|
-
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
316
335
|
if (this.bars.size > 0 && [...this.bars.values()].some((b) => b.status === "active")) {
|
|
336
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
317
337
|
this.render();
|
|
318
338
|
}
|
|
319
339
|
}, this.options.spinnerInterval);
|
|
@@ -686,11 +706,13 @@ class Spinner {
|
|
|
686
706
|
symbols = getSymbols(supportsUnicode());
|
|
687
707
|
lastRenderedLines = 0;
|
|
688
708
|
sigintHandler = null;
|
|
709
|
+
animate;
|
|
689
710
|
constructor(options = {}) {
|
|
690
711
|
this.label = options.label ?? "";
|
|
691
712
|
this.detail = options.detail;
|
|
692
713
|
this.interval = options.interval ?? 80;
|
|
693
714
|
this.colorFn = spinnerColors[options.color ?? "cyan"];
|
|
715
|
+
this.animate = options.animate ?? true;
|
|
694
716
|
const style = options.style ?? "circle";
|
|
695
717
|
this.frames = supportsUnicode() ? FRAME_SETS[style] : ASCII_FRAME_SET;
|
|
696
718
|
}
|
|
@@ -702,7 +724,7 @@ class Spinner {
|
|
|
702
724
|
this.state = "spinning";
|
|
703
725
|
this.frameIndex = 0;
|
|
704
726
|
this.lastRenderedLines = 0;
|
|
705
|
-
if (!isInteractive()) {
|
|
727
|
+
if (!isInteractive() || !this.animate) {
|
|
706
728
|
console.log(`${this.symbols.bullet} ${this.label}`);
|
|
707
729
|
return this;
|
|
708
730
|
}
|
|
@@ -760,14 +782,12 @@ class Spinner {
|
|
|
760
782
|
this.timer = null;
|
|
761
783
|
}
|
|
762
784
|
this.state = state;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
785
|
+
const symbol = state === "success" ? this.symbols.success : this.symbols.error;
|
|
786
|
+
const colorFn = state === "success" ? colors.success : colors.error;
|
|
787
|
+
if (!isInteractive() || !this.animate) {
|
|
766
788
|
console.log(`${colorFn(symbol)} ${this.label}`);
|
|
767
789
|
} else {
|
|
768
790
|
this.clearOutput();
|
|
769
|
-
const symbol = state === "success" ? this.symbols.success : this.symbols.error;
|
|
770
|
-
const colorFn = state === "success" ? colors.success : colors.error;
|
|
771
791
|
process.stdout.write(`${colorFn(symbol)} ${this.label}
|
|
772
792
|
`);
|
|
773
793
|
}
|
|
@@ -843,8 +863,8 @@ class StepProgress {
|
|
|
843
863
|
this.setupSignalHandler();
|
|
844
864
|
this.render();
|
|
845
865
|
this.timer = setInterval(() => {
|
|
846
|
-
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
847
866
|
if (this.steps.some((s) => s.status === "active")) {
|
|
867
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
848
868
|
this.render();
|
|
849
869
|
}
|
|
850
870
|
}, this.spinnerInterval);
|
|
@@ -1140,16 +1160,6 @@ function summary(options) {
|
|
|
1140
1160
|
return new Summary(options);
|
|
1141
1161
|
}
|
|
1142
1162
|
|
|
1143
|
-
// src/commands/check/index.ts
|
|
1144
|
-
import {
|
|
1145
|
-
buildDocCovSpec,
|
|
1146
|
-
DocCov,
|
|
1147
|
-
NodeFileSystem,
|
|
1148
|
-
parseExamplesFlag,
|
|
1149
|
-
resolveTarget
|
|
1150
|
-
} from "@doccov/sdk";
|
|
1151
|
-
import chalk7 from "chalk";
|
|
1152
|
-
|
|
1153
1163
|
// src/utils/filter-options.ts
|
|
1154
1164
|
import { mergeFilters, parseListFlag } from "@doccov/sdk";
|
|
1155
1165
|
import chalk3 from "chalk";
|
|
@@ -1217,12 +1227,15 @@ import * as fs2 from "node:fs";
|
|
|
1217
1227
|
import * as path3 from "node:path";
|
|
1218
1228
|
import {
|
|
1219
1229
|
applyEdits,
|
|
1230
|
+
applyForgottenExportFixes,
|
|
1220
1231
|
categorizeDrifts,
|
|
1221
1232
|
createSourceFile,
|
|
1222
1233
|
findJSDocLocation,
|
|
1223
1234
|
generateFixesForExport,
|
|
1235
|
+
generateForgottenExportFixes,
|
|
1224
1236
|
mergeFixes,
|
|
1225
1237
|
parseJSDocToPatch,
|
|
1238
|
+
previewForgottenExportFixes,
|
|
1226
1239
|
serializeJSDoc
|
|
1227
1240
|
} from "@doccov/sdk";
|
|
1228
1241
|
import chalk5 from "chalk";
|
|
@@ -1295,12 +1308,12 @@ async function handleFixes(openpkg, doccov, options, deps) {
|
|
|
1295
1308
|
const fixedDriftKeys = new Set;
|
|
1296
1309
|
const allDrifts = collectDriftsFromExports(openpkg.exports ?? [], doccov);
|
|
1297
1310
|
if (allDrifts.length === 0) {
|
|
1298
|
-
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
1311
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
|
|
1299
1312
|
}
|
|
1300
1313
|
const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
|
|
1301
1314
|
if (fixable.length === 0) {
|
|
1302
1315
|
log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
|
|
1303
|
-
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
1316
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
|
|
1304
1317
|
}
|
|
1305
1318
|
log("");
|
|
1306
1319
|
log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
|
|
@@ -1329,11 +1342,11 @@ async function handleFixes(openpkg, doccov, options, deps) {
|
|
|
1329
1342
|
editsByFile.set(edit.filePath, fileEdits);
|
|
1330
1343
|
}
|
|
1331
1344
|
if (edits.length === 0) {
|
|
1332
|
-
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
1345
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
|
|
1333
1346
|
}
|
|
1334
1347
|
if (isPreview) {
|
|
1335
1348
|
displayPreview(editsByFile, targetDir, log);
|
|
1336
|
-
return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
|
|
1349
|
+
return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
|
|
1337
1350
|
}
|
|
1338
1351
|
const applyResult = await applyEdits(edits);
|
|
1339
1352
|
if (applyResult.errors.length > 0) {
|
|
@@ -1352,9 +1365,71 @@ async function handleFixes(openpkg, doccov, options, deps) {
|
|
|
1352
1365
|
return {
|
|
1353
1366
|
fixedDriftKeys,
|
|
1354
1367
|
editsApplied: totalFixes,
|
|
1355
|
-
filesModified: applyResult.filesModified
|
|
1368
|
+
filesModified: applyResult.filesModified,
|
|
1369
|
+
forgottenExportsFixed: 0
|
|
1356
1370
|
};
|
|
1357
1371
|
}
|
|
1372
|
+
async function handleForgottenExportFixes(doccov, options, deps) {
|
|
1373
|
+
const { isPreview, targetDir, entryFile } = options;
|
|
1374
|
+
const { log, error } = deps;
|
|
1375
|
+
if (!doccov.apiSurface || doccov.apiSurface.forgotten.length === 0) {
|
|
1376
|
+
return { fixesApplied: 0, filesModified: 0 };
|
|
1377
|
+
}
|
|
1378
|
+
const fixable = doccov.apiSurface.forgotten.filter((f) => !f.isExternal && f.fix);
|
|
1379
|
+
if (fixable.length === 0) {
|
|
1380
|
+
return { fixesApplied: 0, filesModified: 0 };
|
|
1381
|
+
}
|
|
1382
|
+
const fixes = generateForgottenExportFixes(doccov.apiSurface, {
|
|
1383
|
+
baseDir: targetDir,
|
|
1384
|
+
entryFile: entryFile ?? "src/index.ts"
|
|
1385
|
+
});
|
|
1386
|
+
if (fixes.length === 0) {
|
|
1387
|
+
return { fixesApplied: 0, filesModified: 0 };
|
|
1388
|
+
}
|
|
1389
|
+
log("");
|
|
1390
|
+
log(chalk5.bold(`Found ${fixes.length} forgotten export(s) to fix`));
|
|
1391
|
+
if (isPreview) {
|
|
1392
|
+
displayForgottenExportPreview(fixes, targetDir, log);
|
|
1393
|
+
return { fixesApplied: 0, filesModified: 0 };
|
|
1394
|
+
}
|
|
1395
|
+
const result = await applyForgottenExportFixes(fixes);
|
|
1396
|
+
if (result.errors.length > 0) {
|
|
1397
|
+
for (const err of result.errors) {
|
|
1398
|
+
error(chalk5.red(` ${err.file}: ${err.error}`));
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (result.fixesApplied > 0) {
|
|
1402
|
+
log("");
|
|
1403
|
+
log(chalk5.green(`✓ Added ${result.fixesApplied} export(s) to ${result.filesModified} file(s)`));
|
|
1404
|
+
const grouped = new Map;
|
|
1405
|
+
for (const fix of fixes) {
|
|
1406
|
+
const relativePath = path3.relative(targetDir, fix.targetFile);
|
|
1407
|
+
const types = grouped.get(relativePath) ?? [];
|
|
1408
|
+
types.push(fix.typeName);
|
|
1409
|
+
grouped.set(relativePath, types);
|
|
1410
|
+
}
|
|
1411
|
+
for (const [file, types] of grouped) {
|
|
1412
|
+
log(chalk5.dim(` ${file}: ${types.join(", ")}`));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return { fixesApplied: result.fixesApplied, filesModified: result.filesModified };
|
|
1416
|
+
}
|
|
1417
|
+
function displayForgottenExportPreview(fixes, targetDir, log) {
|
|
1418
|
+
log(chalk5.bold("Preview - forgotten exports that would be added:"));
|
|
1419
|
+
log("");
|
|
1420
|
+
const previews = previewForgottenExportFixes(fixes);
|
|
1421
|
+
for (const [filePath, preview] of previews) {
|
|
1422
|
+
const relativePath = path3.relative(targetDir, filePath);
|
|
1423
|
+
log(chalk5.cyan(`${relativePath}:${preview.insertLine + 1}`));
|
|
1424
|
+
log("");
|
|
1425
|
+
for (const stmt of preview.statements) {
|
|
1426
|
+
log(chalk5.green(` + ${stmt}`));
|
|
1427
|
+
}
|
|
1428
|
+
log("");
|
|
1429
|
+
}
|
|
1430
|
+
log(chalk5.yellow(`${fixes.length} export(s) would be added.`));
|
|
1431
|
+
log(chalk5.gray("Run with --fix to apply these changes."));
|
|
1432
|
+
}
|
|
1358
1433
|
function generateEditForExport(exp, drifts, targetDir, log) {
|
|
1359
1434
|
if (!exp.source?.file) {
|
|
1360
1435
|
log(chalk5.gray(` Skipping ${exp.name}: no source location`));
|
|
@@ -1835,6 +1910,24 @@ function renderMarkdown(stats, options = {}) {
|
|
|
1835
1910
|
lines.push("");
|
|
1836
1911
|
}
|
|
1837
1912
|
}
|
|
1913
|
+
if (stats.apiSurface && stats.apiSurface.forgotten.length > 0) {
|
|
1914
|
+
lines.push("");
|
|
1915
|
+
lines.push("## API Surface");
|
|
1916
|
+
lines.push("");
|
|
1917
|
+
lines.push(`**${stats.apiSurface.completeness}% complete** (${stats.apiSurface.forgotten.length} forgotten exports)`);
|
|
1918
|
+
lines.push("");
|
|
1919
|
+
lines.push("| Type | Defined In | Referenced By |");
|
|
1920
|
+
lines.push("|------|------------|---------------|");
|
|
1921
|
+
for (const f of stats.apiSurface.forgotten.slice(0, limit)) {
|
|
1922
|
+
const definedIn = f.definedIn ? `${f.definedIn.file}${f.definedIn.line ? `:${f.definedIn.line}` : ""}` : "-";
|
|
1923
|
+
const refs = f.referencedBy.slice(0, 2).map((r) => `${r.exportName} (${r.location})`).join(", ");
|
|
1924
|
+
const moreRefs = f.referencedBy.length > 2 ? ` +${f.referencedBy.length - 2}` : "";
|
|
1925
|
+
lines.push(`| \`${f.name}\` | ${definedIn} | ${refs}${moreRefs} |`);
|
|
1926
|
+
}
|
|
1927
|
+
if (stats.apiSurface.forgotten.length > limit) {
|
|
1928
|
+
lines.push(`| ... | | ${stats.apiSurface.forgotten.length - limit} more |`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1838
1931
|
lines.push("");
|
|
1839
1932
|
lines.push("---");
|
|
1840
1933
|
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
@@ -2252,7 +2345,8 @@ function computeStats(openpkg, doccov) {
|
|
|
2252
2345
|
exports: sortedExports,
|
|
2253
2346
|
driftIssues,
|
|
2254
2347
|
driftByCategory,
|
|
2255
|
-
driftSummary
|
|
2348
|
+
driftSummary,
|
|
2349
|
+
apiSurface: doccov.apiSurface
|
|
2256
2350
|
};
|
|
2257
2351
|
}
|
|
2258
2352
|
// src/reports/writer.ts
|
|
@@ -2297,23 +2391,32 @@ function writeReports(options) {
|
|
|
2297
2391
|
function displayTextOutput(options, deps) {
|
|
2298
2392
|
const {
|
|
2299
2393
|
openpkg,
|
|
2394
|
+
doccov,
|
|
2300
2395
|
coverageScore,
|
|
2301
2396
|
minCoverage,
|
|
2302
2397
|
maxDrift,
|
|
2398
|
+
minApiSurface,
|
|
2399
|
+
warnBelowApiSurface,
|
|
2303
2400
|
driftExports,
|
|
2304
2401
|
typecheckErrors,
|
|
2305
2402
|
staleRefs,
|
|
2306
2403
|
exampleResult,
|
|
2307
2404
|
specWarnings,
|
|
2308
|
-
specInfos
|
|
2405
|
+
specInfos,
|
|
2406
|
+
verbose
|
|
2309
2407
|
} = options;
|
|
2310
2408
|
const { log } = deps;
|
|
2311
2409
|
const sym = getSymbols(supportsUnicode());
|
|
2312
2410
|
const totalExportsForDrift = openpkg.exports?.length ?? 0;
|
|
2313
2411
|
const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
|
|
2314
2412
|
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
2413
|
+
const apiSurface = doccov.apiSurface;
|
|
2414
|
+
const apiSurfaceScore = apiSurface?.completeness ?? 100;
|
|
2415
|
+
const forgottenCount = apiSurface?.forgotten?.length ?? 0;
|
|
2315
2416
|
const coverageFailed = coverageScore < minCoverage;
|
|
2316
2417
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
2418
|
+
const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
|
|
2419
|
+
const apiSurfaceWarn = warnBelowApiSurface !== undefined && apiSurfaceScore < warnBelowApiSurface && !apiSurfaceFailed;
|
|
2317
2420
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
2318
2421
|
if (specWarnings.length > 0 || specInfos.length > 0) {
|
|
2319
2422
|
log("");
|
|
@@ -2344,6 +2447,18 @@ function displayTextOutput(options, deps) {
|
|
|
2344
2447
|
} else {
|
|
2345
2448
|
summaryBuilder.addKeyValue("Drift", `${driftScore}%`);
|
|
2346
2449
|
}
|
|
2450
|
+
if (forgottenCount > 0 || minApiSurface !== undefined || warnBelowApiSurface !== undefined) {
|
|
2451
|
+
const surfaceLabel = forgottenCount > 0 ? `${apiSurfaceScore}% (${forgottenCount} forgotten)` : `${apiSurfaceScore}%`;
|
|
2452
|
+
if (apiSurfaceFailed) {
|
|
2453
|
+
summaryBuilder.addKeyValue("API Surface", surfaceLabel, "fail");
|
|
2454
|
+
} else if (apiSurfaceWarn) {
|
|
2455
|
+
summaryBuilder.addKeyValue("API Surface", surfaceLabel, "warn");
|
|
2456
|
+
} else if (minApiSurface !== undefined) {
|
|
2457
|
+
summaryBuilder.addKeyValue("API Surface", surfaceLabel, "pass");
|
|
2458
|
+
} else {
|
|
2459
|
+
summaryBuilder.addKeyValue("API Surface", surfaceLabel, forgottenCount > 0 ? "warn" : undefined);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2347
2462
|
if (exampleResult) {
|
|
2348
2463
|
const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
|
|
2349
2464
|
if (typecheckCount > 0) {
|
|
@@ -2377,15 +2492,48 @@ function displayTextOutput(options, deps) {
|
|
|
2377
2492
|
log(colors.muted(` ... and ${staleRefs.length - 5} more`));
|
|
2378
2493
|
}
|
|
2379
2494
|
}
|
|
2495
|
+
if (verbose && forgottenCount > 0 && apiSurface?.forgotten) {
|
|
2496
|
+
log("");
|
|
2497
|
+
log(colors.bold(`Forgotten Exports (${forgottenCount})`));
|
|
2498
|
+
log("");
|
|
2499
|
+
for (const forgotten of apiSurface.forgotten.slice(0, 10)) {
|
|
2500
|
+
log(` ${colors.warning(forgotten.name)}`);
|
|
2501
|
+
if (forgotten.definedIn) {
|
|
2502
|
+
log(colors.muted(` Defined in: ${forgotten.definedIn.file}${forgotten.definedIn.line ? `:${forgotten.definedIn.line}` : ""}`));
|
|
2503
|
+
}
|
|
2504
|
+
if (forgotten.referencedBy.length > 0) {
|
|
2505
|
+
log(colors.muted(" Referenced by:"));
|
|
2506
|
+
for (const ref of forgotten.referencedBy.slice(0, 3)) {
|
|
2507
|
+
log(colors.muted(` - ${ref.exportName} (${ref.location})`));
|
|
2508
|
+
}
|
|
2509
|
+
if (forgotten.referencedBy.length > 3) {
|
|
2510
|
+
log(colors.muted(` ... and ${forgotten.referencedBy.length - 3} more`));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (forgotten.fix) {
|
|
2514
|
+
log(colors.info(` Fix: Add to ${forgotten.fix.targetFile}:`));
|
|
2515
|
+
log(colors.info(` ${forgotten.fix.exportStatement}`));
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (apiSurface.forgotten.length > 10) {
|
|
2519
|
+
log(colors.muted(` ... and ${apiSurface.forgotten.length - 10} more`));
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2380
2522
|
log("");
|
|
2381
|
-
const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
|
|
2523
|
+
const failed = coverageFailed || driftFailed || apiSurfaceFailed || hasTypecheckErrors || hasStaleRefs;
|
|
2382
2524
|
if (!failed) {
|
|
2383
2525
|
const thresholdParts = [];
|
|
2384
2526
|
thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
|
|
2385
2527
|
if (maxDrift !== undefined) {
|
|
2386
2528
|
thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
|
|
2387
2529
|
}
|
|
2530
|
+
if (minApiSurface !== undefined) {
|
|
2531
|
+
thresholdParts.push(`api-surface ${apiSurfaceScore}% ≥ ${minApiSurface}%`);
|
|
2532
|
+
}
|
|
2388
2533
|
log(colors.success(`${sym.success} Check passed (${thresholdParts.join(", ")})`));
|
|
2534
|
+
if (apiSurfaceWarn) {
|
|
2535
|
+
log(colors.warning(`${sym.warning} API Surface ${apiSurfaceScore}% below warning threshold ${warnBelowApiSurface}%`));
|
|
2536
|
+
}
|
|
2389
2537
|
return true;
|
|
2390
2538
|
}
|
|
2391
2539
|
if (coverageFailed) {
|
|
@@ -2394,6 +2542,9 @@ function displayTextOutput(options, deps) {
|
|
|
2394
2542
|
if (driftFailed) {
|
|
2395
2543
|
log(colors.error(`${sym.error} Drift ${driftScore}% exceeds maximum ${maxDrift}%`));
|
|
2396
2544
|
}
|
|
2545
|
+
if (apiSurfaceFailed) {
|
|
2546
|
+
log(colors.error(`${sym.error} API Surface ${apiSurfaceScore}% below minimum ${minApiSurface}%`));
|
|
2547
|
+
}
|
|
2397
2548
|
if (hasTypecheckErrors) {
|
|
2398
2549
|
log(colors.error(`${sym.error} ${typecheckErrors.length} example type errors`));
|
|
2399
2550
|
}
|
|
@@ -2412,6 +2563,7 @@ function handleNonTextOutput(options, deps) {
|
|
|
2412
2563
|
coverageScore,
|
|
2413
2564
|
minCoverage,
|
|
2414
2565
|
maxDrift,
|
|
2566
|
+
minApiSurface,
|
|
2415
2567
|
driftExports,
|
|
2416
2568
|
typecheckErrors,
|
|
2417
2569
|
limit,
|
|
@@ -2459,8 +2611,56 @@ function handleNonTextOutput(options, deps) {
|
|
|
2459
2611
|
const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
|
|
2460
2612
|
const coverageFailed = coverageScore < minCoverage;
|
|
2461
2613
|
const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
|
|
2614
|
+
const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
|
|
2615
|
+
const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
|
|
2462
2616
|
const hasTypecheckErrors = typecheckErrors.length > 0;
|
|
2463
|
-
return !(coverageFailed || driftFailed || hasTypecheckErrors);
|
|
2617
|
+
return !(coverageFailed || driftFailed || apiSurfaceFailed || hasTypecheckErrors);
|
|
2618
|
+
}
|
|
2619
|
+
function displayApiSurfaceOutput(doccov, deps) {
|
|
2620
|
+
const { log } = deps;
|
|
2621
|
+
const apiSurface = doccov.apiSurface;
|
|
2622
|
+
log("");
|
|
2623
|
+
log(colors.bold("API Surface Analysis"));
|
|
2624
|
+
log("");
|
|
2625
|
+
if (!apiSurface) {
|
|
2626
|
+
log(colors.muted("No API surface data available"));
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const sym = getSymbols(supportsUnicode());
|
|
2630
|
+
const summaryBuilder = summary({ keyWidth: 12 });
|
|
2631
|
+
summaryBuilder.addKeyValue("Referenced", apiSurface.totalReferenced);
|
|
2632
|
+
summaryBuilder.addKeyValue("Exported", apiSurface.exported);
|
|
2633
|
+
summaryBuilder.addKeyValue("Forgotten", apiSurface.forgotten.length);
|
|
2634
|
+
summaryBuilder.addKeyValue("Completeness", `${apiSurface.completeness}%`, apiSurface.forgotten.length > 0 ? "warn" : "pass");
|
|
2635
|
+
summaryBuilder.print();
|
|
2636
|
+
if (apiSurface.forgotten.length > 0) {
|
|
2637
|
+
log("");
|
|
2638
|
+
log(colors.bold(`Forgotten Exports (${apiSurface.forgotten.length})`));
|
|
2639
|
+
log("");
|
|
2640
|
+
for (const forgotten of apiSurface.forgotten) {
|
|
2641
|
+
log(` ${colors.warning(forgotten.name)}`);
|
|
2642
|
+
if (forgotten.definedIn) {
|
|
2643
|
+
log(colors.muted(` Defined in: ${forgotten.definedIn.file}${forgotten.definedIn.line ? `:${forgotten.definedIn.line}` : ""}`));
|
|
2644
|
+
}
|
|
2645
|
+
if (forgotten.referencedBy.length > 0) {
|
|
2646
|
+
log(colors.muted(" Referenced by:"));
|
|
2647
|
+
for (const ref of forgotten.referencedBy.slice(0, 5)) {
|
|
2648
|
+
log(colors.muted(` - ${ref.exportName} (${ref.location})`));
|
|
2649
|
+
}
|
|
2650
|
+
if (forgotten.referencedBy.length > 5) {
|
|
2651
|
+
log(colors.muted(` ... and ${forgotten.referencedBy.length - 5} more`));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (forgotten.fix) {
|
|
2655
|
+
log(colors.info(` Fix: Add to ${forgotten.fix.targetFile}:`));
|
|
2656
|
+
log(colors.info(` ${forgotten.fix.exportStatement}`));
|
|
2657
|
+
}
|
|
2658
|
+
log("");
|
|
2659
|
+
}
|
|
2660
|
+
} else {
|
|
2661
|
+
log("");
|
|
2662
|
+
log(colors.success(`${sym.success} All referenced types are exported`));
|
|
2663
|
+
}
|
|
2464
2664
|
}
|
|
2465
2665
|
|
|
2466
2666
|
// src/commands/check/validation.ts
|
|
@@ -2550,7 +2750,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2550
2750
|
if (Number.isNaN(n) || n < 1)
|
|
2551
2751
|
throw new Error("--max-type-depth must be a positive integer");
|
|
2552
2752
|
return n;
|
|
2553
|
-
}).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
|
|
2753
|
+
}).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--min-api-surface <percentage>", "Minimum API surface completeness percentage (0-100)", (value) => Number(value)).option("--api-surface", "Show only API surface / forgotten exports info").option("-v, --verbose", "Show detailed output including forgotten exports").action(async (entry, options) => {
|
|
2554
2754
|
try {
|
|
2555
2755
|
const spin = spinner("Analyzing...");
|
|
2556
2756
|
let validations = parseExamplesFlag(options.examples);
|
|
@@ -2577,6 +2777,11 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2577
2777
|
const minCoverage = clampPercentage(minCoverageRaw);
|
|
2578
2778
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
2579
2779
|
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
2780
|
+
const apiSurfaceConfig = config?.check?.apiSurface;
|
|
2781
|
+
const minApiSurfaceRaw = options.minApiSurface ?? apiSurfaceConfig?.minCompleteness ?? config?.check?.minApiSurface;
|
|
2782
|
+
const minApiSurface = minApiSurfaceRaw !== undefined ? clampPercentage(minApiSurfaceRaw) : undefined;
|
|
2783
|
+
const warnBelowApiSurface = apiSurfaceConfig?.warnBelow ? clampPercentage(apiSurfaceConfig.warnBelow) : undefined;
|
|
2784
|
+
const apiSurfaceIgnore = apiSurfaceConfig?.ignore ?? [];
|
|
2580
2785
|
const cliFilters = {
|
|
2581
2786
|
include: undefined,
|
|
2582
2787
|
exclude: undefined,
|
|
@@ -2603,7 +2808,9 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2603
2808
|
const doccov = buildDocCovSpec({
|
|
2604
2809
|
openpkg,
|
|
2605
2810
|
openpkgPath: entryFile,
|
|
2606
|
-
packagePath: targetDir
|
|
2811
|
+
packagePath: targetDir,
|
|
2812
|
+
forgottenExports: specResult.forgottenExports,
|
|
2813
|
+
apiSurfaceIgnore
|
|
2607
2814
|
});
|
|
2608
2815
|
const format = options.format ?? "text";
|
|
2609
2816
|
const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
|
|
@@ -2635,12 +2842,23 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2635
2842
|
const allDriftExports = [...collectDrift(openpkg.exports ?? [], doccov), ...runtimeDrifts];
|
|
2636
2843
|
let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
|
|
2637
2844
|
if (shouldFix && driftExports.length > 0) {
|
|
2638
|
-
const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir }, { log, error });
|
|
2845
|
+
const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir, entryFile }, { log, error });
|
|
2639
2846
|
if (!isPreview) {
|
|
2640
2847
|
driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
|
|
2641
2848
|
}
|
|
2642
2849
|
}
|
|
2850
|
+
if (shouldFix && doccov.apiSurface?.forgotten.length) {
|
|
2851
|
+
await handleForgottenExportFixes(doccov, { isPreview, targetDir, entryFile }, { log, error });
|
|
2852
|
+
}
|
|
2643
2853
|
spin.success("Analysis complete");
|
|
2854
|
+
if (options.apiSurface) {
|
|
2855
|
+
displayApiSurfaceOutput(doccov, { log });
|
|
2856
|
+
const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
|
|
2857
|
+
if (minApiSurface !== undefined && apiSurfaceScore < minApiSurface) {
|
|
2858
|
+
process.exit(1);
|
|
2859
|
+
}
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2644
2862
|
if (format !== "text") {
|
|
2645
2863
|
const passed2 = handleNonTextOutput({
|
|
2646
2864
|
format,
|
|
@@ -2649,6 +2867,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2649
2867
|
coverageScore,
|
|
2650
2868
|
minCoverage,
|
|
2651
2869
|
maxDrift,
|
|
2870
|
+
minApiSurface,
|
|
2652
2871
|
driftExports,
|
|
2653
2872
|
typecheckErrors,
|
|
2654
2873
|
limit: parseInt(options.limit, 10) || 20,
|
|
@@ -2667,12 +2886,15 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
2667
2886
|
coverageScore,
|
|
2668
2887
|
minCoverage,
|
|
2669
2888
|
maxDrift,
|
|
2889
|
+
minApiSurface,
|
|
2890
|
+
warnBelowApiSurface,
|
|
2670
2891
|
driftExports,
|
|
2671
2892
|
typecheckErrors,
|
|
2672
2893
|
staleRefs,
|
|
2673
2894
|
exampleResult,
|
|
2674
2895
|
specWarnings,
|
|
2675
|
-
specInfos
|
|
2896
|
+
specInfos,
|
|
2897
|
+
verbose: options.verbose ?? false
|
|
2676
2898
|
}, { log });
|
|
2677
2899
|
if (!passed) {
|
|
2678
2900
|
process.exit(1);
|
|
@@ -3097,7 +3319,8 @@ function registerInfoCommand(program) {
|
|
|
3097
3319
|
const doccov = buildDocCovSpec2({
|
|
3098
3320
|
openpkg,
|
|
3099
3321
|
openpkgPath: entryFile,
|
|
3100
|
-
packagePath: targetDir
|
|
3322
|
+
packagePath: targetDir,
|
|
3323
|
+
forgottenExports: specResult.forgottenExports
|
|
3101
3324
|
});
|
|
3102
3325
|
const stats = computeStats(openpkg, doccov);
|
|
3103
3326
|
spin.success("Analysis complete");
|
|
@@ -3314,7 +3537,7 @@ import { validateDocCovSpec } from "@doccov/spec";
|
|
|
3314
3537
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
3315
3538
|
import chalk11 from "chalk";
|
|
3316
3539
|
// package.json
|
|
3317
|
-
var version = "0.25.
|
|
3540
|
+
var version = "0.25.11";
|
|
3318
3541
|
|
|
3319
3542
|
// src/commands/spec.ts
|
|
3320
3543
|
var defaultDependencies4 = {
|
|
@@ -3408,7 +3631,8 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
3408
3631
|
doccovSpec = buildDocCovSpec3({
|
|
3409
3632
|
openpkgPath: "openpkg.json",
|
|
3410
3633
|
openpkg: normalized,
|
|
3411
|
-
packagePath: targetDir
|
|
3634
|
+
packagePath: targetDir,
|
|
3635
|
+
forgottenExports: result.forgottenExports
|
|
3412
3636
|
});
|
|
3413
3637
|
const doccovValidation = validateDocCovSpec(doccovSpec);
|
|
3414
3638
|
if (!doccovValidation.ok) {
|
package/dist/config/index.d.ts
CHANGED
|
@@ -13,12 +13,22 @@ declare const exampleModeSchema: z.ZodEnum<["presence", "typecheck", "run"]>;
|
|
|
13
13
|
/** Example validation modes - can be single, array, or comma-separated */
|
|
14
14
|
declare const exampleModesSchema: z.ZodUnion<[typeof exampleModeSchema, z.ZodArray<typeof exampleModeSchema>, z.ZodString]>;
|
|
15
15
|
/**
|
|
16
|
+
* API surface configuration schema.
|
|
17
|
+
*/
|
|
18
|
+
declare const apiSurfaceConfigSchema: z.ZodObject<{
|
|
19
|
+
minCompleteness: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
warnBelow: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
16
24
|
* Check command configuration schema.
|
|
17
25
|
*/
|
|
18
26
|
declare const checkConfigSchema: z.ZodObject<{
|
|
19
27
|
examples: z.ZodOptional<typeof exampleModesSchema>;
|
|
20
28
|
minCoverage: z.ZodOptional<z.ZodNumber>;
|
|
21
29
|
maxDrift: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
minApiSurface: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
apiSurface: z.ZodOptional<typeof apiSurfaceConfigSchema>;
|
|
22
32
|
}>;
|
|
23
33
|
declare const docCovConfigSchema: z.ZodObject<{
|
|
24
34
|
include: z.ZodOptional<typeof stringList>;
|
package/dist/config/index.js
CHANGED
|
@@ -23,10 +23,17 @@ var exampleModesSchema = z.union([
|
|
|
23
23
|
z.array(exampleModeSchema),
|
|
24
24
|
z.string()
|
|
25
25
|
]);
|
|
26
|
+
var apiSurfaceConfigSchema = z.object({
|
|
27
|
+
minCompleteness: z.number().min(0).max(100).optional(),
|
|
28
|
+
warnBelow: z.number().min(0).max(100).optional(),
|
|
29
|
+
ignore: z.array(z.string()).optional()
|
|
30
|
+
});
|
|
26
31
|
var checkConfigSchema = z.object({
|
|
27
32
|
examples: exampleModesSchema.optional(),
|
|
28
33
|
minCoverage: z.number().min(0).max(100).optional(),
|
|
29
|
-
maxDrift: z.number().min(0).max(100).optional()
|
|
34
|
+
maxDrift: z.number().min(0).max(100).optional(),
|
|
35
|
+
minApiSurface: z.number().min(0).max(100).optional(),
|
|
36
|
+
apiSurface: apiSurfaceConfigSchema.optional()
|
|
30
37
|
});
|
|
31
38
|
var docCovConfigSchema = z.object({
|
|
32
39
|
include: stringList.optional(),
|
|
@@ -62,7 +69,9 @@ var normalizeConfig = (input) => {
|
|
|
62
69
|
check = {
|
|
63
70
|
examples: input.check.examples,
|
|
64
71
|
minCoverage: input.check.minCoverage,
|
|
65
|
-
maxDrift: input.check.maxDrift
|
|
72
|
+
maxDrift: input.check.maxDrift,
|
|
73
|
+
minApiSurface: input.check.minApiSurface,
|
|
74
|
+
apiSurface: input.check.apiSurface
|
|
66
75
|
};
|
|
67
76
|
}
|
|
68
77
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@ai-sdk/anthropic": "^1.0.0",
|
|
50
50
|
"@ai-sdk/openai": "^1.0.0",
|
|
51
|
-
"@doccov/sdk": "^0.
|
|
52
|
-
"@doccov/spec": "^0.
|
|
51
|
+
"@doccov/sdk": "^0.26.0",
|
|
52
|
+
"@doccov/spec": "^0.26.0",
|
|
53
53
|
"@inquirer/prompts": "^7.8.0",
|
|
54
54
|
"@openpkg-ts/spec": "^0.12.0",
|
|
55
55
|
"ai": "^4.0.0",
|