@doccov/cli 0.25.11 → 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,6 +160,16 @@ 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";
@@ -1141,16 +1160,6 @@ function summary(options) {
1141
1160
  return new Summary(options);
1142
1161
  }
1143
1162
 
1144
- // src/commands/check/index.ts
1145
- import {
1146
- buildDocCovSpec,
1147
- DocCov,
1148
- NodeFileSystem,
1149
- parseExamplesFlag,
1150
- resolveTarget
1151
- } from "@doccov/sdk";
1152
- import chalk7 from "chalk";
1153
-
1154
1163
  // src/utils/filter-options.ts
1155
1164
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
1156
1165
  import chalk3 from "chalk";
@@ -1218,12 +1227,15 @@ import * as fs2 from "node:fs";
1218
1227
  import * as path3 from "node:path";
1219
1228
  import {
1220
1229
  applyEdits,
1230
+ applyForgottenExportFixes,
1221
1231
  categorizeDrifts,
1222
1232
  createSourceFile,
1223
1233
  findJSDocLocation,
1224
1234
  generateFixesForExport,
1235
+ generateForgottenExportFixes,
1225
1236
  mergeFixes,
1226
1237
  parseJSDocToPatch,
1238
+ previewForgottenExportFixes,
1227
1239
  serializeJSDoc
1228
1240
  } from "@doccov/sdk";
1229
1241
  import chalk5 from "chalk";
