@doccov/cli 0.25.11 → 0.27.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,18 @@ 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(),
35
+ minHealth: z.number().min(0).max(100).optional(),
30
36
  minCoverage: z.number().min(0).max(100).optional(),
31
- maxDrift: z.number().min(0).max(100).optional()
37
+ maxDrift: z.number().min(0).max(100).optional(),
38
+ minApiSurface: z.number().min(0).max(100).optional(),
39
+ apiSurface: apiSurfaceConfigSchema.optional()
32
40
  });
33
41
  var docCovConfigSchema = z.object({
34
42
  include: stringList.optional(),
@@ -63,8 +71,11 @@ var normalizeConfig = (input) => {
63
71
  if (input.check) {
64
72
  check = {
65
73
  examples: input.check.examples,
74
+ minHealth: input.check.minHealth,
66
75
  minCoverage: input.check.minCoverage,
67
- maxDrift: input.check.maxDrift
76
+ maxDrift: input.check.maxDrift,
77
+ minApiSurface: input.check.minApiSurface,
78
+ apiSurface: input.check.apiSurface
68
79
  };
69
80
  }
70
81
  return {
@@ -151,6 +162,16 @@ import * as path10 from "node:path";
151
162
  import { fileURLToPath } from "node:url";
152
163
  import { Command } from "commander";
153
164
 
165
+ // src/commands/check/index.ts
166
+ import {
167
+ buildDocCovSpec,
168
+ DocCov,
169
+ NodeFileSystem,
170
+ parseExamplesFlag,
171
+ resolveTarget
172
+ } from "@doccov/sdk";
173
+ import chalk7 from "chalk";
174
+
154
175
  // ../cli-utils/dist/index.js
155
176
  import chalk from "chalk";
156
177
  import chalk2 from "chalk";
@@ -1141,16 +1162,6 @@ function summary(options) {
1141
1162
  return new Summary(options);
1142
1163
  }
1143
1164
 
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
1165
  // src/utils/filter-options.ts
1155
1166
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
1156
1167
  import chalk3 from "chalk";
@@ -1218,12 +1229,15 @@ import * as fs2 from "node:fs";
1218
1229
  import * as path3 from "node:path";
1219
1230
  import {
1220
1231
  applyEdits,
1232
+ applyForgottenExportFixes,
1221
1233
  categorizeDrifts,
1222
1234
  createSourceFile,
1223
1235
  findJSDocLocation,
1224
1236
  generateFixesForExport,
1237
+ generateForgottenExportFixes,
1225
1238
  mergeFixes,
1226
1239
  parseJSDocToPatch,
1240
+ previewForgottenExportFixes,
1227
1241
  serializeJSDoc
1228
1242
  } from "@doccov/sdk";
1229
1243
  import chalk5 from "chalk";
@@ -1296,12 +1310,12 @@ async function handleFixes(openpkg, doccov, options, deps) {
1296
1310
  const fixedDriftKeys = new Set;
1297
1311
  const allDrifts = collectDriftsFromExports(openpkg.exports ?? [], doccov);
1298
1312
  if (allDrifts.length === 0) {
1299
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1313
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1300
1314
  }
1301
1315
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1302
1316
  if (fixable.length === 0) {
1303
1317
  log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1304
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1318
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1305
1319
  }
1306
1320
  log("");
1307
1321
  log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
@@ -1330,11 +1344,11 @@ async function handleFixes(openpkg, doccov, options, deps) {
1330
1344
  editsByFile.set(edit.filePath, fileEdits);
1331
1345
  }
1332
1346
  if (edits.length === 0) {
1333
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1347
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1334
1348
  }
1335
1349
  if (isPreview) {
1336
1350
  displayPreview(editsByFile, targetDir, log);
1337
- return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
1351
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0, forgottenExportsFixed: 0 };
1338
1352
  }
1339
1353
  const applyResult = await applyEdits(edits);
1340
1354
  if (applyResult.errors.length > 0) {
@@ -1350,12 +1364,78 @@ async function handleFixes(openpkg, doccov, options, deps) {
1350
1364
  const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
1351
1365
  log(chalk5.dim(` ${relativePath} (${fixCount} fixes)`));
1352
1366
  }
1367
+ if (options.healthScore !== undefined) {
1368
+ log("");
1369
+ log(chalk5.cyan("Run doccov check again to see updated health score"));
1370
+ }
1353
1371
  return {
1354
1372
  fixedDriftKeys,
1355
1373
  editsApplied: totalFixes,
1356
- filesModified: applyResult.filesModified
1374
+ filesModified: applyResult.filesModified,
1375
+ forgottenExportsFixed: 0
1357
1376
  };
1358
1377
  }
1378
+ async function handleForgottenExportFixes(doccov, options, deps) {
1379
+ const { isPreview, targetDir, entryFile } = options;
1380
+ const { log, error } = deps;
1381
+ if (!doccov.apiSurface || doccov.apiSurface.forgotten.length === 0) {
1382
+ return { fixesApplied: 0, filesModified: 0 };
1383
+ }
1384
+ const fixable = doccov.apiSurface.forgotten.filter((f) => !f.isExternal && f.fix);
1385
+ if (fixable.length === 0) {
1386
+ return { fixesApplied: 0, filesModified: 0 };
1387
+ }
1388
+ const fixes = generateForgottenExportFixes(doccov.apiSurface, {
1389
+ baseDir: targetDir,
1390
+ entryFile: entryFile ?? "src/index.ts"
1391
+ });
1392
+ if (fixes.length === 0) {
1393
+ return { fixesApplied: 0, filesModified: 0 };
1394
+ }
1395
+ log("");
1396
+ log(chalk5.bold(`Found ${fixes.length} forgotten export(s) to fix`));
1397
+ if (isPreview) {
1398
+ displayForgottenExportPreview(fixes, targetDir, log);
1399
+ return { fixesApplied: 0, filesModified: 0 };
1400
+ }
1401
+ const result = await applyForgottenExportFixes(fixes);
1402
+ if (result.errors.length > 0) {
1403
+ for (const err of result.errors) {
1404
+ error(chalk5.red(` ${err.file}: ${err.error}`));
1405
+ }
1406
+ }
1407
+ if (result.fixesApplied > 0) {
1408
+ log("");
1409
+ log(chalk5.green(`✓ Added ${result.fixesApplied} export(s) to ${result.filesModified} file(s)`));
1410
+ const grouped = new Map;
1411
+ for (const fix of fixes) {
1412
+ const relativePath = path3.relative(targetDir, fix.targetFile);
1413
+ const types = grouped.get(relativePath) ?? [];
1414
+ types.push(fix.typeName);
1415
+ grouped.set(relativePath, types);
1416
+ }
1417
+ for (const [file, types] of grouped) {
1418
+ log(chalk5.dim(` ${file}: ${types.join(", ")}`));
1419
+ }
1420
+ }
1421
+ return { fixesApplied: result.fixesApplied, filesModified: result.filesModified };
1422
+ }
1423
+ function displayForgottenExportPreview(fixes, targetDir, log) {
1424
+ log(chalk5.bold("Preview - forgotten exports that would be added:"));
1425
+ log("");
1426
+ const previews = previewForgottenExportFixes(fixes);
1427
+ for (const [filePath, preview] of previews) {
1428
+ const relativePath = path3.relative(targetDir, filePath);
1429
+ log(chalk5.cyan(`${relativePath}:${preview.insertLine + 1}`));
1430
+ log("");
1431
+ for (const stmt of preview.statements) {
1432
+ log(chalk5.green(` + ${stmt}`));
1433
+ }
1434
+ log("");
1435
+ }
1436
+ log(chalk5.yellow(`${fixes.length} export(s) would be added.`));
1437
+ log(chalk5.gray("Run with --fix to apply these changes."));
1438
+ }
1359
1439
  function generateEditForExport(exp, drifts, targetDir, log) {
1360
1440
  if (!exp.source?.file) {
1361
1441
  log(chalk5.gray(` Skipping ${exp.name}: no source location`));
@@ -1726,9 +1806,31 @@ function renderGithubSummary(stats, options = {}) {
1726
1806
  const coverageScore = options.coverageScore ?? stats.coverageScore;
1727
1807
  const driftCount = options.driftCount ?? stats.driftCount;
1728
1808
  const qualityIssues = options.qualityIssues ?? 0;
1729
- let output = `## Documentation Coverage: ${coverageScore}%
1809
+ let output = "";
1810
+ if (stats.health) {
1811
+ const h = stats.health;
1812
+ const status = h.score >= 80 ? "✅" : h.score >= 50 ? "⚠️" : "❌";
1813
+ output += `## ${status} Documentation Health: ${h.score}%
1730
1814
 
1731
1815
  `;
1816
+ output += `| Metric | Score | Details |
1817
+ |--------|-------|---------|
1818
+ `;
1819
+ output += `| Completeness | ${h.completeness.score}% | ${h.completeness.total - h.completeness.documented} missing docs |
1820
+ `;
1821
+ output += `| Accuracy | ${h.accuracy.score}% | ${h.accuracy.issues} drift issues |
1822
+ `;
1823
+ if (h.examples) {
1824
+ output += `| Examples | ${h.examples.score}% | ${h.examples.passed}/${h.examples.total} passed |
1825
+ `;
1826
+ }
1827
+ output += `
1828
+ `;
1829
+ } else {
1830
+ output += `## Documentation Coverage: ${coverageScore}%
1831
+
1832
+ `;
1833
+ }
1732
1834
  output += `| Metric | Value |
1733
1835
  |--------|-------|
1734
1836
  `;
@@ -1740,10 +1842,12 @@ function renderGithubSummary(stats, options = {}) {
1740
1842
  `;
1741
1843
  output += `| Quality Issues | ${qualityIssues} |
1742
1844
  `;
1743
- const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
1744
- output += `
1845
+ if (!stats.health) {
1846
+ const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
1847
+ output += `
1745
1848
  ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "needs improvement" : "failing"}
1746
1849
  `;
1850
+ }
1747
1851
  return output;
1748
1852
  }
1749
1853
  // src/reports/markdown.ts
@@ -1757,6 +1861,19 @@ function renderMarkdown(stats, options = {}) {
1757
1861
  const lines = [];
1758
1862
  lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
1759
1863
  lines.push("");
1864
+ if (stats.health) {
1865
+ const h = stats.health;
1866
+ lines.push(`## Documentation Health: ${h.score}%`);
1867
+ lines.push("");
1868
+ lines.push("| Metric | Score | Details |");
1869
+ lines.push("|--------|-------|---------|");
1870
+ lines.push(`| Completeness | ${h.completeness.score}% | ${h.completeness.total - h.completeness.documented} missing docs |`);
1871
+ lines.push(`| Accuracy | ${h.accuracy.score}% | ${h.accuracy.issues} drift issues |`);
1872
+ if (h.examples) {
1873
+ lines.push(`| Examples | ${h.examples.score}% | ${h.examples.passed}/${h.examples.total} passed |`);
1874
+ }
1875
+ lines.push("");
1876
+ }
1760
1877
  lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar2(stats.coverageScore)}\``);
1761
1878
  lines.push("");
1762
1879
  lines.push("| Metric | Value |");
@@ -1836,6 +1953,24 @@ function renderMarkdown(stats, options = {}) {
1836
1953
  lines.push("");
1837
1954
  }
1838
1955
  }
1956
+ if (stats.apiSurface && stats.apiSurface.forgotten.length > 0) {
1957
+ lines.push("");
1958
+ lines.push("## API Surface");
1959
+ lines.push("");
1960
+ lines.push(`**${stats.apiSurface.completeness}% complete** (${stats.apiSurface.forgotten.length} forgotten exports)`);
1961
+ lines.push("");
1962
+ lines.push("| Type | Defined In | Referenced By |");
1963
+ lines.push("|------|------------|---------------|");
1964
+ for (const f of stats.apiSurface.forgotten.slice(0, limit)) {
1965
+ const definedIn = f.definedIn ? `${f.definedIn.file}${f.definedIn.line ? `:${f.definedIn.line}` : ""}` : "-";
1966
+ const refs = f.referencedBy.slice(0, 2).map((r) => `${r.exportName} (${r.location})`).join(", ");
1967
+ const moreRefs = f.referencedBy.length > 2 ? ` +${f.referencedBy.length - 2}` : "";
1968
+ lines.push(`| \`${f.name}\` | ${definedIn} | ${refs}${moreRefs} |`);
1969
+ }
1970
+ if (stats.apiSurface.forgotten.length > limit) {
1971
+ lines.push(`| ... | | ${stats.apiSurface.forgotten.length - limit} more |`);
1972
+ }
1973
+ }
1839
1974
  lines.push("");
1840
1975
  lines.push("---");
1841
1976
  lines.push("*Generated by [DocCov](https://doccov.com)*");
@@ -2253,7 +2388,9 @@ function computeStats(openpkg, doccov) {
2253
2388
  exports: sortedExports,
2254
2389
  driftIssues,
2255
2390
  driftByCategory,
2256
- driftSummary
2391
+ driftSummary,
2392
+ apiSurface: doccov.apiSurface,
2393
+ health: doccov.summary.health
2257
2394
  };
2258
2395
  }
2259
2396
  // src/reports/writer.ts
@@ -2295,26 +2432,97 @@ function writeReports(options) {
2295
2432
  return results;
2296
2433
  }
2297
2434
  // src/commands/check/output.ts
2435
+ function getHealthStatus(score) {
2436
+ if (score >= 80)
2437
+ return "pass";
2438
+ if (score >= 60)
2439
+ return "warn";
2440
+ return "fail";
2441
+ }
2442
+ function getHealthColor(status) {
2443
+ switch (status) {
2444
+ case "pass":
2445
+ return colors.success;
2446
+ case "warn":
2447
+ return colors.warning;
2448
+ case "fail":
2449
+ return colors.error;
2450
+ }
2451
+ }
2452
+ function displayHealthTree(health, log) {
2453
+ const tree = supportsUnicode() ? { branch: "├─", corner: "└─" } : { branch: "|-", corner: "\\-" };
2454
+ const missingTotal = Object.values(health.completeness.missing).reduce((a, b) => a + b, 0);
2455
+ const completenessLabel = missingTotal > 0 ? `${health.completeness.score}% (${missingTotal} missing docs)` : `${health.completeness.score}%`;
2456
+ const completenessColor = getHealthColor(getHealthStatus(health.completeness.score));
2457
+ log(`${tree.branch} ${colors.muted("completeness")} ${completenessColor(completenessLabel)}`);
2458
+ const accuracyLabel = health.accuracy.issues > 0 ? `${health.accuracy.score}% (${health.accuracy.issues} drift issues${health.accuracy.fixable > 0 ? `, ${health.accuracy.fixable} fixable` : ""})` : `${health.accuracy.score}%`;
2459
+ const accuracyColor = getHealthColor(getHealthStatus(health.accuracy.score));
2460
+ const lastBranch = !health.examples ? tree.corner : tree.branch;
2461
+ log(`${lastBranch} ${colors.muted("accuracy")} ${accuracyColor(accuracyLabel)}`);
2462
+ if (health.examples) {
2463
+ const examplesLabel = health.examples.failed > 0 ? `${health.examples.score}% (${health.examples.failed} failed)` : `${health.examples.score}%`;
2464
+ const examplesColor = getHealthColor(getHealthStatus(health.examples.score));
2465
+ log(`${tree.corner} ${colors.muted("examples")} ${examplesColor(examplesLabel)}`);
2466
+ }
2467
+ }
2468
+ function displayHealthVerbose(health, log) {
2469
+ const tree = supportsUnicode() ? { branch: "├─", corner: "└─" } : { branch: "|-", corner: "\\-" };
2470
+ log(colors.bold("COMPLETENESS") + ` ${health.completeness.score}%`);
2471
+ const missingRules = Object.entries(health.completeness.missing);
2472
+ for (let i = 0;i < missingRules.length; i++) {
2473
+ const [rule, count] = missingRules[i];
2474
+ const isLast = i === missingRules.length - 1;
2475
+ const prefix2 = isLast ? tree.corner : tree.branch;
2476
+ const label = count > 0 ? `(${count} missing)` : "";
2477
+ const pct = health.completeness.total > 0 ? Math.round((health.completeness.total - count) / health.completeness.total * 100) : 100;
2478
+ const color = getHealthColor(getHealthStatus(pct));
2479
+ log(`${prefix2} ${colors.muted(rule.padEnd(12))} ${color(`${pct}%`)} ${colors.muted(label)}`);
2480
+ }
2481
+ log("");
2482
+ log(colors.bold("ACCURACY") + ` ${health.accuracy.score}% ${colors.muted(`(${health.accuracy.issues} issues)`)}`);
2483
+ const categories = Object.entries(health.accuracy.byCategory);
2484
+ for (let i = 0;i < categories.length; i++) {
2485
+ const [category, count] = categories[i];
2486
+ const isLast = i === categories.length - 1;
2487
+ const prefix2 = isLast ? tree.corner : tree.branch;
2488
+ log(`${prefix2} ${colors.muted(category.padEnd(12))} ${count}`);
2489
+ }
2490
+ if (health.examples) {
2491
+ log("");
2492
+ log(colors.bold("EXAMPLES") + ` ${health.examples.score}%`);
2493
+ log(`${tree.branch} ${colors.muted("passed".padEnd(12))} ${health.examples.passed}`);
2494
+ log(`${tree.corner} ${colors.muted("failed".padEnd(12))} ${health.examples.failed}`);
2495
+ }
2496
+ }
2298
2497
  function displayTextOutput(options, deps) {
2299
2498
  const {
2300
2499
  openpkg,
2500
+ doccov,
2301
2501
  coverageScore,
2302
- minCoverage,
2303
- maxDrift,
2502
+ minHealth,
2503
+ minApiSurface,
2504
+ warnBelowApiSurface,
2304
2505
  driftExports,
2305
2506
  typecheckErrors,
2306
2507
  staleRefs,
2307
2508
  exampleResult,
2308
2509
  specWarnings,
2309
- specInfos
2510
+ specInfos,
2511
+ verbose
2310
2512
  } = options;
2311
2513
  const { log } = deps;
2312
2514
  const sym = getSymbols(supportsUnicode());
2515
+ const health = doccov.summary.health;
2516
+ const healthScore = health?.score ?? coverageScore;
2313
2517
  const totalExportsForDrift = openpkg.exports?.length ?? 0;
2314
2518
  const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
2315
2519
  const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
2316
- const coverageFailed = coverageScore < minCoverage;
2317
- const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2520
+ const apiSurface = doccov.apiSurface;
2521
+ const apiSurfaceScore = apiSurface?.completeness ?? 100;
2522
+ const forgottenCount = apiSurface?.forgotten?.length ?? 0;
2523
+ const healthFailed = healthScore < minHealth;
2524
+ const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2525
+ const apiSurfaceWarn = warnBelowApiSurface !== undefined && apiSurfaceScore < warnBelowApiSurface && !apiSurfaceFailed;
2318
2526
  const hasTypecheckErrors = typecheckErrors.length > 0;
2319
2527
  if (specWarnings.length > 0 || specInfos.length > 0) {
2320
2528
  log("");
@@ -2333,31 +2541,35 @@ function displayTextOutput(options, deps) {
2333
2541
  }
2334
2542
  const pkgName = openpkg.meta?.name ?? "unknown";
2335
2543
  const pkgVersion = openpkg.meta?.version ?? "";
2336
- const totalExports = openpkg.exports?.length ?? 0;
2337
2544
  log("");
2338
- log(colors.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
2545
+ log(colors.bold(`${pkgName}${pkgVersion ? ` v${pkgVersion}` : ""}`));
2339
2546
  log("");
2340
- const summaryBuilder = summary({ keyWidth: 10 });
2341
- summaryBuilder.addKeyValue("Exports", totalExports);
2342
- summaryBuilder.addKeyValue("Coverage", `${coverageScore}%`, coverageFailed ? "fail" : "pass");
2343
- if (maxDrift !== undefined) {
2344
- summaryBuilder.addKeyValue("Drift", `${driftScore}%`, driftFailed ? "fail" : "pass");
2547
+ const hasStaleRefs = staleRefs.length > 0;
2548
+ if (health) {
2549
+ const healthColor = getHealthColor(getHealthStatus(health.score));
2550
+ log(`${colors.muted("Health")} ${healthColor(`${health.score}%`)}`);
2551
+ if (verbose) {
2552
+ log("");
2553
+ displayHealthVerbose(health, log);
2554
+ } else {
2555
+ displayHealthTree(health, log);
2556
+ }
2345
2557
  } else {
2558
+ const summaryBuilder = summary({ keyWidth: 10 });
2559
+ summaryBuilder.addKeyValue("Exports", openpkg.exports?.length ?? 0);
2560
+ summaryBuilder.addKeyValue("Coverage", `${coverageScore}%`);
2346
2561
  summaryBuilder.addKeyValue("Drift", `${driftScore}%`);
2562
+ summaryBuilder.print();
2347
2563
  }
2348
- if (exampleResult) {
2349
- const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
2350
- if (typecheckCount > 0) {
2351
- summaryBuilder.addKeyValue("Examples", `${typecheckCount} type error(s)`, "warn");
2352
- } else {
2353
- summaryBuilder.addKeyValue("Examples", "validated", "pass");
2354
- }
2564
+ log("");
2565
+ if (forgottenCount > 0 || minApiSurface !== undefined || warnBelowApiSurface !== undefined) {
2566
+ const surfaceLabel = forgottenCount > 0 ? `${apiSurfaceScore}% (${forgottenCount} forgotten)` : `${apiSurfaceScore}%`;
2567
+ const surfaceColor = apiSurfaceFailed ? colors.error : apiSurfaceWarn || forgottenCount > 0 ? colors.warning : colors.success;
2568
+ log(`${colors.muted("API Surface")} ${surfaceColor(surfaceLabel)}`);
2355
2569
  }
2356
- const hasStaleRefs = staleRefs.length > 0;
2357
2570
  if (hasStaleRefs) {
2358
- summaryBuilder.addKeyValue("Docs", `${staleRefs.length} stale ref(s)`, "warn");
2571
+ log(`${colors.muted("Stale refs")} ${colors.warning(`${staleRefs.length} found`)}`);
2359
2572
  }
2360
- summaryBuilder.print();
2361
2573
  if (hasTypecheckErrors) {
2362
2574
  log("");
2363
2575
  for (const err of typecheckErrors.slice(0, 5)) {
@@ -2378,22 +2590,55 @@ function displayTextOutput(options, deps) {
2378
2590
  log(colors.muted(` ... and ${staleRefs.length - 5} more`));
2379
2591
  }
2380
2592
  }
2593
+ if (verbose && forgottenCount > 0 && apiSurface?.forgotten) {
2594
+ log("");
2595
+ log(colors.bold(`Forgotten Exports (${forgottenCount})`));
2596
+ log("");
2597
+ for (const forgotten of apiSurface.forgotten.slice(0, 10)) {
2598
+ log(` ${colors.warning(forgotten.name)}`);
2599
+ if (forgotten.definedIn) {
2600
+ log(colors.muted(` Defined in: ${forgotten.definedIn.file}${forgotten.definedIn.line ? `:${forgotten.definedIn.line}` : ""}`));
2601
+ }
2602
+ if (forgotten.referencedBy.length > 0) {
2603
+ log(colors.muted(" Referenced by:"));
2604
+ for (const ref of forgotten.referencedBy.slice(0, 3)) {
2605
+ log(colors.muted(` - ${ref.exportName} (${ref.location})`));
2606
+ }
2607
+ if (forgotten.referencedBy.length > 3) {
2608
+ log(colors.muted(` ... and ${forgotten.referencedBy.length - 3} more`));
2609
+ }
2610
+ }
2611
+ if (forgotten.fix) {
2612
+ log(colors.info(` Fix: Add to ${forgotten.fix.targetFile}:`));
2613
+ log(colors.info(` ${forgotten.fix.exportStatement}`));
2614
+ }
2615
+ }
2616
+ if (apiSurface.forgotten.length > 10) {
2617
+ log(colors.muted(` ... and ${apiSurface.forgotten.length - 10} more`));
2618
+ }
2619
+ }
2381
2620
  log("");
2382
- const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
2621
+ const failed = healthFailed || apiSurfaceFailed || hasTypecheckErrors || hasStaleRefs;
2383
2622
  if (!failed) {
2384
2623
  const thresholdParts = [];
2385
- thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
2386
- if (maxDrift !== undefined) {
2387
- thresholdParts.push(`drift ${driftScore}% ${maxDrift}%`);
2624
+ thresholdParts.push(`health ${healthScore}% ≥ ${minHealth}%`);
2625
+ if (minApiSurface !== undefined) {
2626
+ thresholdParts.push(`api-surface ${apiSurfaceScore}% ${minApiSurface}%`);
2388
2627
  }
2389
2628
  log(colors.success(`${sym.success} Check passed (${thresholdParts.join(", ")})`));
2629
+ if (apiSurfaceWarn) {
2630
+ log(colors.warning(`${sym.warning} API Surface ${apiSurfaceScore}% below warning threshold ${warnBelowApiSurface}%`));
2631
+ }
2632
+ if (!verbose && health) {
2633
+ log(colors.muted("Use --verbose for detailed breakdown"));
2634
+ }
2390
2635
  return true;
2391
2636
  }
2392
- if (coverageFailed) {
2393
- log(colors.error(`${sym.error} Coverage ${coverageScore}% below minimum ${minCoverage}%`));
2637
+ if (healthFailed) {
2638
+ log(colors.error(`${sym.error} Health ${healthScore}% below minimum ${minHealth}%`));
2394
2639
  }
2395
- if (driftFailed) {
2396
- log(colors.error(`${sym.error} Drift ${driftScore}% exceeds maximum ${maxDrift}%`));
2640
+ if (apiSurfaceFailed) {
2641
+ log(colors.error(`${sym.error} API Surface ${apiSurfaceScore}% below minimum ${minApiSurface}%`));
2397
2642
  }
2398
2643
  if (hasTypecheckErrors) {
2399
2644
  log(colors.error(`${sym.error} ${typecheckErrors.length} example type errors`));
@@ -2401,8 +2646,10 @@ function displayTextOutput(options, deps) {
2401
2646
  if (hasStaleRefs) {
2402
2647
  log(colors.error(`${sym.error} ${staleRefs.length} stale references in docs`));
2403
2648
  }
2404
- log("");
2405
- log(colors.muted("Use --format json or --format markdown for detailed reports"));
2649
+ if (health && health.accuracy.fixable > 0) {
2650
+ log("");
2651
+ log(colors.muted(`Use --fix to auto-fix ${health.accuracy.fixable} drift issue(s)`));
2652
+ }
2406
2653
  return false;
2407
2654
  }
2408
2655
  function handleNonTextOutput(options, deps) {
@@ -2411,8 +2658,8 @@ function handleNonTextOutput(options, deps) {
2411
2658
  openpkg,
2412
2659
  doccov,
2413
2660
  coverageScore,
2414
- minCoverage,
2415
- maxDrift,
2661
+ minHealth,
2662
+ minApiSurface,
2416
2663
  driftExports,
2417
2664
  typecheckErrors,
2418
2665
  limit,
@@ -2424,6 +2671,7 @@ function handleNonTextOutput(options, deps) {
2424
2671
  const stats = computeStats(openpkg, doccov);
2425
2672
  const report = generateReportFromDocCov(openpkg, doccov);
2426
2673
  const jsonContent = JSON.stringify(report, null, 2);
2674
+ const healthScore = doccov.summary.health?.score ?? coverageScore;
2427
2675
  let formatContent;
2428
2676
  switch (format) {
2429
2677
  case "json":
@@ -2455,13 +2703,57 @@ function handleNonTextOutput(options, deps) {
2455
2703
  cwd
2456
2704
  });
2457
2705
  }
2458
- const totalExportsForDrift = openpkg.exports?.length ?? 0;
2459
- const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
2460
- const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
2461
- const coverageFailed = coverageScore < minCoverage;
2462
- const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2706
+ const healthFailed = healthScore < minHealth;
2707
+ const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
2708
+ const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2463
2709
  const hasTypecheckErrors = typecheckErrors.length > 0;
2464
- return !(coverageFailed || driftFailed || hasTypecheckErrors);
2710
+ return !(healthFailed || apiSurfaceFailed || hasTypecheckErrors);
2711
+ }
2712
+ function displayApiSurfaceOutput(doccov, deps) {
2713
+ const { log } = deps;
2714
+ const apiSurface = doccov.apiSurface;
2715
+ log("");
2716
+ log(colors.bold("API Surface Analysis"));
2717
+ log("");
2718
+ if (!apiSurface) {
2719
+ log(colors.muted("No API surface data available"));
2720
+ return;
2721
+ }
2722
+ const sym = getSymbols(supportsUnicode());
2723
+ const summaryBuilder = summary({ keyWidth: 12 });
2724
+ summaryBuilder.addKeyValue("Referenced", apiSurface.totalReferenced);
2725
+ summaryBuilder.addKeyValue("Exported", apiSurface.exported);
2726
+ summaryBuilder.addKeyValue("Forgotten", apiSurface.forgotten.length);
2727
+ summaryBuilder.addKeyValue("Completeness", `${apiSurface.completeness}%`, apiSurface.forgotten.length > 0 ? "warn" : "pass");
2728
+ summaryBuilder.print();
2729
+ if (apiSurface.forgotten.length > 0) {
2730
+ log("");
2731
+ log(colors.bold(`Forgotten Exports (${apiSurface.forgotten.length})`));
2732
+ log("");
2733
+ for (const forgotten of apiSurface.forgotten) {
2734
+ log(` ${colors.warning(forgotten.name)}`);
2735
+ if (forgotten.definedIn) {
2736
+ log(colors.muted(` Defined in: ${forgotten.definedIn.file}${forgotten.definedIn.line ? `:${forgotten.definedIn.line}` : ""}`));
2737
+ }
2738
+ if (forgotten.referencedBy.length > 0) {
2739
+ log(colors.muted(" Referenced by:"));
2740
+ for (const ref of forgotten.referencedBy.slice(0, 5)) {
2741
+ log(colors.muted(` - ${ref.exportName} (${ref.location})`));
2742
+ }
2743
+ if (forgotten.referencedBy.length > 5) {
2744
+ log(colors.muted(` ... and ${forgotten.referencedBy.length - 5} more`));
2745
+ }
2746
+ }
2747
+ if (forgotten.fix) {
2748
+ log(colors.info(` Fix: Add to ${forgotten.fix.targetFile}:`));
2749
+ log(colors.info(` ${forgotten.fix.exportStatement}`));
2750
+ }
2751
+ log("");
2752
+ }
2753
+ } else {
2754
+ log("");
2755
+ log(colors.success(`${sym.success} All referenced types are exported`));
2756
+ }
2465
2757
  }
2466
2758
 
2467
2759
  // src/commands/check/validation.ts
@@ -2546,12 +2838,12 @@ function registerCheckCommand(program, dependencies = {}) {
2546
2838
  ...defaultDependencies,
2547
2839
  ...dependencies
2548
2840
  };
2549
- 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("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--preview", "Preview fixes with diff output (implies --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)", (value) => {
2841
+ 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-health <percentage>", "Minimum health score (0-100)", (value) => Number(value)).option("--min-coverage <percentage>", "[deprecated] Use --min-health instead", (value) => Number(value)).option("--max-drift <percentage>", "[deprecated] Use --min-health instead", (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("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--preview", "Preview fixes with diff output (implies --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)", (value) => {
2550
2842
  const n = parseInt(value, 10);
2551
2843
  if (Number.isNaN(n) || n < 1)
2552
2844
  throw new Error("--max-type-depth must be a positive integer");
2553
2845
  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) => {
2846
+ }).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
2847
  try {
2556
2848
  const spin = spinner("Analyzing...");
2557
2849
  let validations = parseExamplesFlag(options.examples);
@@ -2573,11 +2865,30 @@ function registerCheckCommand(program, dependencies = {}) {
2573
2865
  }
2574
2866
  hasExamples = validations.length > 0;
2575
2867
  }
2576
- const DEFAULT_MIN_COVERAGE = 80;
2577
- const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage ?? DEFAULT_MIN_COVERAGE;
2868
+ if (options.minCoverage !== undefined) {
2869
+ log(chalk7.yellow("Warning: --min-coverage is deprecated. Use --min-health instead."));
2870
+ }
2871
+ if (options.maxDrift !== undefined) {
2872
+ log(chalk7.yellow("Warning: --max-drift is deprecated. Use --min-health instead."));
2873
+ }
2874
+ if (config?.check?.minCoverage !== undefined) {
2875
+ log(chalk7.yellow("Warning: config.check.minCoverage is deprecated. Use minHealth."));
2876
+ }
2877
+ if (config?.check?.maxDrift !== undefined) {
2878
+ log(chalk7.yellow("Warning: config.check.maxDrift is deprecated. Use minHealth."));
2879
+ }
2880
+ const DEFAULT_MIN_HEALTH = 80;
2881
+ const minHealthRaw = options.minHealth ?? config?.check?.minHealth ?? DEFAULT_MIN_HEALTH;
2882
+ const minHealth = clampPercentage(minHealthRaw);
2883
+ const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage ?? DEFAULT_MIN_HEALTH;
2578
2884
  const minCoverage = clampPercentage(minCoverageRaw);
2579
2885
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
2580
2886
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
2887
+ const apiSurfaceConfig = config?.check?.apiSurface;
2888
+ const minApiSurfaceRaw = options.minApiSurface ?? apiSurfaceConfig?.minCompleteness ?? config?.check?.minApiSurface;
2889
+ const minApiSurface = minApiSurfaceRaw !== undefined ? clampPercentage(minApiSurfaceRaw) : undefined;
2890
+ const warnBelowApiSurface = apiSurfaceConfig?.warnBelow ? clampPercentage(apiSurfaceConfig.warnBelow) : undefined;
2891
+ const apiSurfaceIgnore = apiSurfaceConfig?.ignore ?? [];
2581
2892
  const cliFilters = {
2582
2893
  include: undefined,
2583
2894
  exclude: undefined,
@@ -2604,7 +2915,9 @@ function registerCheckCommand(program, dependencies = {}) {
2604
2915
  const doccov = buildDocCovSpec({
2605
2916
  openpkg,
2606
2917
  openpkgPath: entryFile,
2607
- packagePath: targetDir
2918
+ packagePath: targetDir,
2919
+ forgottenExports: specResult.forgottenExports,
2920
+ apiSurfaceIgnore
2608
2921
  });
2609
2922
  const format = options.format ?? "text";
2610
2923
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
@@ -2635,21 +2948,33 @@ function registerCheckCommand(program, dependencies = {}) {
2635
2948
  const coverageScore = doccov.summary.score;
2636
2949
  const allDriftExports = [...collectDrift(openpkg.exports ?? [], doccov), ...runtimeDrifts];
2637
2950
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
2951
+ const healthScore = doccov.summary.health?.score ?? coverageScore;
2638
2952
  if (shouldFix && driftExports.length > 0) {
2639
- const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir }, { log, error });
2953
+ const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir, entryFile, healthScore }, { log, error });
2640
2954
  if (!isPreview) {
2641
2955
  driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
2642
2956
  }
2643
2957
  }
2958
+ if (shouldFix && doccov.apiSurface?.forgotten.length) {
2959
+ await handleForgottenExportFixes(doccov, { isPreview, targetDir, entryFile }, { log, error });
2960
+ }
2644
2961
  spin.success("Analysis complete");
2962
+ if (options.apiSurface) {
2963
+ displayApiSurfaceOutput(doccov, { log });
2964
+ const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
2965
+ if (minApiSurface !== undefined && apiSurfaceScore < minApiSurface) {
2966
+ process.exit(1);
2967
+ }
2968
+ return;
2969
+ }
2645
2970
  if (format !== "text") {
2646
2971
  const passed2 = handleNonTextOutput({
2647
2972
  format,
2648
2973
  openpkg,
2649
2974
  doccov,
2650
2975
  coverageScore,
2651
- minCoverage,
2652
- maxDrift,
2976
+ minHealth,
2977
+ minApiSurface,
2653
2978
  driftExports,
2654
2979
  typecheckErrors,
2655
2980
  limit: parseInt(options.limit, 10) || 20,
@@ -2666,14 +2991,16 @@ function registerCheckCommand(program, dependencies = {}) {
2666
2991
  openpkg,
2667
2992
  doccov,
2668
2993
  coverageScore,
2669
- minCoverage,
2670
- maxDrift,
2994
+ minHealth,
2995
+ minApiSurface,
2996
+ warnBelowApiSurface,
2671
2997
  driftExports,
2672
2998
  typecheckErrors,
2673
2999
  staleRefs,
2674
3000
  exampleResult,
2675
3001
  specWarnings,
2676
- specInfos
3002
+ specInfos,
3003
+ verbose: options.verbose ?? false
2677
3004
  }, { log });
2678
3005
  if (!passed) {
2679
3006
  process.exit(1);
@@ -3098,7 +3425,8 @@ function registerInfoCommand(program) {
3098
3425
  const doccov = buildDocCovSpec2({
3099
3426
  openpkg,
3100
3427
  openpkgPath: entryFile,
3101
- packagePath: targetDir
3428
+ packagePath: targetDir,
3429
+ forgottenExports: specResult.forgottenExports
3102
3430
  });
3103
3431
  const stats = computeStats(openpkg, doccov);
3104
3432
  spin.success("Analysis complete");
@@ -3312,12 +3640,11 @@ import {
3312
3640
  resolveTarget as resolveTarget3
3313
3641
  } from "@doccov/sdk";
3314
3642
  import { validateDocCovSpec } from "@doccov/spec";
3315
- import { normalize, validateSpec } from "@openpkg-ts/spec";
3643
+ import {
3644
+ normalize,
3645
+ validateSpec
3646
+ } from "@openpkg-ts/spec";
3316
3647
  import chalk11 from "chalk";
3317
- // package.json
3318
- var version = "0.25.9";
3319
-
3320
- // src/commands/spec.ts
3321
3648
  var defaultDependencies4 = {
3322
3649
  createDocCov: (options) => new DocCov3(options),
3323
3650
  writeFileSync: fs6.writeFileSync,
@@ -3371,24 +3698,13 @@ function registerSpecCommand(program, dependencies = {}) {
3371
3698
  cwd: options.cwd,
3372
3699
  schemaExtraction: options.runtime ? "hybrid" : "static"
3373
3700
  });
3374
- const generationInput = {
3375
- entryPoint: path8.relative(targetDir, entryFile),
3376
- entryPointSource: entryPointInfo.source,
3377
- isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
3378
- generatorName: "@doccov/cli",
3379
- generatorVersion: version,
3380
- packageManager: packageInfo?.packageManager,
3381
- isMonorepo: resolved.isMonorepo,
3382
- targetPackage: packageInfo?.name
3383
- };
3384
3701
  const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
3385
3702
  filters: {
3386
3703
  include: resolvedFilters.include,
3387
3704
  exclude: resolvedFilters.exclude,
3388
3705
  visibility: resolvedFilters.visibility
3389
- },
3390
- generationInput
3391
- } : { generationInput };
3706
+ }
3707
+ } : {};
3392
3708
  const result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
3393
3709
  if (!result) {
3394
3710
  spin.fail("Generation failed");
@@ -3409,7 +3725,8 @@ function registerSpecCommand(program, dependencies = {}) {
3409
3725
  doccovSpec = buildDocCovSpec3({
3410
3726
  openpkgPath: "openpkg.json",
3411
3727
  openpkg: normalized,
3412
- packagePath: targetDir
3728
+ packagePath: targetDir,
3729
+ forgottenExports: result.forgottenExports
3413
3730
  });
3414
3731
  const doccovValidation = validateDocCovSpec(doccovSpec);
3415
3732
  if (!doccovValidation.ok) {
@@ -3444,7 +3761,11 @@ function registerSpecCommand(program, dependencies = {}) {
3444
3761
  log(chalk11.gray(` ${getArrayLength(normalized.types)} types`));
3445
3762
  }
3446
3763
  }
3447
- const schemaExtraction = normalized.generation?.analysis?.schemaExtraction;
3764
+ const isFullGenerationInfo = (gen) => {
3765
+ return gen !== undefined && "analysis" in gen && "environment" in gen;
3766
+ };
3767
+ const fullGen = isFullGenerationInfo(normalized.generation) ? normalized.generation : undefined;
3768
+ const schemaExtraction = fullGen?.analysis?.schemaExtraction;
3448
3769
  if (options.runtime && (!schemaExtraction?.runtimeCount || schemaExtraction.runtimeCount === 0)) {
3449
3770
  const pm = await detectPackageManager(fileSystem);
3450
3771
  const buildCmd = pm.name === "npm" ? "npm run build" : `${pm.name} run build`;
@@ -3452,21 +3773,20 @@ function registerSpecCommand(program, dependencies = {}) {
3452
3773
  log(chalk11.yellow("⚠ Runtime extraction requested but no schemas extracted."));
3453
3774
  log(chalk11.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
3454
3775
  }
3455
- if (options.verbose && normalized.generation) {
3456
- const gen = normalized.generation;
3776
+ if (options.verbose && fullGen) {
3457
3777
  log("");
3458
3778
  log(chalk11.bold("Generation Info"));
3459
- log(chalk11.gray(` Timestamp: ${gen.timestamp}`));
3460
- log(chalk11.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
3461
- log(chalk11.gray(` Entry point: ${gen.analysis.entryPoint}`));
3462
- log(chalk11.gray(` Detected via: ${gen.analysis.entryPointSource}`));
3463
- log(chalk11.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
3464
- log(chalk11.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
3465
- if (gen.analysis.maxTypeDepth) {
3466
- log(chalk11.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
3779
+ log(chalk11.gray(` Timestamp: ${fullGen.timestamp}`));
3780
+ log(chalk11.gray(` Generator: ${fullGen.generator.name}@${fullGen.generator.version}`));
3781
+ log(chalk11.gray(` Entry point: ${fullGen.analysis.entryPoint}`));
3782
+ log(chalk11.gray(` Detected via: ${fullGen.analysis.entryPointSource}`));
3783
+ log(chalk11.gray(` Declaration only: ${fullGen.analysis.isDeclarationOnly ? "yes" : "no"}`));
3784
+ log(chalk11.gray(` External types: ${fullGen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
3785
+ if (fullGen.analysis.maxTypeDepth) {
3786
+ log(chalk11.gray(` Max type depth: ${fullGen.analysis.maxTypeDepth}`));
3467
3787
  }
3468
- if (gen.analysis.schemaExtraction) {
3469
- const se = gen.analysis.schemaExtraction;
3788
+ if (fullGen.analysis.schemaExtraction) {
3789
+ const se = fullGen.analysis.schemaExtraction;
3470
3790
  log(chalk11.gray(` Schema extraction: ${se.method}`));
3471
3791
  if (se.runtimeCount) {
3472
3792
  log(chalk11.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
@@ -3474,20 +3794,20 @@ function registerSpecCommand(program, dependencies = {}) {
3474
3794
  }
3475
3795
  log("");
3476
3796
  log(chalk11.bold("Environment"));
3477
- log(chalk11.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
3478
- if (gen.environment.packageManager) {
3479
- log(chalk11.gray(` Package manager: ${gen.environment.packageManager}`));
3797
+ log(chalk11.gray(` node_modules: ${fullGen.environment.hasNodeModules ? "found" : "not found"}`));
3798
+ if (fullGen.environment.packageManager) {
3799
+ log(chalk11.gray(` Package manager: ${fullGen.environment.packageManager}`));
3480
3800
  }
3481
- if (gen.environment.isMonorepo) {
3801
+ if (fullGen.environment.isMonorepo) {
3482
3802
  log(chalk11.gray(` Monorepo: yes`));
3483
3803
  }
3484
- if (gen.environment.targetPackage) {
3485
- log(chalk11.gray(` Target package: ${gen.environment.targetPackage}`));
3804
+ if (fullGen.environment.targetPackage) {
3805
+ log(chalk11.gray(` Target package: ${fullGen.environment.targetPackage}`));
3486
3806
  }
3487
- if (gen.issues.length > 0) {
3807
+ if (fullGen.issues.length > 0) {
3488
3808
  log("");
3489
3809
  log(chalk11.bold("Issues"));
3490
- for (const issue of gen.issues) {
3810
+ for (const issue of fullGen.issues) {
3491
3811
  const prefix2 = issue.severity === "error" ? chalk11.red(">") : issue.severity === "warning" ? chalk11.yellow(">") : chalk11.cyan(">");
3492
3812
  log(`${prefix2} [${issue.code}] ${issue.message}`);
3493
3813
  if (issue.suggestion) {
@@ -3548,9 +3868,9 @@ function getColorForScore(score) {
3548
3868
  function formatSnapshot(snapshot) {
3549
3869
  const color = getColorForScore(snapshot.coverageScore);
3550
3870
  const date = formatDate(snapshot.timestamp);
3551
- const version2 = snapshot.version ? ` v${snapshot.version}` : "";
3871
+ const version = snapshot.version ? ` v${snapshot.version}` : "";
3552
3872
  const commit = snapshot.commit ? chalk12.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
3553
- return `${chalk12.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
3873
+ return `${chalk12.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version}${commit}`;
3554
3874
  }
3555
3875
  function formatWeekDate(timestamp) {
3556
3876
  const date = new Date(timestamp);
@@ -13,12 +13,23 @@ 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>;
28
+ minHealth: z.ZodOptional<z.ZodNumber>;
20
29
  minCoverage: z.ZodOptional<z.ZodNumber>;
21
30
  maxDrift: z.ZodOptional<z.ZodNumber>;
31
+ minApiSurface: z.ZodOptional<z.ZodNumber>;
32
+ apiSurface: z.ZodOptional<typeof apiSurfaceConfigSchema>;
22
33
  }>;
23
34
  declare const docCovConfigSchema: z.ZodObject<{
24
35
  include: z.ZodOptional<typeof stringList>;
@@ -23,10 +23,18 @@ 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(),
33
+ minHealth: z.number().min(0).max(100).optional(),
28
34
  minCoverage: z.number().min(0).max(100).optional(),
29
- maxDrift: z.number().min(0).max(100).optional()
35
+ maxDrift: z.number().min(0).max(100).optional(),
36
+ minApiSurface: z.number().min(0).max(100).optional(),
37
+ apiSurface: apiSurfaceConfigSchema.optional()
30
38
  });
31
39
  var docCovConfigSchema = z.object({
32
40
  include: stringList.optional(),
@@ -61,8 +69,11 @@ var normalizeConfig = (input) => {
61
69
  if (input.check) {
62
70
  check = {
63
71
  examples: input.check.examples,
72
+ minHealth: input.check.minHealth,
64
73
  minCoverage: input.check.minCoverage,
65
- maxDrift: input.check.maxDrift
74
+ maxDrift: input.check.maxDrift,
75
+ minApiSurface: input.check.minApiSurface,
76
+ apiSurface: input.check.apiSurface
66
77
  };
67
78
  }
68
79
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.25.11",
3
+ "version": "0.27.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.27.0",
52
+ "@doccov/spec": "^0.27.0",
53
53
  "@inquirer/prompts": "^7.8.0",
54
54
  "@openpkg-ts/spec": "^0.12.0",
55
55
  "ai": "^4.0.0",