@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 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
- if (!isInteractive()) {
764
- const symbol = state === "success" ? this.symbols.success : this.symbols.error;
765
- const colorFn = state === "success" ? colors.success : colors.error;
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.8";
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) {
@@ -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>;
@@ -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.25.9",
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.25.9",
52
- "@doccov/spec": "^0.24.1",
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",