@@ -1296,12 +1308,12 @@ async function handleFixes(openpkg, doccov, options, deps) {
1296
1308
  const fixedDriftKeys = new Set;
1297
1309
  const allDrifts = collectDriftsFromExports(openpkg.exports ?? [], doccov);
1298
1310
  if (allDrifts.length === 0) {
1299
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1311
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1300
1312
  }
1301
1313
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1302
1314
  if (fixable.length === 0) {
1303
1315
  log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1304
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1316
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1305
1317
  }
1306
1318
  log("");
1307
1319
  log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
@@ -1330,11 +1342,11 @@ async function handleFixes(openpkg, doccov, options, deps) {
1330
1342
  editsByFile.set(edit.filePath, fileEdits);
1331
1343
  }
1332
1344
  if (edits.length === 0) {
1333
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1345
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1334
1346
  }
1335
1347
  if (isPreview) {
1336
1348
  displayPreview(editsByFile, targetDir, log);
1337
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1349
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1338
1350
  }
1339
1351
  const applyResult = await applyEdits(edits);
1340
1352
  if (applyResult.errors.length > 0) {
@@ -1353,9 +1365,71 @@ async function handleFixes(openpkg, doccov, options, deps) {
1353
1365
  return {
1354
1366
  fixedDriftKeys,
1355
1367
  editsApplied: totalFixes,
1356
- filesModified: applyResult.filesModified
1368
+ filesModified: applyResult.filesModified,
1369
+ forgottenExportsFixed: 0
1357
1370
  };
1358
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
+ }
1359
1433
  function generateEditForExport(exp, drifts, targetDir, log) {
1360
1434
  if (!exp.source?.file) {
1361
1435
  log(chalk5.gray(` Skipping ${exp.name}: no source location`));
@@ -1836,6 +1910,24 @@ function renderMarkdown(stats, options = {}) {
1836
1910
  lines.push("");
1837
1911
  }
1838
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
+ }
1839
1931
  lines.push("");
1840
1932
  lines.push("---");
1841
1933
  lines.push("*Generated by [DocCov](https://doccov.com)*");
@@ -2253,7 +2345,8 @@ function computeStats(openpkg, doccov) {
2253
2345
  exports: sortedExports,
2254
2346
  driftIssues,
2255
2347
  driftByCategory,
2256
- driftSummary
2348
+ driftSummary,
2349
+ apiSurface: doccov.apiSurface
2257
2350
  };
2258
2351
  }
2259
2352
  // src/reports/writer.ts
@@ -2298,23 +2391,32 @@ function writeReports(options) {
2298
2391
  function displayTextOutput(options, deps) {
2299
2392
  const {
2300
2393
  openpkg,
2394
+ doccov,
2301
2395
  coverageScore,
2302
2396
  minCoverage,
2303
2397
  maxDrift,
2398
+ minApiSurface,
2399
+ warnBelowApiSurface,
2304
2400
  driftExports,
2305
2401
  typecheckErrors,
2306
2402
  staleRefs,
2307
2403
  exampleResult,
2308
2404
  specWarnings,
2309
- specInfos
2405
+ specInfos,
2406
+ verbose
2310
2407
  } = options;
2311
2408
  const { log } = deps;
2312
2409
  const sym = getSymbols(supportsUnicode());
2313
2410
  const totalExportsForDrift = openpkg.exports?.length ?? 0;
2314
2411
  const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
2315
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;
2316
2416
  const coverageFailed = coverageScore < minCoverage;
2317
2417
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2418
+ const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2419
+ const apiSurfaceWarn = warnBelowApiSurface !== undefined && apiSurfaceScore < warnBelowApiSurface && !apiSurfaceFailed;
2318
2420
  const hasTypecheckErrors = typecheckErrors.length > 0;
2319
2421
  if (specWarnings.length > 0 || specInfos.length > 0) {
2320
2422
  log("");
@@ -2345,6 +2447,18 @@ function displayTextOutput(options, deps) {
2345
2447
  } else {
2346
2448
  summaryBuilder.addKeyValue("Drift", `${driftScore}%`);
2347
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
+ }
2348
2462
  if (exampleResult) {
2349
2463
  const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
2350
2464
  if (typecheckCount > 0) {
@@ -2378,15 +2492,48 @@ function displayTextOutput(options, deps) {
2378
2492
  log(colors.muted(` ... and ${staleRefs.length - 5} more`));
2379
2493
  }
2380
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
+ }
2381
2522
  log("");
2382
- const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
2523
+ const failed = coverageFailed || driftFailed || apiSurfaceFailed || hasTypecheckErrors || hasStaleRefs;
2383
2524
  if (!failed) {
2384
2525
  const thresholdParts = [];
2385
2526
  thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
2386
2527
  if (maxDrift !== undefined) {
2387
2528
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
2388
2529
  }
2530
+ if (minApiSurface !== undefined) {
2531
+ thresholdParts.push(`api-surface ${apiSurfaceScore}% ≥ ${minApiSurface}%`);
2532
+ }
2389
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
+ }
2390
2537
  return true;
2391
2538
  }
2392
2539
  if (coverageFailed) {
@@ -2395,6 +2542,9 @@ function displayTextOutput(options, deps) {
2395
2542
  if (driftFailed) {
2396
2543
  log(colors.error(`${sym.error} Drift ${driftScore}% exceeds maximum ${maxDrift}%`));
2397
2544
  }
2545
+ if (apiSurfaceFailed) {
2546
+ log(colors.error(`${sym.error} API Surface ${apiSurfaceScore}% below minimum ${minApiSurface}%`));
2547
+ }
2398
2548
  if (hasTypecheckErrors) {
2399
2549
  log(colors.error(`${sym.error} ${typecheckErrors.length} example type errors`));
2400
2550
  }
@@ -2413,6 +2563,7 @@ function handleNonTextOutput(options, deps) {
2413
2563
  coverageScore,
2414
2564
  minCoverage,
2415
2565
  maxDrift,
2566
+ minApiSurface,
2416
2567
  driftExports,
2417
2568
  typecheckErrors,
2418
2569
  limit,
@@ -2460,8 +2611,56 @@ function handleNonTextOutput(options, deps) {
2460
2611
  const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
2461
2612
  const coverageFailed = coverageScore < minCoverage;
2462
2613
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2614
+ const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
2615
+ const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2463
2616
  const hasTypecheckErrors = typecheckErrors.length > 0;
2464
- 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
+ }
2465
2664
  }
2466
2665
 
2467
2666
  // src/commands/check/validation.ts
@@ -2551,7 +2750,7 @@ function registerCheckCommand(program, dependencies = {}) {
2551
2750
  if (Number.isNaN(n) || n < 1)
2552
2751
  throw new Error("--max-type-depth must be a positive integer");
2553
2752
  return n;
2554
- }).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) => {
2555
2754
  try {
2556
2755
  const spin = spinner("Analyzing...");
2557
2756
  let validations = parseExamplesFlag(options.examples);
@@ -2578,6 +2777,11 @@ function registerCheckCommand(program, dependencies = {}) {
2578
2777
  const minCoverage = clampPercentage(minCoverageRaw);
2579
2778
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
2580
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 ?? [];
2581
2785
  const cliFilters = {
2582
2786
  include: undefined,
2583
2787
  exclude: undefined,
@@ -2604,7 +2808,9 @@ function registerCheckCommand(program, dependencies = {}) {
2604
2808
  const doccov = buildDocCovSpec({
2605
2809
  openpkg,
2606
2810
  openpkgPath: entryFile,
2607
- packagePath: targetDir
2811
+ packagePath: targetDir,
2812
+ forgottenExports: specResult.forgottenExports,
2813
+ apiSurfaceIgnore
2608
2814
  });
2609
2815
  const format = options.format ?? "text";
2610
2816
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
@@ -2636,12 +2842,23 @@ function registerCheckCommand(program, dependencies = {}) {
2636
2842
  const allDriftExports = [...collectDrift(openpkg.exports ?? [], doccov), ...runtimeDrifts];
2637
2843
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
2638
2844
  if (shouldFix && driftExports.length > 0) {
2639
- const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir }, { log, error });
2845
+ const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir, entryFile }, { log, error });
2640
2846
  if (!isPreview) {
2641
2847
  driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
2642
2848
  }
2643
2849
  }
2850
+ if (shouldFix && doccov.apiSurface?.forgotten.length) {
2851
+ await handleForgottenExportFixes(doccov, { isPreview, targetDir, entryFile }, { log, error });
2852
+ }
2644
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
+ }
2645
2862
  if (format !== "text") {
2646
2863
  const passed2 = handleNonTextOutput({
2647
2864
  format,
@@ -2650,6 +2867,7 @@ function registerCheckCommand(program, dependencies = {}) {
2650
2867
  coverageScore,
2651
2868
  minCoverage,
2652
2869
  maxDrift,
2870
+ minApiSurface,
2653
2871
  driftExports,
2654
2872
  typecheckErrors,
2655
2873
  limit: parseInt(options.limit, 10) || 20,
@@ -2668,12 +2886,15 @@ function registerCheckCommand(program, dependencies = {}) {
2668
2886
  coverageScore,
2669
2887
  minCoverage,
2670
2888
  maxDrift,
2889
+ minApiSurface,
2890
+ warnBelowApiSurface,
2671
2891
  driftExports,
2672
2892
  typecheckErrors,
2673
2893
  staleRefs,
2674
2894
  exampleResult,
2675
2895
  specWarnings,
2676
- specInfos
2896
+ specInfos,
2897
+ verbose: options.verbose ?? false
2677
2898
  }, { log });
2678
2899
  if (!passed) {
2679
2900
  process.exit(1);
@@ -3098,7 +3319,8 @@ function registerInfoCommand(program) {
3098
3319
  const doccov = buildDocCovSpec2({
3099
3320
  openpkg,
3100
3321
  openpkgPath: entryFile,
3101
- packagePath: targetDir
3322
+ packagePath: targetDir,
3323
+ forgottenExports: specResult.forgottenExports
3102
3324
  });
3103
3325
  const stats = computeStats(openpkg, doccov);
3104
3326
  spin.success("Analysis complete");
@@ -3315,7 +3537,7 @@ import { validateDocCovSpec } from "@doccov/spec";
3315
3537
  import { normalize, validateSpec } from "@openpkg-ts/spec";
3316
3538
  import chalk11 from "chalk";
3317
3539
  // package.json
3318
- var version = "0.25.9";
3540
+ var version = "0.25.11";
3319
3541
 
3320
3542
  // src/commands/spec.ts
3321
3543
  var defaultDependencies4 = {
@@ -3409,7 +3631,8 @@ function registerSpecCommand(program, dependencies = {}) {
3409
3631
  doccovSpec = buildDocCovSpec3({
3410
3632
  openpkgPath: "openpkg.json",
3411
3633
  openpkg: normalized,
3412
- packagePath: targetDir
3634
+ packagePath: targetDir,
3635
+ forgottenExports: result.forgottenExports
3413
3636
  });
3414
3637
  const doccovValidation = validateDocCovSpec(doccovSpec);
3415
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.11",
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.11",
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",