@doccov/cli 0.26.0 → 0.27.1

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
@@ -32,6 +32,7 @@ var apiSurfaceConfigSchema = z.object({
32
32
  });
33
33
  var checkConfigSchema = z.object({
34
34
  examples: exampleModesSchema.optional(),
35
+ minHealth: z.number().min(0).max(100).optional(),
35
36
  minCoverage: z.number().min(0).max(100).optional(),
36
37
  maxDrift: z.number().min(0).max(100).optional(),
37
38
  minApiSurface: z.number().min(0).max(100).optional(),
@@ -70,6 +71,7 @@ var normalizeConfig = (input) => {
70
71
  if (input.check) {
71
72
  check = {
72
73
  examples: input.check.examples,
74
+ minHealth: input.check.minHealth,
73
75
  minCoverage: input.check.minCoverage,
74
76
  maxDrift: input.check.maxDrift,
75
77
  minApiSurface: input.check.minApiSurface,
@@ -1362,6 +1364,10 @@ async function handleFixes(openpkg, doccov, options, deps) {
1362
1364
  const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
1363
1365
  log(chalk5.dim(` ${relativePath} (${fixCount} fixes)`));
1364
1366
  }
1367
+ if (options.healthScore !== undefined) {
1368
+ log("");
1369
+ log(chalk5.cyan("Run doccov check again to see updated health score"));
1370
+ }
1365
1371
  return {
1366
1372
  fixedDriftKeys,
1367
1373
  editsApplied: totalFixes,
@@ -1800,9 +1806,31 @@ function renderGithubSummary(stats, options = {}) {
1800
1806
  const coverageScore = options.coverageScore ?? stats.coverageScore;
1801
1807
  const driftCount = options.driftCount ?? stats.driftCount;
1802
1808
  const qualityIssues = options.qualityIssues ?? 0;
1803
- 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}%
1814
+
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}%
1804
1831
 
1805
1832
  `;
1833
+ }
1806
1834
  output += `| Metric | Value |
1807
1835
  |--------|-------|
1808
1836
  `;
@@ -1814,10 +1842,12 @@ function renderGithubSummary(stats, options = {}) {
1814
1842
  `;
1815
1843
  output += `| Quality Issues | ${qualityIssues} |
1816
1844
  `;
1817
- const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
1818
- output += `
1845
+ if (!stats.health) {
1846
+ const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
1847
+ output += `
1819
1848
  ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "needs improvement" : "failing"}
1820
1849
  `;
1850
+ }
1821
1851
  return output;
1822
1852
  }
1823
1853
  // src/reports/markdown.ts
@@ -1831,6 +1861,19 @@ function renderMarkdown(stats, options = {}) {
1831
1861
  const lines = [];
1832
1862
  lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
1833
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
+ }
1834
1877
  lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar2(stats.coverageScore)}\``);
1835
1878
  lines.push("");
1836
1879
  lines.push("| Metric | Value |");
@@ -2346,7 +2389,8 @@ function computeStats(openpkg, doccov) {
2346
2389
  driftIssues,
2347
2390
  driftByCategory,
2348
2391
  driftSummary,
2349
- apiSurface: doccov.apiSurface
2392
+ apiSurface: doccov.apiSurface,
2393
+ health: doccov.summary.health
2350
2394
  };
2351
2395
  }
2352
2396
  // src/reports/writer.ts
@@ -2388,13 +2432,74 @@ function writeReports(options) {
2388
2432
  return results;
2389
2433
  }
2390
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
+ }
2391
2497
  function displayTextOutput(options, deps) {
2392
2498
  const {
2393
2499
  openpkg,
2394
2500
  doccov,
2395
2501
  coverageScore,
2396
- minCoverage,
2397
- maxDrift,
2502
+ minHealth,
2398
2503
  minApiSurface,
2399
2504
  warnBelowApiSurface,
2400
2505
  driftExports,
@@ -2407,14 +2512,15 @@ function displayTextOutput(options, deps) {
2407
2512
  } = options;
2408
2513
  const { log } = deps;
2409
2514
  const sym = getSymbols(supportsUnicode());
2515
+ const health = doccov.summary.health;
2516
+ const healthScore = health?.score ?? coverageScore;
2410
2517
  const totalExportsForDrift = openpkg.exports?.length ?? 0;
2411
2518
  const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
2412
2519
  const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
2413
2520
  const apiSurface = doccov.apiSurface;
2414
2521
  const apiSurfaceScore = apiSurface?.completeness ?? 100;
2415
2522
  const forgottenCount = apiSurface?.forgotten?.length ?? 0;
2416
- const coverageFailed = coverageScore < minCoverage;
2417
- const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2523
+ const healthFailed = healthScore < minHealth;
2418
2524
  const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2419
2525
  const apiSurfaceWarn = warnBelowApiSurface !== undefined && apiSurfaceScore < warnBelowApiSurface && !apiSurfaceFailed;
2420
2526
  const hasTypecheckErrors = typecheckErrors.length > 0;
@@ -2435,43 +2541,35 @@ function displayTextOutput(options, deps) {
2435
2541
  }
2436
2542
  const pkgName = openpkg.meta?.name ?? "unknown";
2437
2543
  const pkgVersion = openpkg.meta?.version ?? "";
2438
- const totalExports = openpkg.exports?.length ?? 0;
2439
2544
  log("");
2440
- log(colors.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
2545
+ log(colors.bold(`${pkgName}${pkgVersion ? ` v${pkgVersion}` : ""}`));
2441
2546
  log("");
2442
- const summaryBuilder = summary({ keyWidth: 10 });
2443
- summaryBuilder.addKeyValue("Exports", totalExports);
2444
- summaryBuilder.addKeyValue("Coverage", `${coverageScore}%`, coverageFailed ? "fail" : "pass");
2445
- if (maxDrift !== undefined) {
2446
- 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
+ }
2447
2557
  } else {
2558
+ const summaryBuilder = summary({ keyWidth: 10 });
2559
+ summaryBuilder.addKeyValue("Exports", openpkg.exports?.length ?? 0);
2560
+ summaryBuilder.addKeyValue("Coverage", `${coverageScore}%`);
2448
2561
  summaryBuilder.addKeyValue("Drift", `${driftScore}%`);
2562
+ summaryBuilder.print();
2449
2563
  }
2564
+ log("");
2450
2565
  if (forgottenCount > 0 || minApiSurface !== undefined || warnBelowApiSurface !== undefined) {
2451
2566
  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
- }
2462
- if (exampleResult) {
2463
- const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
2464
- if (typecheckCount > 0) {
2465
- summaryBuilder.addKeyValue("Examples", `${typecheckCount} type error(s)`, "warn");
2466
- } else {
2467
- summaryBuilder.addKeyValue("Examples", "validated", "pass");
2468
- }
2567
+ const surfaceColor = apiSurfaceFailed ? colors.error : apiSurfaceWarn || forgottenCount > 0 ? colors.warning : colors.success;
2568
+ log(`${colors.muted("API Surface")} ${surfaceColor(surfaceLabel)}`);
2469
2569
  }
2470
- const hasStaleRefs = staleRefs.length > 0;
2471
2570
  if (hasStaleRefs) {
2472
- summaryBuilder.addKeyValue("Docs", `${staleRefs.length} stale ref(s)`, "warn");
2571
+ log(`${colors.muted("Stale refs")} ${colors.warning(`${staleRefs.length} found`)}`);
2473
2572
  }
2474
- summaryBuilder.print();
2475
2573
  if (hasTypecheckErrors) {
2476
2574
  log("");
2477
2575
  for (const err of typecheckErrors.slice(0, 5)) {
@@ -2520,13 +2618,10 @@ function displayTextOutput(options, deps) {
2520
2618
  }
2521
2619
  }
2522
2620
  log("");
2523
- const failed = coverageFailed || driftFailed || apiSurfaceFailed || hasTypecheckErrors || hasStaleRefs;
2621
+ const failed = healthFailed || apiSurfaceFailed || hasTypecheckErrors || hasStaleRefs;
2524
2622
  if (!failed) {
2525
2623
  const thresholdParts = [];
2526
- thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
2527
- if (maxDrift !== undefined) {
2528
- thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
2529
- }
2624
+ thresholdParts.push(`health ${healthScore}% ≥ ${minHealth}%`);
2530
2625
  if (minApiSurface !== undefined) {
2531
2626
  thresholdParts.push(`api-surface ${apiSurfaceScore}% ≥ ${minApiSurface}%`);
2532
2627
  }
@@ -2534,13 +2629,13 @@ function displayTextOutput(options, deps) {
2534
2629
  if (apiSurfaceWarn) {
2535
2630
  log(colors.warning(`${sym.warning} API Surface ${apiSurfaceScore}% below warning threshold ${warnBelowApiSurface}%`));
2536
2631
  }
2632
+ if (!verbose && health) {
2633
+ log(colors.muted("Use --verbose for detailed breakdown"));
2634
+ }
2537
2635
  return true;
2538
2636
  }
2539
- if (coverageFailed) {
2540
- log(colors.error(`${sym.error} Coverage ${coverageScore}% below minimum ${minCoverage}%`));
2541
- }
2542
- if (driftFailed) {
2543
- log(colors.error(`${sym.error} Drift ${driftScore}% exceeds maximum ${maxDrift}%`));
2637
+ if (healthFailed) {
2638
+ log(colors.error(`${sym.error} Health ${healthScore}% below minimum ${minHealth}%`));
2544
2639
  }
2545
2640
  if (apiSurfaceFailed) {
2546
2641
  log(colors.error(`${sym.error} API Surface ${apiSurfaceScore}% below minimum ${minApiSurface}%`));
@@ -2551,8 +2646,10 @@ function displayTextOutput(options, deps) {
2551
2646
  if (hasStaleRefs) {
2552
2647
  log(colors.error(`${sym.error} ${staleRefs.length} stale references in docs`));
2553
2648
  }
2554
- log("");
2555
- 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
+ }
2556
2653
  return false;
2557
2654
  }
2558
2655
  function handleNonTextOutput(options, deps) {
@@ -2561,8 +2658,7 @@ function handleNonTextOutput(options, deps) {
2561
2658
  openpkg,
2562
2659
  doccov,
2563
2660
  coverageScore,
2564
- minCoverage,
2565
- maxDrift,
2661
+ minHealth,
2566
2662
  minApiSurface,
2567
2663
  driftExports,
2568
2664
  typecheckErrors,
@@ -2575,6 +2671,7 @@ function handleNonTextOutput(options, deps) {
2575
2671
  const stats = computeStats(openpkg, doccov);
2576
2672
  const report = generateReportFromDocCov(openpkg, doccov);
2577
2673
  const jsonContent = JSON.stringify(report, null, 2);
2674
+ const healthScore = doccov.summary.health?.score ?? coverageScore;
2578
2675
  let formatContent;
2579
2676
  switch (format) {
2580
2677
  case "json":
@@ -2606,15 +2703,11 @@ function handleNonTextOutput(options, deps) {
2606
2703
  cwd
2607
2704
  });
2608
2705
  }
2609
- const totalExportsForDrift = openpkg.exports?.length ?? 0;
2610
- const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
2611
- const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
2612
- const coverageFailed = coverageScore < minCoverage;
2613
- const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
2706
+ const healthFailed = healthScore < minHealth;
2614
2707
  const apiSurfaceScore = doccov.apiSurface?.completeness ?? 100;
2615
2708
  const apiSurfaceFailed = minApiSurface !== undefined && apiSurfaceScore < minApiSurface;
2616
2709
  const hasTypecheckErrors = typecheckErrors.length > 0;
2617
- return !(coverageFailed || driftFailed || apiSurfaceFailed || hasTypecheckErrors);
2710
+ return !(healthFailed || apiSurfaceFailed || hasTypecheckErrors);
2618
2711
  }
2619
2712
  function displayApiSurfaceOutput(doccov, deps) {
2620
2713
  const { log } = deps;
@@ -2745,7 +2838,7 @@ function registerCheckCommand(program, dependencies = {}) {
2745
2838
  ...defaultDependencies,
2746
2839
  ...dependencies
2747
2840
  };
2748
- 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) => {
2749
2842
  const n = parseInt(value, 10);
2750
2843
  if (Number.isNaN(n) || n < 1)
2751
2844
  throw new Error("--max-type-depth must be a positive integer");
@@ -2772,8 +2865,22 @@ function registerCheckCommand(program, dependencies = {}) {
2772
2865
  }
2773
2866
  hasExamples = validations.length > 0;
2774
2867
  }
2775
- const DEFAULT_MIN_COVERAGE = 80;
2776
- 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;
2777
2884
  const minCoverage = clampPercentage(minCoverageRaw);
2778
2885
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
2779
2886
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
@@ -2796,7 +2903,7 @@ function registerCheckCommand(program, dependencies = {}) {
2796
2903
  resolveExternalTypes,
2797
2904
  maxDepth: options.maxTypeDepth,
2798
2905
  useCache: options.cache !== false,
2799
- cwd: options.cwd
2906
+ cwd: targetDir
2800
2907
  });
2801
2908
  const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
2802
2909
  const specResult = await analyzer.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
@@ -2841,8 +2948,9 @@ function registerCheckCommand(program, dependencies = {}) {
2841
2948
  const coverageScore = doccov.summary.score;
2842
2949
  const allDriftExports = [...collectDrift(openpkg.exports ?? [], doccov), ...runtimeDrifts];
2843
2950
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
2951
+ const healthScore = doccov.summary.health?.score ?? coverageScore;
2844
2952
  if (shouldFix && driftExports.length > 0) {
2845
- const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir, entryFile }, { log, error });
2953
+ const fixResult = await handleFixes(openpkg, doccov, { isPreview, targetDir, entryFile, healthScore }, { log, error });
2846
2954
  if (!isPreview) {
2847
2955
  driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
2848
2956
  }
@@ -2865,15 +2973,14 @@ function registerCheckCommand(program, dependencies = {}) {
2865
2973
  openpkg,
2866
2974
  doccov,
2867
2975
  coverageScore,
2868
- minCoverage,
2869
- maxDrift,
2976
+ minHealth,
2870
2977
  minApiSurface,
2871
2978
  driftExports,
2872
2979
  typecheckErrors,
2873
2980
  limit: parseInt(options.limit, 10) || 20,
2874
2981
  stdout: options.stdout,
2875
2982
  outputPath: options.output,
2876
- cwd: options.cwd
2983
+ cwd: targetDir
2877
2984
  }, { log });
2878
2985
  if (!passed2) {
2879
2986
  process.exit(1);
@@ -2884,8 +2991,7 @@ function registerCheckCommand(program, dependencies = {}) {
2884
2991
  openpkg,
2885
2992
  doccov,
2886
2993
  coverageScore,
2887
- minCoverage,
2888
- maxDrift,
2994
+ minHealth,
2889
2995
  minApiSurface,
2890
2996
  warnBelowApiSurface,
2891
2997
  driftExports,
@@ -3534,12 +3640,11 @@ import {
3534
3640
  resolveTarget as resolveTarget3
3535
3641
  } from "@doccov/sdk";
3536
3642
  import { validateDocCovSpec } from "@doccov/spec";
3537
- import { normalize, validateSpec } from "@openpkg-ts/spec";
3643
+ import {
3644
+ normalize,
3645
+ validateSpec
3646
+ } from "@openpkg-ts/spec";
3538
3647
  import chalk11 from "chalk";
3539
- // package.json
3540
- var version = "0.25.11";
3541
-
3542
- // src/commands/spec.ts
3543
3648
  var defaultDependencies4 = {
3544
3649
  createDocCov: (options) => new DocCov3(options),
3545
3650
  writeFileSync: fs6.writeFileSync,
@@ -3593,24 +3698,13 @@ function registerSpecCommand(program, dependencies = {}) {
3593
3698
  cwd: options.cwd,
3594
3699
  schemaExtraction: options.runtime ? "hybrid" : "static"
3595
3700
  });
3596
- const generationInput = {
3597
- entryPoint: path8.relative(targetDir, entryFile),
3598
- entryPointSource: entryPointInfo.source,
3599
- isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
3600
- generatorName: "@doccov/cli",
3601
- generatorVersion: version,
3602
- packageManager: packageInfo?.packageManager,
3603
- isMonorepo: resolved.isMonorepo,
3604
- targetPackage: packageInfo?.name
3605
- };
3606
3701
  const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
3607
3702
  filters: {
3608
3703
  include: resolvedFilters.include,
3609
3704
  exclude: resolvedFilters.exclude,
3610
3705
  visibility: resolvedFilters.visibility
3611
- },
3612
- generationInput
3613
- } : { generationInput };
3706
+ }
3707
+ } : {};
3614
3708
  const result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
3615
3709
  if (!result) {
3616
3710
  spin.fail("Generation failed");
@@ -3667,7 +3761,11 @@ function registerSpecCommand(program, dependencies = {}) {
3667
3761
  log(chalk11.gray(` ${getArrayLength(normalized.types)} types`));
3668
3762
  }
3669
3763
  }
3670
- 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;
3671
3769
  if (options.runtime && (!schemaExtraction?.runtimeCount || schemaExtraction.runtimeCount === 0)) {
3672
3770
  const pm = await detectPackageManager(fileSystem);
3673
3771
  const buildCmd = pm.name === "npm" ? "npm run build" : `${pm.name} run build`;
@@ -3675,21 +3773,20 @@ function registerSpecCommand(program, dependencies = {}) {
3675
3773
  log(chalk11.yellow("⚠ Runtime extraction requested but no schemas extracted."));
3676
3774
  log(chalk11.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
3677
3775
  }
3678
- if (options.verbose && normalized.generation) {
3679
- const gen = normalized.generation;
3776
+ if (options.verbose && fullGen) {
3680
3777
  log("");
3681
3778
  log(chalk11.bold("Generation Info"));
3682
- log(chalk11.gray(` Timestamp: ${gen.timestamp}`));
3683
- log(chalk11.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
3684
- log(chalk11.gray(` Entry point: ${gen.analysis.entryPoint}`));
3685
- log(chalk11.gray(` Detected via: ${gen.analysis.entryPointSource}`));
3686
- log(chalk11.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
3687
- log(chalk11.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
3688
- if (gen.analysis.maxTypeDepth) {
3689
- 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}`));
3690
3787
  }
3691
- if (gen.analysis.schemaExtraction) {
3692
- const se = gen.analysis.schemaExtraction;
3788
+ if (fullGen.analysis.schemaExtraction) {
3789
+ const se = fullGen.analysis.schemaExtraction;
3693
3790
  log(chalk11.gray(` Schema extraction: ${se.method}`));
3694
3791
  if (se.runtimeCount) {
3695
3792
  log(chalk11.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
@@ -3697,20 +3794,20 @@ function registerSpecCommand(program, dependencies = {}) {
3697
3794
  }
3698
3795
  log("");
3699
3796
  log(chalk11.bold("Environment"));
3700
- log(chalk11.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
3701
- if (gen.environment.packageManager) {
3702
- 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}`));
3703
3800
  }
3704
- if (gen.environment.isMonorepo) {
3801
+ if (fullGen.environment.isMonorepo) {
3705
3802
  log(chalk11.gray(` Monorepo: yes`));
3706
3803
  }
3707
- if (gen.environment.targetPackage) {
3708
- log(chalk11.gray(` Target package: ${gen.environment.targetPackage}`));
3804
+ if (fullGen.environment.targetPackage) {
3805
+ log(chalk11.gray(` Target package: ${fullGen.environment.targetPackage}`));
3709
3806
  }
3710
- if (gen.issues.length > 0) {
3807
+ if (fullGen.issues.length > 0) {
3711
3808
  log("");
3712
3809
  log(chalk11.bold("Issues"));
3713
- for (const issue of gen.issues) {
3810
+ for (const issue of fullGen.issues) {
3714
3811
  const prefix2 = issue.severity === "error" ? chalk11.red(">") : issue.severity === "warning" ? chalk11.yellow(">") : chalk11.cyan(">");
3715
3812
  log(`${prefix2} [${issue.code}] ${issue.message}`);
3716
3813
  if (issue.suggestion) {
@@ -3771,9 +3868,9 @@ function getColorForScore(score) {
3771
3868
  function formatSnapshot(snapshot) {
3772
3869
  const color = getColorForScore(snapshot.coverageScore);
3773
3870
  const date = formatDate(snapshot.timestamp);
3774
- const version2 = snapshot.version ? ` v${snapshot.version}` : "";
3871
+ const version = snapshot.version ? ` v${snapshot.version}` : "";
3775
3872
  const commit = snapshot.commit ? chalk12.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
3776
- 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}`;
3777
3874
  }
3778
3875
  function formatWeekDate(timestamp) {
3779
3876
  const date = new Date(timestamp);
@@ -25,6 +25,7 @@ declare const apiSurfaceConfigSchema: z.ZodObject<{
25
25
  */
26
26
  declare const checkConfigSchema: z.ZodObject<{
27
27
  examples: z.ZodOptional<typeof exampleModesSchema>;
28
+ minHealth: z.ZodOptional<z.ZodNumber>;
28
29
  minCoverage: z.ZodOptional<z.ZodNumber>;
29
30
  maxDrift: z.ZodOptional<z.ZodNumber>;
30
31
  minApiSurface: z.ZodOptional<z.ZodNumber>;
@@ -30,6 +30,7 @@ var apiSurfaceConfigSchema = z.object({
30
30
  });
31
31
  var checkConfigSchema = z.object({
32
32
  examples: exampleModesSchema.optional(),
33
+ minHealth: z.number().min(0).max(100).optional(),
33
34
  minCoverage: z.number().min(0).max(100).optional(),
34
35
  maxDrift: z.number().min(0).max(100).optional(),
35
36
  minApiSurface: z.number().min(0).max(100).optional(),
@@ -68,6 +69,7 @@ var normalizeConfig = (input) => {
68
69
  if (input.check) {
69
70
  check = {
70
71
  examples: input.check.examples,
72
+ minHealth: input.check.minHealth,
71
73
  minCoverage: input.check.minCoverage,
72
74
  maxDrift: input.check.maxDrift,
73
75
  minApiSurface: input.check.minApiSurface,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
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.26.0",
52
- "@doccov/spec": "^0.26.0",
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",