@doccov/cli 0.9.0 → 0.10.2

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
@@ -33,14 +33,24 @@ var docsConfigSchema = z.object({
33
33
  include: stringList.optional(),
34
34
  exclude: stringList.optional()
35
35
  });
36
- var lintSeveritySchema = z.enum(["error", "warn", "off"]);
36
+ var severitySchema = z.enum(["error", "warn", "off"]);
37
+ var exampleModeSchema = z.enum([
38
+ "presence",
39
+ "typecheck",
40
+ "run"
41
+ ]);
42
+ var exampleModesSchema = z.union([
43
+ exampleModeSchema,
44
+ z.array(exampleModeSchema),
45
+ z.string()
46
+ ]);
37
47
  var checkConfigSchema = z.object({
38
- lint: z.boolean().optional(),
39
- typecheck: z.boolean().optional(),
40
- exec: z.boolean().optional()
48
+ examples: exampleModesSchema.optional(),
49
+ minCoverage: z.number().min(0).max(100).optional(),
50
+ maxDrift: z.number().min(0).max(100).optional()
41
51
  });
42
- var lintConfigSchema = z.object({
43
- rules: z.record(lintSeveritySchema).optional()
52
+ var qualityConfigSchema = z.object({
53
+ rules: z.record(severitySchema).optional()
44
54
  });
45
55
  var docCovConfigSchema = z.object({
46
56
  include: stringList.optional(),
@@ -48,7 +58,7 @@ var docCovConfigSchema = z.object({
48
58
  plugins: z.array(z.unknown()).optional(),
49
59
  docs: docsConfigSchema.optional(),
50
60
  check: checkConfigSchema.optional(),
51
- lint: lintConfigSchema.optional()
61
+ quality: qualityConfigSchema.optional()
52
62
  });
53
63
  var normalizeList = (value) => {
54
64
  if (!value) {
@@ -75,15 +85,15 @@ var normalizeConfig = (input) => {
75
85
  let check;
76
86
  if (input.check) {
77
87
  check = {
78
- lint: input.check.lint,
79
- typecheck: input.check.typecheck,
80
- exec: input.check.exec
88
+ examples: input.check.examples,
89
+ minCoverage: input.check.minCoverage,
90
+ maxDrift: input.check.maxDrift
81
91
  };
82
92
  }
83
- let lint;
84
- if (input.lint) {
85
- lint = {
86
- rules: input.lint.rules
93
+ let quality;
94
+ if (input.quality) {
95
+ quality = {
96
+ rules: input.quality.rules
87
97
  };
88
98
  }
89
99
  return {
@@ -92,7 +102,7 @@ var normalizeConfig = (input) => {
92
102
  plugins: input.plugins,
93
103
  docs,
94
104
  check,
95
- lint
105
+ quality
96
106
  };
97
107
  };
98
108
 
@@ -169,36 +179,330 @@ ${formatIssues(issues)}`);
169
179
  var defineConfig = (config) => config;
170
180
  // src/cli.ts
171
181
  import { readFileSync as readFileSync4 } from "node:fs";
172
- import * as path8 from "node:path";
182
+ import * as path9 from "node:path";
173
183
  import { fileURLToPath } from "node:url";
174
184
  import { Command } from "commander";
175
185
 
176
186
  // src/commands/check.ts
177
- import * as fs from "node:fs";
178
- import * as path2 from "node:path";
187
+ import * as fs2 from "node:fs";
188
+ import * as path3 from "node:path";
179
189
  import {
180
190
  applyEdits,
181
191
  categorizeDrifts,
182
192
  createSourceFile,
183
193
  DocCov,
184
- detectExampleAssertionFailures,
185
- detectExampleRuntimeErrors,
194
+ enrichSpec,
186
195
  findJSDocLocation,
187
196
  generateFixesForExport,
188
- getDefaultConfig as getLintDefaultConfig,
189
- hasNonAssertionComments,
190
- lintExport,
197
+ generateReport,
191
198
  mergeFixes,
192
199
  NodeFileSystem,
193
- parseAssertions,
200
+ parseExamplesFlag,
194
201
  parseJSDocToPatch,
195
202
  resolveTarget,
196
- runExamplesWithPackage,
197
203
  serializeJSDoc,
198
- typecheckExamples
204
+ validateExamples
199
205
  } from "@doccov/sdk";
200
- import chalk from "chalk";
206
+ import {
207
+ DRIFT_CATEGORIES as DRIFT_CATEGORIES2
208
+ } from "@openpkg-ts/spec";
209
+ import chalk2 from "chalk";
210
+
211
+ // src/reports/github.ts
212
+ function renderGithubSummary(stats, options = {}) {
213
+ const coverageScore = options.coverageScore ?? stats.coverageScore;
214
+ const driftCount = options.driftCount ?? stats.driftCount;
215
+ const qualityIssues = options.qualityIssues ?? 0;
216
+ let output = `## Documentation Coverage: ${coverageScore}%
217
+
218
+ `;
219
+ output += `| Metric | Value |
220
+ |--------|-------|
221
+ `;
222
+ output += `| Coverage Score | ${coverageScore}% |
223
+ `;
224
+ output += `| Total Exports | ${stats.totalExports} |
225
+ `;
226
+ output += `| Drift Issues | ${driftCount} |
227
+ `;
228
+ output += `| Quality Issues | ${qualityIssues} |
229
+ `;
230
+ const status = coverageScore >= 80 ? "✅" : coverageScore >= 50 ? "⚠️" : "❌";
231
+ output += `
232
+ ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "needs improvement" : "failing"}
233
+ `;
234
+ return output;
235
+ }
236
+ // src/reports/markdown.ts
237
+ import { DRIFT_CATEGORY_LABELS } from "@openpkg-ts/spec";
238
+ function bar(pct, width = 10) {
239
+ const filled = Math.round(pct / 100 * width);
240
+ return "█".repeat(filled) + "░".repeat(width - filled);
241
+ }
242
+ function renderMarkdown(stats, options = {}) {
243
+ const limit = options.limit ?? 20;
244
+ const lines = [];
245
+ lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
246
+ lines.push("");
247
+ lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
248
+ lines.push("");
249
+ lines.push("| Metric | Value |");
250
+ lines.push("|--------|-------|");
251
+ lines.push(`| Exports | ${stats.totalExports} |`);
252
+ lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
253
+ lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
254
+ lines.push(`| Undocumented | ${stats.undocumented} |`);
255
+ lines.push(`| Drift issues | ${stats.driftCount} |`);
256
+ lines.push("");
257
+ lines.push("## Coverage by Signal");
258
+ lines.push("");
259
+ lines.push("| Signal | Coverage |");
260
+ lines.push("|--------|----------|");
261
+ for (const [sig, s] of Object.entries(stats.signalCoverage)) {
262
+ lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
263
+ }
264
+ if (stats.byKind.length > 0) {
265
+ lines.push("");
266
+ lines.push("## Coverage by Kind");
267
+ lines.push("");
268
+ lines.push("| Kind | Count | Avg Score |");
269
+ lines.push("|------|-------|-----------|");
270
+ for (const k of stats.byKind) {
271
+ lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
272
+ }
273
+ }
274
+ const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
275
+ if (lowExports.length > 0) {
276
+ lines.push("");
277
+ lines.push("## Lowest Coverage Exports");
278
+ lines.push("");
279
+ lines.push("| Export | Kind | Score | Missing |");
280
+ lines.push("|--------|------|-------|---------|");
281
+ for (const e of lowExports) {
282
+ lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
283
+ }
284
+ const totalLow = stats.exports.filter((e) => e.score < 100).length;
285
+ if (totalLow > limit) {
286
+ lines.push(`| ... | | | ${totalLow - limit} more |`);
287
+ }
288
+ }
289
+ if (stats.driftIssues.length > 0) {
290
+ lines.push("");
291
+ lines.push("## Drift Issues");
292
+ lines.push("");
293
+ const { driftSummary } = stats;
294
+ const summaryParts = [];
295
+ if (driftSummary.byCategory.structural > 0) {
296
+ summaryParts.push(`${driftSummary.byCategory.structural} structural`);
297
+ }
298
+ if (driftSummary.byCategory.semantic > 0) {
299
+ summaryParts.push(`${driftSummary.byCategory.semantic} semantic`);
300
+ }
301
+ if (driftSummary.byCategory.example > 0) {
302
+ summaryParts.push(`${driftSummary.byCategory.example} example`);
303
+ }
304
+ const fixableNote = driftSummary.fixable > 0 ? ` (${driftSummary.fixable} auto-fixable)` : "";
305
+ lines.push(`**${driftSummary.total} issues** (${summaryParts.join(", ")})${fixableNote}`);
306
+ lines.push("");
307
+ const categories = ["structural", "semantic", "example"];
308
+ for (const category of categories) {
309
+ const issues = stats.driftByCategory[category];
310
+ if (issues.length === 0)
311
+ continue;
312
+ lines.push(`### ${DRIFT_CATEGORY_LABELS[category]}`);
313
+ lines.push("");
314
+ lines.push("| Export | Issue |");
315
+ lines.push("|--------|-------|");
316
+ for (const d of issues.slice(0, Math.min(limit, 10))) {
317
+ const hint = d.suggestion ? ` → ${d.suggestion}` : "";
318
+ lines.push(`| \`${d.exportName}\` | ${d.issue}${hint} |`);
319
+ }
320
+ if (issues.length > 10) {
321
+ lines.push(`| ... | ${issues.length - 10} more ${category} issues |`);
322
+ }
323
+ lines.push("");
324
+ }
325
+ }
326
+ lines.push("");
327
+ lines.push("---");
328
+ lines.push("*Generated by [DocCov](https://doccov.com)*");
329
+ return lines.join(`
330
+ `);
331
+ }
201
332
 
333
+ // src/reports/html.ts
334
+ function escapeHtml(s) {
335
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
336
+ }
337
+ function renderHtml(stats, options = {}) {
338
+ const md = renderMarkdown(stats, options);
339
+ return `<!DOCTYPE html>
340
+ <html lang="en">
341
+ <head>
342
+ <meta charset="UTF-8">
343
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
344
+ <title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
345
+ <style>
346
+ :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
347
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
348
+ h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
349
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
350
+ th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
351
+ th { background: #161b22; }
352
+ code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
353
+ a { color: var(--accent); }
354
+ </style>
355
+ </head>
356
+ <body>
357
+ <pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
358
+ </body>
359
+ </html>`;
360
+ }
361
+ // src/reports/stats.ts
362
+ import {
363
+ DRIFT_CATEGORIES
364
+ } from "@openpkg-ts/spec";
365
+ var FIXABLE_DRIFT_TYPES = new Set([
366
+ "param-mismatch",
367
+ "param-type-mismatch",
368
+ "optionality-mismatch",
369
+ "return-type-mismatch",
370
+ "generic-constraint-mismatch",
371
+ "example-assertion-failed",
372
+ "deprecated-mismatch",
373
+ "async-mismatch",
374
+ "property-type-drift"
375
+ ]);
376
+ function computeStats(spec) {
377
+ const exports = spec.exports ?? [];
378
+ const signals = {
379
+ description: { covered: 0, total: 0 },
380
+ params: { covered: 0, total: 0 },
381
+ returns: { covered: 0, total: 0 },
382
+ examples: { covered: 0, total: 0 }
383
+ };
384
+ const kindMap = new Map;
385
+ const driftIssues = [];
386
+ const driftByCategory = {
387
+ structural: [],
388
+ semantic: [],
389
+ example: []
390
+ };
391
+ let fullyDocumented = 0;
392
+ let partiallyDocumented = 0;
393
+ let undocumented = 0;
394
+ for (const exp of exports) {
395
+ const score = exp.docs?.coverageScore ?? 0;
396
+ const missing = exp.docs?.missing ?? [];
397
+ for (const sig of ["description", "params", "returns", "examples"]) {
398
+ signals[sig].total++;
399
+ if (!missing.includes(sig))
400
+ signals[sig].covered++;
401
+ }
402
+ const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
403
+ kindEntry.count++;
404
+ kindEntry.totalScore += score;
405
+ kindMap.set(exp.kind, kindEntry);
406
+ if (score === 100)
407
+ fullyDocumented++;
408
+ else if (score > 0)
409
+ partiallyDocumented++;
410
+ else
411
+ undocumented++;
412
+ for (const d of exp.docs?.drift ?? []) {
413
+ const item = {
414
+ exportName: exp.name,
415
+ type: d.type,
416
+ issue: d.issue,
417
+ suggestion: d.suggestion
418
+ };
419
+ driftIssues.push(item);
420
+ const category = DRIFT_CATEGORIES[d.type] ?? "semantic";
421
+ driftByCategory[category].push(item);
422
+ }
423
+ }
424
+ const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
425
+ k,
426
+ { ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
427
+ ]));
428
+ const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
429
+ kind,
430
+ count,
431
+ avgScore: Math.round(totalScore / count)
432
+ })).sort((a, b) => b.count - a.count);
433
+ const sortedExports = exports.map((e) => ({
434
+ name: e.name,
435
+ kind: e.kind,
436
+ score: e.docs?.coverageScore ?? 0,
437
+ missing: e.docs?.missing ?? []
438
+ })).sort((a, b) => a.score - b.score);
439
+ const driftSummary = {
440
+ total: driftIssues.length,
441
+ byCategory: {
442
+ structural: driftByCategory.structural.length,
443
+ semantic: driftByCategory.semantic.length,
444
+ example: driftByCategory.example.length
445
+ },
446
+ fixable: driftIssues.filter((d) => FIXABLE_DRIFT_TYPES.has(d.type)).length
447
+ };
448
+ const exportsWithDrift = new Set(driftIssues.map((d) => d.exportName)).size;
449
+ const driftScore = exports.length === 0 ? 0 : Math.round(exportsWithDrift / exports.length * 100);
450
+ return {
451
+ packageName: spec.meta.name ?? "unknown",
452
+ version: spec.meta.version ?? "0.0.0",
453
+ coverageScore: spec.docs?.coverageScore ?? 0,
454
+ driftScore,
455
+ totalExports: exports.length,
456
+ fullyDocumented,
457
+ partiallyDocumented,
458
+ undocumented,
459
+ driftCount: driftIssues.length,
460
+ signalCoverage,
461
+ byKind,
462
+ exports: sortedExports,
463
+ driftIssues,
464
+ driftByCategory,
465
+ driftSummary
466
+ };
467
+ }
468
+ // src/reports/writer.ts
469
+ import * as fs from "node:fs";
470
+ import * as path2 from "node:path";
471
+ import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
472
+ import chalk from "chalk";
473
+ function writeReport(options) {
474
+ const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
475
+ const reportPath = outputPath ? path2.resolve(cwd, outputPath) : path2.resolve(cwd, getReportPath(format));
476
+ const dir = path2.dirname(reportPath);
477
+ if (!fs.existsSync(dir)) {
478
+ fs.mkdirSync(dir, { recursive: true });
479
+ }
480
+ fs.writeFileSync(reportPath, content);
481
+ const relativePath = path2.relative(cwd, reportPath);
482
+ if (!silent) {
483
+ console.log(chalk.green(`✓ Wrote ${format} report to ${relativePath}`));
484
+ }
485
+ return { path: reportPath, format, relativePath };
486
+ }
487
+ function writeReports(options) {
488
+ const { format, formatContent, jsonContent, outputPath, cwd = process.cwd() } = options;
489
+ const results = [];
490
+ if (format !== "json") {
491
+ results.push(writeReport({
492
+ format: "json",
493
+ content: jsonContent,
494
+ cwd,
495
+ silent: true
496
+ }));
497
+ }
498
+ results.push(writeReport({
499
+ format,
500
+ content: format === "json" ? jsonContent : formatContent,
501
+ outputPath,
502
+ cwd
503
+ }));
504
+ return results;
505
+ }
202
506
  // src/utils/llm-assertion-parser.ts
203
507
  import { createAnthropic } from "@ai-sdk/anthropic";
204
508
  import { createOpenAI } from "@ai-sdk/openai";
@@ -296,7 +600,7 @@ function registerCheckCommand(program, dependencies = {}) {
296
600
  ...defaultDependencies,
297
601
  ...dependencies
298
602
  };
299
- program.command("check [entry]").description("Fail if documentation coverage falls below a threshold").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("--require-examples", "Require at least one @example for every export").option("--exec", "Execute @example blocks at runtime").option("--no-lint", "Skip lint checks").option("--no-typecheck", "Skip example type checking").option("--ignore-drift", "Do not fail on documentation drift").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift and lint issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --fix)").action(async (entry, options) => {
603
+ 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("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --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)").option("--no-cache", "Bypass spec cache and force regeneration").action(async (entry, options) => {
300
604
  try {
301
605
  const fileSystem = new NodeFileSystem(options.cwd);
302
606
  const resolved = await resolveTarget(fileSystem, {
@@ -306,220 +610,93 @@ function registerCheckCommand(program, dependencies = {}) {
306
610
  });
307
611
  const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
308
612
  if (packageInfo) {
309
- log(chalk.gray(`Found package at ${packageInfo.path}`));
613
+ log(chalk2.gray(`Found package at ${packageInfo.path}`));
310
614
  }
311
615
  if (!entry) {
312
- log(chalk.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
616
+ log(chalk2.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
313
617
  }
314
- const minCoverage = clampCoverage(options.minCoverage ?? 80);
618
+ const config = await loadDocCovConfig(targetDir);
619
+ const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
620
+ const minCoverage = minCoverageRaw !== undefined ? clampCoverage(minCoverageRaw) : undefined;
621
+ const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
622
+ const maxDrift = maxDriftRaw !== undefined ? clampCoverage(maxDriftRaw) : undefined;
315
623
  const resolveExternalTypes = !options.skipResolve;
316
- process.stdout.write(chalk.cyan(`> Analyzing documentation coverage...
317
- `));
318
624
  let specResult;
319
- try {
320
- const doccov = createDocCov({ resolveExternalTypes });
321
- specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
322
- process.stdout.write(chalk.green(`✓ Documentation analysis complete
323
- `));
324
- } catch (analysisError) {
325
- process.stdout.write(chalk.red(`✗ Failed to analyze documentation coverage
326
- `));
327
- throw analysisError;
625
+ const doccov = createDocCov({
626
+ resolveExternalTypes,
627
+ maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
628
+ useCache: options.cache !== false,
629
+ cwd: options.cwd
630
+ });
631
+ specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
632
+ if (specResult.fromCache) {
633
+ log(chalk2.gray("Using cached spec"));
328
634
  }
329
635
  if (!specResult) {
330
636
  throw new Error("Failed to analyze documentation coverage.");
331
637
  }
332
- const spec = specResult.spec;
333
- const warnings = specResult.diagnostics.filter((d) => d.severity === "warning");
334
- const infos = specResult.diagnostics.filter((d) => d.severity === "info");
335
- if (warnings.length > 0 || infos.length > 0) {
336
- log("");
337
- for (const diag of warnings) {
338
- log(chalk.yellow(`⚠ ${diag.message}`));
339
- if (diag.suggestion) {
340
- log(chalk.gray(` ${diag.suggestion}`));
341
- }
342
- }
343
- for (const diag of infos) {
344
- log(chalk.cyan(`ℹ ${diag.message}`));
345
- if (diag.suggestion) {
346
- log(chalk.gray(` ${diag.suggestion}`));
347
- }
348
- }
349
- log("");
350
- }
638
+ const spec = enrichSpec(specResult.spec);
639
+ const format = options.format ?? "text";
640
+ const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
641
+ const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
351
642
  const shouldFix = options.fix || options.write;
352
- const lintViolations = [];
353
- if (options.lint !== false) {
354
- process.stdout.write(chalk.cyan(`> Running lint checks...
355
- `));
356
- const lintConfig = getLintDefaultConfig();
357
- for (const exp of spec.exports ?? []) {
358
- const violations = lintExport(exp, undefined, lintConfig);
359
- for (const violation of violations) {
360
- lintViolations.push({ exportName: exp.name, violation });
361
- }
362
- }
363
- if (lintViolations.length === 0) {
364
- process.stdout.write(chalk.green(`✓ No lint issues
365
- `));
366
- } else {
367
- const errors = lintViolations.filter((v) => v.violation.severity === "error").length;
368
- const warns = lintViolations.filter((v) => v.violation.severity === "warn").length;
369
- process.stdout.write(chalk.yellow(`⚠ ${lintViolations.length} lint issue(s) (${errors} error, ${warns} warn)
370
- `));
643
+ const violations = [];
644
+ for (const exp of spec.exports ?? []) {
645
+ for (const v of exp.docs?.violations ?? []) {
646
+ violations.push({ exportName: exp.name, violation: v });
371
647
  }
372
648
  }
649
+ const validations = parseExamplesFlag(options.examples);
650
+ let exampleResult;
373
651
  const typecheckErrors = [];
374
- if (options.typecheck !== false) {
375
- const allExamplesForTypecheck = [];
376
- for (const exp of spec.exports ?? []) {
377
- if (exp.examples && exp.examples.length > 0) {
378
- allExamplesForTypecheck.push({
379
- exportName: exp.name,
380
- examples: exp.examples
381
- });
382
- }
383
- }
384
- if (allExamplesForTypecheck.length > 0) {
385
- process.stdout.write(chalk.cyan(`> Type-checking examples...
386
- `));
387
- for (const { exportName, examples } of allExamplesForTypecheck) {
388
- const result = typecheckExamples(examples, targetDir);
389
- for (const err of result.errors) {
390
- typecheckErrors.push({ exportName, error: err });
391
- }
392
- }
393
- if (typecheckErrors.length === 0) {
394
- process.stdout.write(chalk.green(`✓ All examples type-check
395
- `));
396
- } else {
397
- process.stdout.write(chalk.red(`✗ ${typecheckErrors.length} type error(s)
398
- `));
399
- }
400
- }
401
- }
402
652
  const runtimeDrifts = [];
403
- if (options.exec) {
404
- const allExamples = [];
405
- for (const entry2 of spec.exports ?? []) {
406
- if (entry2.examples && entry2.examples.length > 0) {
407
- allExamples.push({ exportName: entry2.name, examples: entry2.examples });
653
+ if (validations.length > 0) {
654
+ exampleResult = await validateExamples(spec.exports ?? [], {
655
+ validations,
656
+ packagePath: targetDir,
657
+ exportNames: (spec.exports ?? []).map((e) => e.name),
658
+ timeout: 5000,
659
+ installTimeout: 60000,
660
+ llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
661
+ const result = await parseAssertionsWithLLM(example);
662
+ return result;
663
+ } : undefined
664
+ });
665
+ if (exampleResult.typecheck) {
666
+ for (const err of exampleResult.typecheck.errors) {
667
+ typecheckErrors.push({
668
+ exportName: err.exportName,
669
+ error: err.error
670
+ });
408
671
  }
409
672
  }
410
- if (allExamples.length === 0) {
411
- log(chalk.gray("No @example blocks found"));
412
- } else {
413
- process.stdout.write(chalk.cyan(`> Installing package for examples...
414
- `));
415
- const flatExamples = allExamples.flatMap((e) => e.examples);
416
- const packageResult = await runExamplesWithPackage(flatExamples, {
417
- packagePath: targetDir,
418
- timeout: 5000,
419
- installTimeout: 60000,
420
- cwd: targetDir
421
- });
422
- if (!packageResult.installSuccess) {
423
- process.stdout.write(chalk.red(`✗ Package install failed: ${packageResult.installError}
424
- `));
425
- log(chalk.yellow("Skipping example execution. Ensure the package is built."));
426
- } else {
427
- process.stdout.write(chalk.cyan(`> Running @example blocks...
428
- `));
429
- let examplesRun = 0;
430
- let examplesFailed = 0;
431
- let exampleIndex = 0;
432
- for (const { exportName, examples } of allExamples) {
433
- const entryResults = new Map;
434
- for (let i = 0;i < examples.length; i++) {
435
- const result = packageResult.results.get(exampleIndex);
436
- if (result) {
437
- entryResults.set(i, result);
438
- examplesRun++;
439
- if (!result.success)
440
- examplesFailed++;
441
- }
442
- exampleIndex++;
443
- }
444
- const entry2 = (spec.exports ?? []).find((e) => e.name === exportName);
445
- if (entry2) {
446
- const runtimeErrorDrifts = detectExampleRuntimeErrors(entry2, entryResults);
447
- for (const drift of runtimeErrorDrifts) {
448
- runtimeDrifts.push({
449
- name: entry2.name,
450
- issue: drift.issue,
451
- suggestion: drift.suggestion
452
- });
453
- }
454
- const assertionDrifts = detectExampleAssertionFailures(entry2, entryResults);
455
- for (const drift of assertionDrifts) {
456
- runtimeDrifts.push({
457
- name: entry2.name,
458
- issue: drift.issue,
459
- suggestion: drift.suggestion
460
- });
461
- }
462
- if (isLLMAssertionParsingAvailable() && entry2.examples) {
463
- for (let exIdx = 0;exIdx < entry2.examples.length; exIdx++) {
464
- const example = entry2.examples[exIdx];
465
- const result = entryResults.get(exIdx);
466
- if (!result?.success || typeof example !== "string")
467
- continue;
468
- const regexAssertions = parseAssertions(example);
469
- if (regexAssertions.length === 0 && hasNonAssertionComments(example)) {
470
- const llmResult = await parseAssertionsWithLLM(example);
471
- if (llmResult?.hasAssertions && llmResult.assertions.length > 0) {
472
- const stdoutLines = result.stdout.split(`
473
- `).map((l) => l.trim()).filter((l) => l.length > 0);
474
- for (let aIdx = 0;aIdx < llmResult.assertions.length; aIdx++) {
475
- const assertion = llmResult.assertions[aIdx];
476
- const actual = stdoutLines[aIdx];
477
- if (actual === undefined) {
478
- runtimeDrifts.push({
479
- name: entry2.name,
480
- issue: `Assertion expected "${assertion.expected}" but no output was produced`,
481
- suggestion: `Consider using standard syntax: ${assertion.suggestedSyntax}`
482
- });
483
- } else if (assertion.expected.trim() !== actual.trim()) {
484
- runtimeDrifts.push({
485
- name: entry2.name,
486
- issue: `Assertion failed: expected "${assertion.expected}" but got "${actual}"`,
487
- suggestion: `Consider using standard syntax: ${assertion.suggestedSyntax}`
488
- });
489
- }
490
- }
491
- }
492
- }
493
- }
494
- }
495
- }
496
- }
497
- if (examplesFailed > 0) {
498
- process.stdout.write(chalk.red(`✗ ${examplesFailed}/${examplesRun} example(s) failed
499
- `));
500
- } else {
501
- process.stdout.write(chalk.green(`✓ ${examplesRun} example(s) passed
502
- `));
503
- }
673
+ if (exampleResult.run) {
674
+ for (const drift of exampleResult.run.drifts) {
675
+ runtimeDrifts.push({
676
+ name: drift.exportName,
677
+ type: "example-runtime-error",
678
+ issue: drift.issue,
679
+ suggestion: drift.suggestion,
680
+ category: "example"
681
+ });
504
682
  }
505
683
  }
506
684
  }
507
685
  const coverageScore = spec.docs?.coverageScore ?? 0;
508
- const failingExports = collectFailingExports(spec.exports ?? [], minCoverage);
509
- const missingExamples = options.requireExamples ? failingExports.filter((item) => item.missing?.includes("examples")) : [];
510
- let driftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
686
+ const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
687
+ let driftExports = validations.length > 0 ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
511
688
  const fixedDriftKeys = new Set;
512
689
  if (shouldFix && driftExports.length > 0) {
513
690
  const allDrifts = collectDriftsFromExports(spec.exports ?? []);
514
691
  if (allDrifts.length > 0) {
515
692
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
516
693
  if (fixable.length === 0) {
517
- log(chalk.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
694
+ log(chalk2.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
518
695
  } else {
519
696
  log("");
520
- log(chalk.bold(`Found ${fixable.length} fixable issue(s)`));
697
+ log(chalk2.bold(`Found ${fixable.length} fixable issue(s)`));
521
698
  if (nonFixable.length > 0) {
522
- log(chalk.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
699
+ log(chalk2.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
523
700
  }
524
701
  log("");
525
702
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -527,29 +704,30 @@ function registerCheckCommand(program, dependencies = {}) {
527
704
  const editsByFile = new Map;
528
705
  for (const [exp, drifts] of groupedDrifts) {
529
706
  if (!exp.source?.file) {
530
- log(chalk.gray(` Skipping ${exp.name}: no source location`));
707
+ log(chalk2.gray(` Skipping ${exp.name}: no source location`));
531
708
  continue;
532
709
  }
533
710
  if (exp.source.file.endsWith(".d.ts")) {
534
- log(chalk.gray(` Skipping ${exp.name}: declaration file`));
711
+ log(chalk2.gray(` Skipping ${exp.name}: declaration file`));
535
712
  continue;
536
713
  }
537
- const filePath = path2.resolve(targetDir, exp.source.file);
538
- if (!fs.existsSync(filePath)) {
539
- log(chalk.gray(` Skipping ${exp.name}: file not found`));
714
+ const filePath = path3.resolve(targetDir, exp.source.file);
715
+ if (!fs2.existsSync(filePath)) {
716
+ log(chalk2.gray(` Skipping ${exp.name}: file not found`));
540
717
  continue;
541
718
  }
542
719
  const sourceFile = createSourceFile(filePath);
543
720
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
544
721
  if (!location) {
545
- log(chalk.gray(` Skipping ${exp.name}: could not find declaration`));
722
+ log(chalk2.gray(` Skipping ${exp.name}: could not find declaration`));
546
723
  continue;
547
724
  }
548
725
  let existingPatch = {};
549
726
  if (location.hasExisting && location.existingJSDoc) {
550
727
  existingPatch = parseJSDocToPatch(location.existingJSDoc);
551
728
  }
552
- const fixes = generateFixesForExport({ ...exp, docs: { ...exp.docs, drift: drifts } }, existingPatch);
729
+ const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
730
+ const fixes = generateFixesForExport(expWithDrift, existingPatch);
553
731
  if (fixes.length === 0)
554
732
  continue;
555
733
  for (const drift of drifts) {
@@ -574,39 +752,27 @@ function registerCheckCommand(program, dependencies = {}) {
574
752
  }
575
753
  if (edits.length > 0) {
576
754
  if (options.dryRun) {
577
- log(chalk.bold("Dry run - changes that would be made:"));
755
+ log(chalk2.bold("Dry run - changes that would be made:"));
578
756
  log("");
579
757
  for (const [filePath, fileEdits] of editsByFile) {
580
- const relativePath = path2.relative(targetDir, filePath);
581
- log(chalk.cyan(` ${relativePath}:`));
758
+ const relativePath = path3.relative(targetDir, filePath);
759
+ log(chalk2.cyan(` ${relativePath}:`));
582
760
  for (const { export: exp, edit, fixes } of fileEdits) {
583
761
  const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
584
- log(` ${chalk.bold(exp.name)} [${lineInfo}]`);
762
+ log(` ${chalk2.bold(exp.name)} [${lineInfo}]`);
585
763
  for (const fix of fixes) {
586
- log(chalk.green(` + ${fix.description}`));
764
+ log(chalk2.green(` + ${fix.description}`));
587
765
  }
588
766
  }
589
767
  log("");
590
768
  }
591
- log(chalk.gray("Run without --dry-run to apply these changes."));
769
+ log(chalk2.gray("Run without --dry-run to apply these changes."));
592
770
  } else {
593
- process.stdout.write(chalk.cyan(`> Applying fixes...
594
- `));
595
771
  const applyResult = await applyEdits(edits);
596
772
  if (applyResult.errors.length > 0) {
597
- process.stdout.write(chalk.yellow(`⚠ Some fixes could not be applied
598
- `));
599
773
  for (const err of applyResult.errors) {
600
- error(chalk.red(` ${err.file}: ${err.error}`));
774
+ error(chalk2.red(` ${err.file}: ${err.error}`));
601
775
  }
602
- } else {
603
- process.stdout.write(chalk.green(`✓ Applied ${applyResult.editsApplied} fix(es) to ${applyResult.filesModified} file(s)
604
- `));
605
- }
606
- log("");
607
- for (const [filePath, fileEdits] of editsByFile) {
608
- const relativePath = path2.relative(targetDir, filePath);
609
- log(chalk.green(` ✓ ${relativePath}: ${fileEdits.length} fix(es)`));
610
776
  }
611
777
  }
612
778
  }
@@ -616,68 +782,149 @@ function registerCheckCommand(program, dependencies = {}) {
616
782
  driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
617
783
  }
618
784
  }
619
- const coverageFailed = coverageScore < minCoverage;
620
- const hasMissingExamples = missingExamples.length > 0;
621
- const hasDrift = !options.ignoreDrift && driftExports.length > 0;
622
- const hasLintErrors = lintViolations.filter((v) => v.violation.severity === "error").length > 0;
785
+ if (format !== "text") {
786
+ const limit = parseInt(options.limit, 10) || 20;
787
+ const stats = computeStats(spec);
788
+ const report = generateReport(specResult.spec);
789
+ const jsonContent = JSON.stringify(report, null, 2);
790
+ let formatContent;
791
+ switch (format) {
792
+ case "json":
793
+ formatContent = jsonContent;
794
+ break;
795
+ case "markdown":
796
+ formatContent = renderMarkdown(stats, { limit });
797
+ break;
798
+ case "html":
799
+ formatContent = renderHtml(stats, { limit });
800
+ break;
801
+ case "github":
802
+ formatContent = renderGithubSummary(stats, {
803
+ coverageScore,
804
+ driftCount: driftExports.length,
805
+ qualityIssues: violations.length
806
+ });
807
+ break;
808
+ default:
809
+ throw new Error(`Unknown format: ${format}`);
810
+ }
811
+ if (options.stdout) {
812
+ log(formatContent);
813
+ } else {
814
+ writeReports({
815
+ format,
816
+ formatContent,
817
+ jsonContent,
818
+ outputPath: options.output,
819
+ cwd: options.cwd
820
+ });
821
+ }
822
+ const totalExportsForDrift2 = spec.exports?.length ?? 0;
823
+ const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
824
+ const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
825
+ const coverageFailed2 = minCoverage !== undefined && coverageScore < minCoverage;
826
+ const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
827
+ const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
828
+ const hasTypecheckErrors2 = typecheckErrors.length > 0;
829
+ if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2) {
830
+ process.exit(1);
831
+ }
832
+ return;
833
+ }
834
+ const totalExportsForDrift = spec.exports?.length ?? 0;
835
+ const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
836
+ const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
837
+ const coverageFailed = minCoverage !== undefined && coverageScore < minCoverage;
838
+ const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
839
+ const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
623
840
  const hasTypecheckErrors = typecheckErrors.length > 0;
624
- if (!coverageFailed && !hasMissingExamples && !hasDrift && !hasLintErrors && !hasTypecheckErrors) {
625
- log(chalk.green(`✓ Docs coverage ${coverageScore}% (min ${minCoverage}%)`));
626
- if (failingExports.length > 0) {
627
- log(chalk.gray("Some exports have partial docs:"));
628
- for (const { name, missing } of failingExports.slice(0, 10)) {
629
- log(chalk.gray(` ${name}: missing ${missing?.join(", ")}`));
841
+ if (specWarnings.length > 0 || specInfos.length > 0) {
842
+ log("");
843
+ for (const diag of specWarnings) {
844
+ log(chalk2.yellow(`⚠ ${diag.message}`));
845
+ if (diag.suggestion) {
846
+ log(chalk2.gray(` ${diag.suggestion}`));
630
847
  }
631
848
  }
632
- if (options.ignoreDrift && driftExports.length > 0) {
633
- log("");
634
- log(chalk.yellow(`⚠️ ${driftExports.length} drift issue(s) detected (ignored):`));
635
- for (const drift of driftExports.slice(0, 10)) {
636
- log(chalk.yellow(` • ${drift.name}: ${drift.issue}`));
637
- if (drift.suggestion) {
638
- log(chalk.gray(` Suggestion: ${drift.suggestion}`));
639
- }
849
+ for (const diag of specInfos) {
850
+ log(chalk2.cyan(`ℹ ${diag.message}`));
851
+ if (diag.suggestion) {
852
+ log(chalk2.gray(` ${diag.suggestion}`));
640
853
  }
641
854
  }
642
- return;
643
855
  }
644
- error("");
645
- if (coverageFailed) {
646
- error(chalk.red(`Docs coverage ${coverageScore}% fell below required ${minCoverage}%.`));
647
- }
648
- if (hasMissingExamples) {
649
- error(chalk.red(`${missingExamples.length} export(s) missing examples (required via --require-examples)`));
856
+ const pkgName = spec.meta?.name ?? "unknown";
857
+ const pkgVersion = spec.meta?.version ?? "";
858
+ const totalExports = spec.exports?.length ?? 0;
859
+ const errorCount = violations.filter((v) => v.violation.severity === "error").length;
860
+ const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
861
+ log("");
862
+ log(chalk2.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
863
+ log("");
864
+ log(` Exports: ${totalExports}`);
865
+ if (minCoverage !== undefined) {
866
+ if (coverageFailed) {
867
+ log(chalk2.red(` Coverage: ✗ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
868
+ } else {
869
+ log(chalk2.green(` Coverage: ✓ ${coverageScore}%`) + chalk2.dim(` (min ${minCoverage}%)`));
870
+ }
871
+ } else {
872
+ log(` Coverage: ${coverageScore}%`);
650
873
  }
651
- if (hasLintErrors) {
652
- error("");
653
- error(chalk.bold("Lint errors:"));
654
- for (const { exportName, violation } of lintViolations.filter((v) => v.violation.severity === "error").slice(0, 10)) {
655
- error(chalk.red(` ${exportName}: ${violation.message}`));
874
+ if (maxDrift !== undefined) {
875
+ if (driftFailed) {
876
+ log(chalk2.red(` Drift: ✗ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
877
+ } else {
878
+ log(chalk2.green(` Drift: ✓ ${driftScore}%`) + chalk2.dim(` (max ${maxDrift}%)`));
656
879
  }
880
+ } else {
881
+ log(` Drift: ${driftScore}%`);
657
882
  }
658
- if (hasTypecheckErrors) {
659
- error("");
660
- error(chalk.bold("Type errors in examples:"));
661
- for (const { exportName, error: err } of typecheckErrors.slice(0, 10)) {
662
- error(chalk.red(` • ${exportName} @example ${err.exampleIndex + 1}, line ${err.line}: ${err.message}`));
883
+ if (exampleResult) {
884
+ const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
885
+ if (typecheckCount > 0) {
886
+ log(` Examples: ${typecheckCount} type errors`);
887
+ } else {
888
+ log(chalk2.green(` Examples: ✓ validated`));
663
889
  }
664
890
  }
665
- if (failingExports.length > 0 || driftExports.length > 0) {
666
- error("");
667
- error(chalk.bold("Missing documentation details:"));
668
- for (const { name, missing } of failingExports.slice(0, 10)) {
669
- error(chalk.red(` • ${name}: missing ${missing?.join(", ")}`));
891
+ if (errorCount > 0 || warnCount > 0) {
892
+ const parts = [];
893
+ if (errorCount > 0)
894
+ parts.push(`${errorCount} errors`);
895
+ if (warnCount > 0)
896
+ parts.push(`${warnCount} warnings`);
897
+ log(` Quality: ${parts.join(", ")}`);
898
+ }
899
+ log("");
900
+ const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
901
+ if (!failed) {
902
+ const thresholdParts = [];
903
+ if (minCoverage !== undefined) {
904
+ thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
670
905
  }
671
- for (const drift of driftExports.slice(0, 10)) {
672
- error(chalk.red(` ${drift.name}: ${drift.issue}`));
673
- if (drift.suggestion) {
674
- error(chalk.yellow(` Suggestion: ${drift.suggestion}`));
675
- }
906
+ if (maxDrift !== undefined) {
907
+ thresholdParts.push(`drift ${driftScore}% ${maxDrift}%`);
908
+ }
909
+ if (thresholdParts.length > 0) {
910
+ log(chalk2.green(`✓ Check passed (${thresholdParts.join(", ")})`));
911
+ } else {
912
+ log(chalk2.green("✓ Check passed"));
913
+ log(chalk2.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
676
914
  }
915
+ return;
677
916
  }
917
+ if (hasQualityErrors) {
918
+ log(chalk2.red(`✗ ${errorCount} quality errors`));
919
+ }
920
+ if (hasTypecheckErrors) {
921
+ log(chalk2.red(`✗ ${typecheckErrors.length} example type errors`));
922
+ }
923
+ log("");
924
+ log(chalk2.dim("Use --format json or --format markdown for detailed reports"));
678
925
  process.exit(1);
679
926
  } catch (commandError) {
680
- error(chalk.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
927
+ error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
681
928
  process.exit(1);
682
929
  }
683
930
  });
@@ -688,20 +935,6 @@ function clampCoverage(value) {
688
935
  }
689
936
  return Math.min(100, Math.max(0, Math.round(value)));
690
937
  }
691
- function collectFailingExports(exportsList, minCoverage) {
692
- const offenders = [];
693
- for (const entry of exportsList) {
694
- const exportScore = entry.docs?.coverageScore ?? 0;
695
- const missing = entry.docs?.missing;
696
- if (exportScore < minCoverage || missing && missing.length > 0) {
697
- offenders.push({
698
- name: entry.name,
699
- missing
700
- });
701
- }
702
- }
703
- return offenders;
704
- }
705
938
  function collectDrift(exportsList) {
706
939
  const drifts = [];
707
940
  for (const entry of exportsList) {
@@ -709,11 +942,13 @@ function collectDrift(exportsList) {
709
942
  if (!drift || drift.length === 0) {
710
943
  continue;
711
944
  }
712
- for (const signal of drift) {
945
+ for (const d of drift) {
713
946
  drifts.push({
714
947
  name: entry.name,
715
- issue: signal.issue ?? "Documentation drift detected.",
716
- suggestion: signal.suggestion
948
+ type: d.type,
949
+ issue: d.issue ?? "Documentation drift detected.",
950
+ suggestion: d.suggestion,
951
+ category: DRIFT_CATEGORIES2[d.type]
717
952
  });
718
953
  }
719
954
  }
@@ -721,15 +956,15 @@ function collectDrift(exportsList) {
721
956
  }
722
957
 
723
958
  // src/commands/diff.ts
724
- import * as fs2 from "node:fs";
725
- import * as path3 from "node:path";
959
+ import * as fs3 from "node:fs";
960
+ import * as path4 from "node:path";
726
961
  import {
727
962
  diffSpecWithDocs,
728
963
  getDocsImpactSummary,
729
964
  hasDocsImpact,
730
965
  parseMarkdownFiles
731
966
  } from "@doccov/sdk";
732
- import chalk2 from "chalk";
967
+ import chalk3 from "chalk";
733
968
  import { glob } from "glob";
734
969
 
735
970
  // src/utils/docs-impact-ai.ts
@@ -794,7 +1029,7 @@ Keep it concise and developer-friendly.`
794
1029
 
795
1030
  // src/commands/diff.ts
796
1031
  var defaultDependencies2 = {
797
- readFileSync: fs2.readFileSync,
1032
+ readFileSync: fs3.readFileSync,
798
1033
  log: console.log,
799
1034
  error: console.error
800
1035
  };
@@ -838,7 +1073,7 @@ function registerDiffCommand(program, dependencies = {}) {
838
1073
  const configResult = await loadDocCovConfig(process.cwd());
839
1074
  if (configResult?.docs?.include) {
840
1075
  docsPatterns = configResult.docs.include;
841
- log(chalk2.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1076
+ log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
842
1077
  }
843
1078
  }
844
1079
  if (docsPatterns && docsPatterns.length > 0) {
@@ -867,10 +1102,10 @@ function registerDiffCommand(program, dependencies = {}) {
867
1102
  printTextDiff(diff, log, error);
868
1103
  if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
869
1104
  if (!isAIDocsAnalysisAvailable()) {
870
- log(chalk2.yellow(`
1105
+ log(chalk3.yellow(`
871
1106
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
872
1107
  } else {
873
- log(chalk2.gray(`
1108
+ log(chalk3.gray(`
874
1109
  Generating AI summary...`));
875
1110
  const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
876
1111
  file: f.file,
@@ -881,46 +1116,46 @@ Generating AI summary...`));
881
1116
  const summary = await generateImpactSummary(impacts);
882
1117
  if (summary) {
883
1118
  log("");
884
- log(chalk2.bold("AI Summary"));
885
- log(chalk2.cyan(` ${summary}`));
1119
+ log(chalk3.bold("AI Summary"));
1120
+ log(chalk3.cyan(` ${summary}`));
886
1121
  }
887
1122
  }
888
1123
  }
889
1124
  break;
890
1125
  }
891
1126
  if (strictOptions.has("regression") && diff.coverageDelta < 0) {
892
- error(chalk2.red(`
1127
+ error(chalk3.red(`
893
1128
  Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
894
1129
  process.exitCode = 1;
895
1130
  return;
896
1131
  }
897
1132
  if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
898
- error(chalk2.red(`
1133
+ error(chalk3.red(`
899
1134
  ${diff.driftIntroduced} new drift issue(s) introduced`));
900
1135
  process.exitCode = 1;
901
1136
  return;
902
1137
  }
903
1138
  if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
904
1139
  const summary = getDocsImpactSummary(diff);
905
- error(chalk2.red(`
1140
+ error(chalk3.red(`
906
1141
  ${summary.totalIssues} docs issue(s) require attention`));
907
1142
  process.exitCode = 1;
908
1143
  return;
909
1144
  }
910
1145
  if (strictOptions.has("breaking") && diff.breaking.length > 0) {
911
- error(chalk2.red(`
1146
+ error(chalk3.red(`
912
1147
  ${diff.breaking.length} breaking change(s) detected`));
913
1148
  process.exitCode = 1;
914
1149
  return;
915
1150
  }
916
1151
  if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
917
- error(chalk2.red(`
1152
+ error(chalk3.red(`
918
1153
  ${diff.newUndocumented.length} new undocumented export(s)`));
919
1154
  process.exitCode = 1;
920
1155
  return;
921
1156
  }
922
1157
  } catch (commandError) {
923
- error(chalk2.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1158
+ error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
924
1159
  process.exitCode = 1;
925
1160
  }
926
1161
  });
@@ -934,7 +1169,7 @@ async function loadMarkdownFiles(patterns) {
934
1169
  const matches = await glob(pattern, { nodir: true });
935
1170
  for (const filePath of matches) {
936
1171
  try {
937
- const content = fs2.readFileSync(filePath, "utf-8");
1172
+ const content = fs3.readFileSync(filePath, "utf-8");
938
1173
  files.push({ path: filePath, content });
939
1174
  } catch {}
940
1175
  }
@@ -942,8 +1177,8 @@ async function loadMarkdownFiles(patterns) {
942
1177
  return parseMarkdownFiles(files);
943
1178
  }
944
1179
  function loadSpec(filePath, readFileSync2) {
945
- const resolvedPath = path3.resolve(filePath);
946
- if (!fs2.existsSync(resolvedPath)) {
1180
+ const resolvedPath = path4.resolve(filePath);
1181
+ if (!fs3.existsSync(resolvedPath)) {
947
1182
  throw new Error(`File not found: ${filePath}`);
948
1183
  }
949
1184
  try {
@@ -955,7 +1190,7 @@ function loadSpec(filePath, readFileSync2) {
955
1190
  }
956
1191
  function printTextDiff(diff, log, _error) {
957
1192
  log("");
958
- log(chalk2.bold("DocCov Diff Report"));
1193
+ log(chalk3.bold("DocCov Diff Report"));
959
1194
  log("─".repeat(40));
960
1195
  printCoverage(diff, log);
961
1196
  printAPIChanges(diff, log);
@@ -965,11 +1200,11 @@ function printTextDiff(diff, log, _error) {
965
1200
  log("");
966
1201
  }
967
1202
  function printCoverage(diff, log) {
968
- const coverageColor = diff.coverageDelta > 0 ? chalk2.green : diff.coverageDelta < 0 ? chalk2.red : chalk2.gray;
1203
+ const coverageColor = diff.coverageDelta > 0 ? chalk3.green : diff.coverageDelta < 0 ? chalk3.red : chalk3.gray;
969
1204
  const coverageSymbol = diff.coverageDelta > 0 ? "↑" : diff.coverageDelta < 0 ? "↓" : "→";
970
1205
  const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
971
1206
  log("");
972
- log(chalk2.bold("Coverage"));
1207
+ log(chalk3.bold("Coverage"));
973
1208
  log(` ${diff.oldCoverage}% ${coverageSymbol} ${diff.newCoverage}% ${coverageColor(`(${deltaStr}%)`)}`);
974
1209
  }
975
1210
  function printAPIChanges(diff, log) {
@@ -977,31 +1212,31 @@ function printAPIChanges(diff, log) {
977
1212
  if (!hasChanges)
978
1213
  return;
979
1214
  log("");
980
- log(chalk2.bold("API Changes"));
1215
+ log(chalk3.bold("API Changes"));
981
1216
  const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
982
1217
  const classesWithMembers = new Set(membersByClass.keys());
983
1218
  for (const [className, changes] of membersByClass) {
984
1219
  const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
985
1220
  const isHighSeverity = categorized?.severity === "high";
986
- const label = isHighSeverity ? chalk2.red(" [BREAKING]") : chalk2.yellow(" [CHANGED]");
987
- log(chalk2.cyan(` ${className}`) + label);
1221
+ const label = isHighSeverity ? chalk3.red(" [BREAKING]") : chalk3.yellow(" [CHANGED]");
1222
+ log(chalk3.cyan(` ${className}`) + label);
988
1223
  const removed = changes.filter((c) => c.changeType === "removed");
989
1224
  for (const mc of removed) {
990
- const suggestion = mc.suggestion ? chalk2.gray(` → ${mc.suggestion}`) : "";
991
- log(chalk2.red(` ✖ ${mc.memberName}()`) + suggestion);
1225
+ const suggestion = mc.suggestion ? chalk3.gray(` → ${mc.suggestion}`) : "";
1226
+ log(chalk3.red(` ✖ ${mc.memberName}()`) + suggestion);
992
1227
  }
993
1228
  const changed = changes.filter((c) => c.changeType === "signature-changed");
994
1229
  for (const mc of changed) {
995
- log(chalk2.yellow(` ~ ${mc.memberName}() signature changed`));
1230
+ log(chalk3.yellow(` ~ ${mc.memberName}() signature changed`));
996
1231
  if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
997
- log(chalk2.gray(` was: ${mc.oldSignature}`));
998
- log(chalk2.gray(` now: ${mc.newSignature}`));
1232
+ log(chalk3.gray(` was: ${mc.oldSignature}`));
1233
+ log(chalk3.gray(` now: ${mc.newSignature}`));
999
1234
  }
1000
1235
  }
1001
1236
  const added = changes.filter((c) => c.changeType === "added");
1002
1237
  if (added.length > 0) {
1003
1238
  const addedNames = added.map((a) => `${a.memberName}()`).join(", ");
1004
- log(chalk2.green(` + ${addedNames}`));
1239
+ log(chalk3.green(` + ${addedNames}`));
1005
1240
  }
1006
1241
  }
1007
1242
  const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
@@ -1010,43 +1245,43 @@ function printAPIChanges(diff, log) {
1010
1245
  const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
1011
1246
  if (functionChanges.length > 0) {
1012
1247
  log("");
1013
- log(chalk2.red(` Function Changes (${functionChanges.length}):`));
1248
+ log(chalk3.red(` Function Changes (${functionChanges.length}):`));
1014
1249
  for (const fc of functionChanges.slice(0, 3)) {
1015
1250
  const reason = fc.reason === "removed" ? "removed" : "signature changed";
1016
- log(chalk2.red(` ✖ ${fc.name} (${reason})`));
1251
+ log(chalk3.red(` ✖ ${fc.name} (${reason})`));
1017
1252
  }
1018
1253
  if (functionChanges.length > 3) {
1019
- log(chalk2.gray(` ... and ${functionChanges.length - 3} more`));
1254
+ log(chalk3.gray(` ... and ${functionChanges.length - 3} more`));
1020
1255
  }
1021
1256
  }
1022
1257
  if (typeChanges.length > 0) {
1023
1258
  log("");
1024
- log(chalk2.yellow(` Type/Interface Changes (${typeChanges.length}):`));
1259
+ log(chalk3.yellow(` Type/Interface Changes (${typeChanges.length}):`));
1025
1260
  const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
1026
- log(chalk2.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
1261
+ log(chalk3.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
1027
1262
  }
1028
1263
  if (otherChanges.length > 0) {
1029
1264
  log("");
1030
- log(chalk2.gray(` Other Changes (${otherChanges.length}):`));
1265
+ log(chalk3.gray(` Other Changes (${otherChanges.length}):`));
1031
1266
  const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
1032
- log(chalk2.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
1267
+ log(chalk3.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
1033
1268
  }
1034
1269
  if (diff.nonBreaking.length > 0) {
1035
1270
  const undocCount = diff.newUndocumented.length;
1036
- const undocSuffix = undocCount > 0 ? chalk2.yellow(` (${undocCount} undocumented)`) : "";
1271
+ const undocSuffix = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
1037
1272
  log("");
1038
- log(chalk2.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
1273
+ log(chalk3.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
1039
1274
  const exportNames = diff.nonBreaking.slice(0, 3);
1040
- log(chalk2.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
1275
+ log(chalk3.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
1041
1276
  }
1042
1277
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1043
1278
  log("");
1044
1279
  const parts = [];
1045
1280
  if (diff.driftIntroduced > 0) {
1046
- parts.push(chalk2.red(`+${diff.driftIntroduced} drift`));
1281
+ parts.push(chalk3.red(`+${diff.driftIntroduced} drift`));
1047
1282
  }
1048
1283
  if (diff.driftResolved > 0) {
1049
- parts.push(chalk2.green(`-${diff.driftResolved} resolved`));
1284
+ parts.push(chalk3.green(`-${diff.driftResolved} resolved`));
1050
1285
  }
1051
1286
  log(` Drift: ${parts.join(", ")}`);
1052
1287
  }
@@ -1056,10 +1291,10 @@ function printDocsRequiringUpdates(diff, log) {
1056
1291
  return;
1057
1292
  const { impactedFiles, missingDocs, stats } = diff.docsImpact;
1058
1293
  log("");
1059
- log(chalk2.bold("Docs Requiring Updates"));
1060
- log(chalk2.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
1294
+ log(chalk3.bold("Docs Requiring Updates"));
1295
+ log(chalk3.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
1061
1296
  if (impactedFiles.length === 0 && missingDocs.length === 0) {
1062
- log(chalk2.green(" ✓ No updates needed"));
1297
+ log(chalk3.green(" ✓ No updates needed"));
1063
1298
  return;
1064
1299
  }
1065
1300
  const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
@@ -1074,49 +1309,49 @@ function printDocsRequiringUpdates(diff, log) {
1074
1309
  }
1075
1310
  }
1076
1311
  for (const file of actionableFiles.slice(0, 6)) {
1077
- const filename = path3.basename(file.file);
1312
+ const filename = path4.basename(file.file);
1078
1313
  const issueCount = file.references.length;
1079
1314
  log("");
1080
- log(chalk2.yellow(` ${filename}`) + chalk2.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
1315
+ log(chalk3.yellow(` ${filename}`) + chalk3.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
1081
1316
  const actionableRefs = file.references.filter((r) => !r.isInstantiation);
1082
1317
  for (const ref of actionableRefs.slice(0, 4)) {
1083
1318
  if (ref.memberName) {
1084
1319
  const action = ref.changeType === "method-removed" ? "→" : "~";
1085
1320
  const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
1086
- log(chalk2.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
1321
+ log(chalk3.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
1087
1322
  } else {
1088
1323
  const action = ref.changeType === "removed" ? "→" : "~";
1089
1324
  const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
1090
- log(chalk2.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
1325
+ log(chalk3.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
1091
1326
  }
1092
1327
  }
1093
1328
  if (actionableRefs.length > 4) {
1094
- log(chalk2.gray(` ... and ${actionableRefs.length - 4} more`));
1329
+ log(chalk3.gray(` ... and ${actionableRefs.length - 4} more`));
1095
1330
  }
1096
1331
  }
1097
1332
  if (actionableFiles.length > 6) {
1098
- log(chalk2.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
1333
+ log(chalk3.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
1099
1334
  }
1100
1335
  if (instantiationOnlyFiles.length > 0) {
1101
1336
  log("");
1102
- const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path3.basename(f.file));
1337
+ const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path4.basename(f.file));
1103
1338
  const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
1104
- log(chalk2.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
1105
- log(chalk2.gray(` ${fileNames.join(", ")}${suffix}`));
1339
+ log(chalk3.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
1340
+ log(chalk3.gray(` ${fileNames.join(", ")}${suffix}`));
1106
1341
  }
1107
1342
  const { allUndocumented } = diff.docsImpact;
1108
1343
  if (missingDocs.length > 0) {
1109
1344
  log("");
1110
- log(chalk2.yellow(` New exports missing docs (${missingDocs.length}):`));
1345
+ log(chalk3.yellow(` New exports missing docs (${missingDocs.length}):`));
1111
1346
  const names = missingDocs.slice(0, 4);
1112
- log(chalk2.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
1347
+ log(chalk3.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
1113
1348
  }
1114
1349
  if (allUndocumented && allUndocumented.length > 0) {
1115
1350
  const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
1116
1351
  log("");
1117
- log(chalk2.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
1352
+ log(chalk3.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
1118
1353
  if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
1119
- log(chalk2.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
1354
+ log(chalk3.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
1120
1355
  }
1121
1356
  }
1122
1357
  }
@@ -1267,7 +1502,7 @@ function generateHTMLReport(diff) {
1267
1502
  <h2>Documentation Impact</h2>
1268
1503
  <div class="card">`;
1269
1504
  for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
1270
- const filename = path3.basename(file.file);
1505
+ const filename = path4.basename(file.file);
1271
1506
  html += `
1272
1507
  <div class="file-item">
1273
1508
  <div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
@@ -1301,278 +1536,39 @@ function generateHTMLReport(diff) {
1301
1536
  <ul>`;
1302
1537
  for (const name of diff.newUndocumented.slice(0, 10)) {
1303
1538
  html += `
1304
- <li><code>${name}</code></li>`;
1305
- }
1306
- if (diff.newUndocumented.length > 10) {
1307
- html += `
1308
- <li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
1309
- }
1310
- html += `
1311
- </ul>`;
1312
- }
1313
- if (diff.docsImpact?.stats) {
1314
- const { stats, allUndocumented } = diff.docsImpact;
1315
- const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
1316
- html += `
1317
- <div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
1318
- <span class="metric-label">Total Documentation Coverage</span>
1319
- <span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
1320
- </div>`;
1321
- }
1322
- html += `
1323
- </div>`;
1324
- }
1325
- html += `
1326
- </div>
1327
- </body>
1328
- </html>`;
1329
- return html;
1330
- }
1331
-
1332
- // src/commands/generate.ts
1333
- import * as fs3 from "node:fs";
1334
- import * as path4 from "node:path";
1335
- import {
1336
- DocCov as DocCov2,
1337
- NodeFileSystem as NodeFileSystem2,
1338
- resolveTarget as resolveTarget2
1339
- } from "@doccov/sdk";
1340
- import { normalize, validateSpec } from "@openpkg-ts/spec";
1341
- import chalk4 from "chalk";
1342
-
1343
- // src/reports/markdown.ts
1344
- function bar(pct, width = 10) {
1345
- const filled = Math.round(pct / 100 * width);
1346
- return "█".repeat(filled) + "░".repeat(width - filled);
1347
- }
1348
- function renderMarkdown(stats, options = {}) {
1349
- const limit = options.limit ?? 20;
1350
- const lines = [];
1351
- lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
1352
- lines.push("");
1353
- lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
1354
- lines.push("");
1355
- lines.push("| Metric | Value |");
1356
- lines.push("|--------|-------|");
1357
- lines.push(`| Exports | ${stats.totalExports} |`);
1358
- lines.push(`| Fully documented | ${stats.fullyDocumented} |`);
1359
- lines.push(`| Partially documented | ${stats.partiallyDocumented} |`);
1360
- lines.push(`| Undocumented | ${stats.undocumented} |`);
1361
- lines.push(`| Drift issues | ${stats.driftCount} |`);
1362
- lines.push("");
1363
- lines.push("## Coverage by Signal");
1364
- lines.push("");
1365
- lines.push("| Signal | Coverage |");
1366
- lines.push("|--------|----------|");
1367
- for (const [sig, s] of Object.entries(stats.signalCoverage)) {
1368
- lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
1369
- }
1370
- if (stats.byKind.length > 0) {
1371
- lines.push("");
1372
- lines.push("## Coverage by Kind");
1373
- lines.push("");
1374
- lines.push("| Kind | Count | Avg Score |");
1375
- lines.push("|------|-------|-----------|");
1376
- for (const k of stats.byKind) {
1377
- lines.push(`| ${k.kind} | ${k.count} | ${k.avgScore}% |`);
1378
- }
1379
- }
1380
- const lowExports = stats.exports.filter((e) => e.score < 100).slice(0, limit);
1381
- if (lowExports.length > 0) {
1382
- lines.push("");
1383
- lines.push("## Lowest Coverage Exports");
1384
- lines.push("");
1385
- lines.push("| Export | Kind | Score | Missing |");
1386
- lines.push("|--------|------|-------|---------|");
1387
- for (const e of lowExports) {
1388
- lines.push(`| \`${e.name}\` | ${e.kind} | ${e.score}% | ${e.missing.join(", ") || "-"} |`);
1389
- }
1390
- const totalLow = stats.exports.filter((e) => e.score < 100).length;
1391
- if (totalLow > limit) {
1392
- lines.push(`| ... | | | ${totalLow - limit} more |`);
1393
- }
1394
- }
1395
- if (stats.driftIssues.length > 0) {
1396
- lines.push("");
1397
- lines.push("## Drift Issues");
1398
- lines.push("");
1399
- lines.push("| Export | Type | Issue |");
1400
- lines.push("|--------|------|-------|");
1401
- for (const d of stats.driftIssues.slice(0, limit)) {
1402
- const hint = d.suggestion ? ` → ${d.suggestion}` : "";
1403
- lines.push(`| \`${d.exportName}\` | ${d.type} | ${d.issue}${hint} |`);
1404
- }
1405
- }
1406
- lines.push("");
1407
- lines.push("---");
1408
- lines.push("*Generated by [DocCov](https://doccov.com)*");
1409
- return lines.join(`
1410
- `);
1411
- }
1412
-
1413
- // src/reports/html.ts
1414
- function escapeHtml(s) {
1415
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1416
- }
1417
- function renderHtml(stats, options = {}) {
1418
- const md = renderMarkdown(stats, options);
1419
- return `<!DOCTYPE html>
1420
- <html lang="en">
1421
- <head>
1422
- <meta charset="UTF-8">
1423
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1424
- <title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
1425
- <style>
1426
- :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
1427
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
1428
- h1, h2 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
1429
- table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
1430
- th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
1431
- th { background: #161b22; }
1432
- code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
1433
- a { color: var(--accent); }
1434
- </style>
1435
- </head>
1436
- <body>
1437
- <pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
1438
- </body>
1439
- </html>`;
1440
- }
1441
- // src/reports/stats.ts
1442
- function computeStats(spec) {
1443
- const exports = spec.exports ?? [];
1444
- const signals = {
1445
- description: { covered: 0, total: 0 },
1446
- params: { covered: 0, total: 0 },
1447
- returns: { covered: 0, total: 0 },
1448
- examples: { covered: 0, total: 0 }
1449
- };
1450
- const kindMap = new Map;
1451
- const driftIssues = [];
1452
- let fullyDocumented = 0;
1453
- let partiallyDocumented = 0;
1454
- let undocumented = 0;
1455
- for (const exp of exports) {
1456
- const score = exp.docs?.coverageScore ?? 0;
1457
- const missing = exp.docs?.missing ?? [];
1458
- for (const sig of ["description", "params", "returns", "examples"]) {
1459
- signals[sig].total++;
1460
- if (!missing.includes(sig))
1461
- signals[sig].covered++;
1462
- }
1463
- const kindEntry = kindMap.get(exp.kind) ?? { count: 0, totalScore: 0 };
1464
- kindEntry.count++;
1465
- kindEntry.totalScore += score;
1466
- kindMap.set(exp.kind, kindEntry);
1467
- if (score === 100)
1468
- fullyDocumented++;
1469
- else if (score > 0)
1470
- partiallyDocumented++;
1471
- else
1472
- undocumented++;
1473
- for (const d of exp.docs?.drift ?? []) {
1474
- driftIssues.push({
1475
- exportName: exp.name,
1476
- type: d.type,
1477
- issue: d.issue,
1478
- suggestion: d.suggestion
1479
- });
1480
- }
1481
- }
1482
- const signalCoverage = Object.fromEntries(Object.entries(signals).map(([k, v]) => [
1483
- k,
1484
- { ...v, pct: v.total ? Math.round(v.covered / v.total * 100) : 0 }
1485
- ]));
1486
- const byKind = Array.from(kindMap.entries()).map(([kind, { count, totalScore }]) => ({
1487
- kind,
1488
- count,
1489
- avgScore: Math.round(totalScore / count)
1490
- })).sort((a, b) => b.count - a.count);
1491
- const sortedExports = exports.map((e) => ({
1492
- name: e.name,
1493
- kind: e.kind,
1494
- score: e.docs?.coverageScore ?? 0,
1495
- missing: e.docs?.missing ?? []
1496
- })).sort((a, b) => a.score - b.score);
1497
- return {
1498
- packageName: spec.meta.name ?? "unknown",
1499
- version: spec.meta.version ?? "0.0.0",
1500
- coverageScore: spec.docs?.coverageScore ?? 0,
1501
- totalExports: exports.length,
1502
- fullyDocumented,
1503
- partiallyDocumented,
1504
- undocumented,
1505
- driftCount: driftIssues.length,
1506
- signalCoverage,
1507
- byKind,
1508
- exports: sortedExports,
1509
- driftIssues
1510
- };
1511
- }
1512
- // src/utils/filter-options.ts
1513
- import { mergeFilters, parseListFlag } from "@doccov/sdk";
1514
- import chalk3 from "chalk";
1515
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
1516
- var mergeFilterOptions = (config, cliOptions) => {
1517
- const messages = [];
1518
- if (config?.include) {
1519
- messages.push(formatList("include filters from config", config.include));
1520
- }
1521
- if (config?.exclude) {
1522
- messages.push(formatList("exclude filters from config", config.exclude));
1523
- }
1524
- if (cliOptions.include) {
1525
- messages.push(formatList("apply include filters from CLI", cliOptions.include));
1526
- }
1527
- if (cliOptions.exclude) {
1528
- messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
1529
- }
1530
- const resolved = mergeFilters(config, cliOptions);
1531
- if (!resolved.include && !resolved.exclude) {
1532
- return { messages };
1539
+ <li><code>${name}</code></li>`;
1540
+ }
1541
+ if (diff.newUndocumented.length > 10) {
1542
+ html += `
1543
+ <li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
1544
+ }
1545
+ html += `
1546
+ </ul>`;
1547
+ }
1548
+ if (diff.docsImpact?.stats) {
1549
+ const { stats, allUndocumented } = diff.docsImpact;
1550
+ const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
1551
+ html += `
1552
+ <div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
1553
+ <span class="metric-label">Total Documentation Coverage</span>
1554
+ <span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
1555
+ </div>`;
1556
+ }
1557
+ html += `
1558
+ </div>`;
1533
1559
  }
1534
- const source = resolved.source === "override" ? "cli" : resolved.source;
1535
- return {
1536
- include: resolved.include,
1537
- exclude: resolved.exclude,
1538
- source,
1539
- messages
1540
- };
1541
- };
1542
-
1543
- // src/commands/generate.ts
1544
- var defaultDependencies3 = {
1545
- createDocCov: (options) => new DocCov2(options),
1546
- writeFileSync: fs3.writeFileSync,
1547
- log: console.log,
1548
- error: console.error
1549
- };
1550
- function getArrayLength(value) {
1551
- return Array.isArray(value) ? value.length : 0;
1552
- }
1553
- function stripDocsFields(spec) {
1554
- const { docs: _rootDocs, ...rest } = spec;
1555
- return {
1556
- ...rest,
1557
- exports: spec.exports?.map((exp) => {
1558
- const { docs: _expDocs, ...expRest } = exp;
1559
- return expRest;
1560
- })
1561
- };
1562
- }
1563
- function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
1564
- const location = diagnostic.location;
1565
- const relativePath = location?.file ? path4.relative(baseDir, location.file) || location.file : undefined;
1566
- const locationText = location && relativePath ? chalk4.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
1567
- const locationPrefix = locationText ? `${locationText} ` : "";
1568
- return `${prefix} ${locationPrefix}${diagnostic.message}`;
1560
+ html += `
1561
+ </div>
1562
+ </body>
1563
+ </html>`;
1564
+ return html;
1569
1565
  }
1570
- function registerGenerateCommand(program, dependencies = {}) {
1571
- const { createDocCov, writeFileSync: writeFileSync2, log, error } = {
1572
- ...defaultDependencies3,
1573
- ...dependencies
1574
- };
1575
- program.command("generate [entry]").description("Generate OpenPkg specification for documentation coverage analysis").option("-o, --output <file>", "Output file", "openpkg.json").option("--format <format>", "Output format: json, markdown, html", "json").option("-p, --package <name>", "Target package name (for monorepos)").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-resolve", "Skip external type resolution from node_modules").option("--include <ids>", "Filter exports by identifier (comma-separated or repeated)").option("--exclude <ids>", "Exclude exports by identifier (comma-separated or repeated)").option("--show-diagnostics", "Print TypeScript diagnostics from analysis").option("--no-docs", "Omit docs coverage fields from output (pure structural spec)").option("--limit <n>", "Max exports to show in report tables (for markdown/html)", "20").option("-y, --yes", "Skip all prompts and use defaults").action(async (entry, options) => {
1566
+
1567
+ // src/commands/info.ts
1568
+ import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1569
+ import chalk4 from "chalk";
1570
+ function registerInfoCommand(program) {
1571
+ program.command("info [entry]").description("Show brief documentation coverage summary").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution from node_modules").action(async (entry, options) => {
1576
1572
  try {
1577
1573
  const fileSystem = new NodeFileSystem2(options.cwd);
1578
1574
  const resolved = await resolveTarget2(fileSystem, {
@@ -1580,94 +1576,26 @@ function registerGenerateCommand(program, dependencies = {}) {
1580
1576
  package: options.package,
1581
1577
  entry
1582
1578
  });
1583
- const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
1584
- if (packageInfo) {
1585
- log(chalk4.gray(`Found package at ${packageInfo.path}`));
1586
- }
1587
- if (!entry) {
1588
- log(chalk4.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
1589
- }
1579
+ const { entryFile } = resolved;
1590
1580
  const resolveExternalTypes = !options.skipResolve;
1591
- const cliFilters = {
1592
- include: parseListFlag(options.include),
1593
- exclude: parseListFlag(options.exclude)
1594
- };
1595
- let config = null;
1596
- try {
1597
- config = await loadDocCovConfig(targetDir);
1598
- if (config?.filePath) {
1599
- log(chalk4.gray(`Loaded configuration from ${path4.relative(targetDir, config.filePath)}`));
1600
- }
1601
- } catch (configError) {
1602
- error(chalk4.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
1603
- process.exit(1);
1604
- }
1605
- const resolvedFilters = mergeFilterOptions(config, cliFilters);
1606
- for (const message of resolvedFilters.messages) {
1607
- log(chalk4.gray(`• ${message}`));
1608
- }
1609
- process.stdout.write(chalk4.cyan(`> Generating OpenPkg spec...
1610
- `));
1611
- let result;
1612
- try {
1613
- const doccov = createDocCov({
1614
- resolveExternalTypes
1615
- });
1616
- const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
1617
- filters: {
1618
- include: resolvedFilters.include,
1619
- exclude: resolvedFilters.exclude
1620
- }
1621
- } : {};
1622
- result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1623
- process.stdout.write(chalk4.green(`✓ Generated OpenPkg spec
1624
- `));
1625
- } catch (generationError) {
1626
- process.stdout.write(chalk4.red(`✗ Failed to generate spec
1627
- `));
1628
- throw generationError;
1629
- }
1630
- if (!result) {
1631
- throw new Error("Failed to produce an OpenPkg spec.");
1632
- }
1633
- let normalized = normalize(result.spec);
1634
- if (options.docs === false) {
1635
- normalized = stripDocsFields(normalized);
1636
- }
1637
- const validation = validateSpec(normalized);
1638
- if (!validation.ok) {
1639
- error(chalk4.red("Spec failed schema validation"));
1640
- for (const err of validation.errors) {
1641
- error(chalk4.red(`schema: ${err.instancePath || "/"} ${err.message}`));
1642
- }
1643
- process.exit(1);
1644
- }
1645
- const format = options.format ?? "json";
1646
- const outputPath = path4.resolve(process.cwd(), options.output);
1647
- if (format === "markdown" || format === "html") {
1648
- const stats = computeStats(normalized);
1649
- const limit = parseInt(options.limit, 10) || 20;
1650
- const reportOutput = format === "html" ? renderHtml(stats, { limit }) : renderMarkdown(stats, { limit });
1651
- writeFileSync2(outputPath, reportOutput);
1652
- log(chalk4.green(`✓ Generated ${format} report: ${options.output}`));
1653
- log(chalk4.gray(` Coverage: ${stats.coverageScore}%`));
1654
- log(chalk4.gray(` ${stats.totalExports} exports, ${stats.driftCount} drift issues`));
1655
- } else {
1656
- writeFileSync2(outputPath, JSON.stringify(normalized, null, 2));
1657
- log(chalk4.green(`✓ Generated ${options.output}`));
1658
- log(chalk4.gray(` ${getArrayLength(normalized.exports)} exports`));
1659
- log(chalk4.gray(` ${getArrayLength(normalized.types)} types`));
1660
- }
1661
- if (options.showDiagnostics && result.diagnostics.length > 0) {
1662
- log("");
1663
- log(chalk4.bold("Diagnostics"));
1664
- for (const diagnostic of result.diagnostics) {
1665
- const prefix = diagnostic.severity === "error" ? chalk4.red("✖") : diagnostic.severity === "warning" ? chalk4.yellow("⚠") : chalk4.cyan("ℹ");
1666
- log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
1667
- }
1581
+ const doccov = new DocCov2({
1582
+ resolveExternalTypes
1583
+ });
1584
+ const specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
1585
+ if (!specResult) {
1586
+ throw new Error("Failed to analyze documentation coverage.");
1668
1587
  }
1669
- } catch (commandError) {
1670
- error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1588
+ const spec = enrichSpec2(specResult.spec);
1589
+ const stats = computeStats(spec);
1590
+ console.log("");
1591
+ console.log(chalk4.bold(`${stats.packageName}@${stats.version}`));
1592
+ console.log("");
1593
+ console.log(` Exports: ${chalk4.bold(stats.totalExports.toString())}`);
1594
+ console.log(` Coverage: ${chalk4.bold(`${stats.coverageScore}%`)}`);
1595
+ console.log(` Drift: ${chalk4.bold(`${stats.driftScore}%`)}`);
1596
+ console.log("");
1597
+ } catch (err) {
1598
+ console.error(chalk4.red("Error:"), err instanceof Error ? err.message : err);
1671
1599
  process.exit(1);
1672
1600
  }
1673
1601
  });
@@ -1677,7 +1605,7 @@ function registerGenerateCommand(program, dependencies = {}) {
1677
1605
  import * as fs4 from "node:fs";
1678
1606
  import * as path5 from "node:path";
1679
1607
  import chalk5 from "chalk";
1680
- var defaultDependencies4 = {
1608
+ var defaultDependencies3 = {
1681
1609
  fileExists: fs4.existsSync,
1682
1610
  writeFileSync: fs4.writeFileSync,
1683
1611
  readFileSync: fs4.readFileSync,
@@ -1686,7 +1614,7 @@ var defaultDependencies4 = {
1686
1614
  };
1687
1615
  function registerInitCommand(program, dependencies = {}) {
1688
1616
  const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync3, log, error } = {
1689
- ...defaultDependencies4,
1617
+ ...defaultDependencies3,
1690
1618
  ...dependencies
1691
1619
  };
1692
1620
  program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
@@ -1779,14 +1707,40 @@ var resolveFormat = (format, packageType) => {
1779
1707
  return format;
1780
1708
  };
1781
1709
  var buildTemplate = (format) => {
1710
+ const configBody = `{
1711
+ // Filter which exports to analyze
1712
+ // include: ['MyClass', 'myFunction'],
1713
+ // exclude: ['internal*'],
1714
+
1715
+ // Check command thresholds
1716
+ check: {
1717
+ // Minimum documentation coverage percentage (0-100)
1718
+ // minCoverage: 80,
1719
+
1720
+ // Maximum drift percentage allowed (0-100)
1721
+ // maxDrift: 20,
1722
+
1723
+ // Example validation: 'presence' | 'typecheck' | 'run'
1724
+ // examples: 'typecheck',
1725
+ },
1726
+
1727
+ // Quality rule severities: 'error' | 'warn' | 'off'
1728
+ quality: {
1729
+ rules: {
1730
+ // 'has-description': 'warn',
1731
+ // 'has-params': 'off',
1732
+ // 'has-returns': 'off',
1733
+ // 'has-examples': 'off',
1734
+ // 'no-empty-returns': 'warn',
1735
+ // 'consistent-param-style': 'off',
1736
+ },
1737
+ },
1738
+ }`;
1782
1739
  if (format === "cjs") {
1783
1740
  return [
1784
1741
  "const { defineConfig } = require('@doccov/cli/config');",
1785
1742
  "",
1786
- "module.exports = defineConfig({",
1787
- " include: [],",
1788
- " exclude: [],",
1789
- "});",
1743
+ `module.exports = defineConfig(${configBody});`,
1790
1744
  ""
1791
1745
  ].join(`
1792
1746
  `);
@@ -1794,10 +1748,7 @@ var buildTemplate = (format) => {
1794
1748
  return [
1795
1749
  "import { defineConfig } from '@doccov/cli/config';",
1796
1750
  "",
1797
- "export default defineConfig({",
1798
- " include: [],",
1799
- " exclude: [],",
1800
- "});",
1751
+ `export default defineConfig(${configBody});`,
1801
1752
  ""
1802
1753
  ].join(`
1803
1754
  `);
@@ -1809,9 +1760,9 @@ import * as fsPromises from "node:fs/promises";
1809
1760
  import * as os from "node:os";
1810
1761
  import * as path7 from "node:path";
1811
1762
  import {
1812
- DocCov as DocCov3,
1813
1763
  buildCloneUrl,
1814
1764
  buildDisplayUrl,
1765
+ DocCov as DocCov3,
1815
1766
  detectBuildInfo,
1816
1767
  detectEntryPoint,
1817
1768
  detectMonorepo,
@@ -1823,6 +1774,10 @@ import {
1823
1774
  NodeFileSystem as NodeFileSystem3,
1824
1775
  parseGitHubUrl
1825
1776
  } from "@doccov/sdk";
1777
+ import {
1778
+ DRIFT_CATEGORIES as DRIFT_CATEGORIES3,
1779
+ DRIFT_CATEGORY_LABELS as DRIFT_CATEGORY_LABELS2
1780
+ } from "@openpkg-ts/spec";
1826
1781
  import chalk6 from "chalk";
1827
1782
  import { simpleGit } from "simple-git";
1828
1783
 
@@ -1919,14 +1874,14 @@ async function generateBuildPlan(repoDir) {
1919
1874
  }
1920
1875
 
1921
1876
  // src/commands/scan.ts
1922
- var defaultDependencies5 = {
1877
+ var defaultDependencies4 = {
1923
1878
  createDocCov: (options) => new DocCov3(options),
1924
1879
  log: console.log,
1925
1880
  error: console.error
1926
1881
  };
1927
1882
  function registerScanCommand(program, dependencies = {}) {
1928
1883
  const { createDocCov, log, error } = {
1929
- ...defaultDependencies5,
1884
+ ...defaultDependencies4,
1930
1885
  ...dependencies
1931
1886
  };
1932
1887
  program.command("scan <url>").description("Analyze docs coverage for any public GitHub repository").option("--ref <branch>", "Branch or tag to analyze").option("--package <name>", "Target package in monorepo").option("--output <format>", "Output format: text or json", "text").option("--no-cleanup", "Keep cloned repo (for debugging)").option("--skip-install", "Skip dependency installation (faster, but may limit type resolution)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--save-spec <path>", "Save full OpenPkg spec to file").action(async (url, options) => {
@@ -2206,6 +2161,44 @@ function registerScanCommand(program, dependencies = {}) {
2206
2161
  }
2207
2162
  });
2208
2163
  }
2164
+ function categorizeDriftIssues(drift) {
2165
+ const byCategory = {
2166
+ structural: [],
2167
+ semantic: [],
2168
+ example: []
2169
+ };
2170
+ for (const d of drift) {
2171
+ const category = DRIFT_CATEGORIES3[d.type] ?? "semantic";
2172
+ byCategory[category].push(d);
2173
+ }
2174
+ return {
2175
+ summary: {
2176
+ total: drift.length,
2177
+ byCategory: {
2178
+ structural: byCategory.structural.length,
2179
+ semantic: byCategory.semantic.length,
2180
+ example: byCategory.example.length
2181
+ }
2182
+ },
2183
+ byCategory
2184
+ };
2185
+ }
2186
+ function formatDriftSummary(summary) {
2187
+ if (summary.total === 0) {
2188
+ return "No drift detected";
2189
+ }
2190
+ const parts = [];
2191
+ if (summary.byCategory.structural > 0) {
2192
+ parts.push(`${summary.byCategory.structural} structural`);
2193
+ }
2194
+ if (summary.byCategory.semantic > 0) {
2195
+ parts.push(`${summary.byCategory.semantic} semantic`);
2196
+ }
2197
+ if (summary.byCategory.example > 0) {
2198
+ parts.push(`${summary.byCategory.example} example`);
2199
+ }
2200
+ return `${summary.total} issues (${parts.join(", ")})`;
2201
+ }
2209
2202
  function printTextResult(result, log) {
2210
2203
  log("");
2211
2204
  log(chalk6.bold("DocCov Scan Results"));
@@ -2222,7 +2215,9 @@ function printTextResult(result, log) {
2222
2215
  log(` ${result.exportCount} exports`);
2223
2216
  log(` ${result.typeCount} types`);
2224
2217
  log(` ${result.undocumented.length} undocumented`);
2225
- log(` ${result.driftCount} drift issues`);
2218
+ const categorized = categorizeDriftIssues(result.drift);
2219
+ const driftColor = result.driftCount > 0 ? chalk6.yellow : chalk6.green;
2220
+ log(` ${driftColor(formatDriftSummary(categorized.summary))}`);
2226
2221
  if (result.undocumented.length > 0) {
2227
2222
  log("");
2228
2223
  log(chalk6.bold("Undocumented Exports"));
@@ -2236,24 +2231,189 @@ function printTextResult(result, log) {
2236
2231
  if (result.drift.length > 0) {
2237
2232
  log("");
2238
2233
  log(chalk6.bold("Drift Issues"));
2239
- for (const d of result.drift.slice(0, 5)) {
2240
- log(chalk6.red(` • ${d.export}: ${d.issue}`));
2241
- }
2242
- if (result.drift.length > 5) {
2243
- log(chalk6.gray(` ... and ${result.drift.length - 5} more`));
2234
+ const categories = ["structural", "semantic", "example"];
2235
+ for (const category of categories) {
2236
+ const issues = categorized.byCategory[category];
2237
+ if (issues.length === 0)
2238
+ continue;
2239
+ const label = DRIFT_CATEGORY_LABELS2[category];
2240
+ log("");
2241
+ log(chalk6.dim(` ${label} (${issues.length})`));
2242
+ for (const d of issues.slice(0, 3)) {
2243
+ log(chalk6.red(` • ${d.export}: ${d.issue}`));
2244
+ }
2245
+ if (issues.length > 3) {
2246
+ log(chalk6.gray(` ... and ${issues.length - 3} more`));
2247
+ }
2244
2248
  }
2245
2249
  }
2246
2250
  log("");
2247
2251
  }
2248
2252
 
2253
+ // src/commands/spec.ts
2254
+ import * as fs7 from "node:fs";
2255
+ import * as path8 from "node:path";
2256
+ import { DocCov as DocCov4, NodeFileSystem as NodeFileSystem4, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2257
+ import { normalize, validateSpec } from "@openpkg-ts/spec";
2258
+ import chalk8 from "chalk";
2259
+
2260
+ // src/utils/filter-options.ts
2261
+ import { mergeFilters, parseListFlag } from "@doccov/sdk";
2262
+ import chalk7 from "chalk";
2263
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
2264
+ var mergeFilterOptions = (config, cliOptions) => {
2265
+ const messages = [];
2266
+ if (config?.include) {
2267
+ messages.push(formatList("include filters from config", config.include));
2268
+ }
2269
+ if (config?.exclude) {
2270
+ messages.push(formatList("exclude filters from config", config.exclude));
2271
+ }
2272
+ if (cliOptions.include) {
2273
+ messages.push(formatList("apply include filters from CLI", cliOptions.include));
2274
+ }
2275
+ if (cliOptions.exclude) {
2276
+ messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
2277
+ }
2278
+ const resolved = mergeFilters(config, cliOptions);
2279
+ if (!resolved.include && !resolved.exclude) {
2280
+ return { messages };
2281
+ }
2282
+ const source = resolved.source === "override" ? "cli" : resolved.source;
2283
+ return {
2284
+ include: resolved.include,
2285
+ exclude: resolved.exclude,
2286
+ source,
2287
+ messages
2288
+ };
2289
+ };
2290
+
2291
+ // src/commands/spec.ts
2292
+ var defaultDependencies5 = {
2293
+ createDocCov: (options) => new DocCov4(options),
2294
+ writeFileSync: fs7.writeFileSync,
2295
+ log: console.log,
2296
+ error: console.error
2297
+ };
2298
+ function getArrayLength(value) {
2299
+ return Array.isArray(value) ? value.length : 0;
2300
+ }
2301
+ function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2302
+ const location = diagnostic.location;
2303
+ const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
2304
+ const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2305
+ const locationPrefix = locationText ? `${locationText} ` : "";
2306
+ return `${prefix} ${locationPrefix}${diagnostic.message}`;
2307
+ }
2308
+ function registerSpecCommand(program, dependencies = {}) {
2309
+ const { createDocCov, writeFileSync: writeFileSync5, log, error } = {
2310
+ ...defaultDependencies5,
2311
+ ...dependencies
2312
+ };
2313
+ program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").action(async (entry, options) => {
2314
+ try {
2315
+ const fileSystem = new NodeFileSystem4(options.cwd);
2316
+ const resolved = await resolveTarget3(fileSystem, {
2317
+ cwd: options.cwd,
2318
+ package: options.package,
2319
+ entry
2320
+ });
2321
+ const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
2322
+ if (packageInfo) {
2323
+ log(chalk8.gray(`Found package at ${packageInfo.path}`));
2324
+ }
2325
+ if (!entry) {
2326
+ log(chalk8.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
2327
+ }
2328
+ let config = null;
2329
+ try {
2330
+ config = await loadDocCovConfig(targetDir);
2331
+ if (config?.filePath) {
2332
+ log(chalk8.gray(`Loaded configuration from ${path8.relative(targetDir, config.filePath)}`));
2333
+ }
2334
+ } catch (configError) {
2335
+ error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2336
+ process.exit(1);
2337
+ }
2338
+ const cliFilters = {
2339
+ include: parseListFlag(options.include),
2340
+ exclude: parseListFlag(options.exclude)
2341
+ };
2342
+ const resolvedFilters = mergeFilterOptions(config, cliFilters);
2343
+ for (const message of resolvedFilters.messages) {
2344
+ log(chalk8.gray(`${message}`));
2345
+ }
2346
+ const resolveExternalTypes = !options.skipResolve;
2347
+ process.stdout.write(chalk8.cyan(`> Generating OpenPkg spec...
2348
+ `));
2349
+ let result;
2350
+ try {
2351
+ const doccov = createDocCov({
2352
+ resolveExternalTypes,
2353
+ maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
2354
+ useCache: options.cache !== false,
2355
+ cwd: options.cwd
2356
+ });
2357
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
2358
+ filters: {
2359
+ include: resolvedFilters.include,
2360
+ exclude: resolvedFilters.exclude
2361
+ }
2362
+ } : {};
2363
+ result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
2364
+ if (result.fromCache) {
2365
+ process.stdout.write(chalk8.gray(`> Using cached spec
2366
+ `));
2367
+ } else {
2368
+ process.stdout.write(chalk8.green(`> Generated OpenPkg spec
2369
+ `));
2370
+ }
2371
+ } catch (generationError) {
2372
+ process.stdout.write(chalk8.red(`> Failed to generate spec
2373
+ `));
2374
+ throw generationError;
2375
+ }
2376
+ if (!result) {
2377
+ throw new Error("Failed to produce an OpenPkg spec.");
2378
+ }
2379
+ const normalized = normalize(result.spec);
2380
+ const validation = validateSpec(normalized);
2381
+ if (!validation.ok) {
2382
+ error(chalk8.red("Spec failed schema validation"));
2383
+ for (const err of validation.errors) {
2384
+ error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2385
+ }
2386
+ process.exit(1);
2387
+ }
2388
+ const outputPath = path8.resolve(process.cwd(), options.output);
2389
+ writeFileSync5(outputPath, JSON.stringify(normalized, null, 2));
2390
+ log(chalk8.green(`> Wrote ${options.output}`));
2391
+ log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2392
+ log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
2393
+ if (options.showDiagnostics && result.diagnostics.length > 0) {
2394
+ log("");
2395
+ log(chalk8.bold("Diagnostics"));
2396
+ for (const diagnostic of result.diagnostics) {
2397
+ const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2398
+ log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2399
+ }
2400
+ }
2401
+ } catch (commandError) {
2402
+ error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2403
+ process.exit(1);
2404
+ }
2405
+ });
2406
+ }
2407
+
2249
2408
  // src/cli.ts
2250
2409
  var __filename2 = fileURLToPath(import.meta.url);
2251
- var __dirname2 = path8.dirname(__filename2);
2252
- var packageJson = JSON.parse(readFileSync4(path8.join(__dirname2, "../package.json"), "utf-8"));
2410
+ var __dirname2 = path9.dirname(__filename2);
2411
+ var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
2253
2412
  var program = new Command;
2254
2413
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2255
- registerGenerateCommand(program);
2256
2414
  registerCheckCommand(program);
2415
+ registerInfoCommand(program);
2416
+ registerSpecCommand(program);
2257
2417
  registerDiffCommand(program);
2258
2418
  registerInitCommand(program);
2259
2419
  registerScanCommand(program);