@doccov/cli 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +852 -666
  2. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -147,36 +147,360 @@ ${formatIssues(issues)}`);
147
147
  var defineConfig = (config) => config;
148
148
  // src/cli.ts
149
149
  import { readFileSync as readFileSync5 } from "node:fs";
150
- import * as path9 from "node:path";
150
+ import * as path10 from "node:path";
151
151
  import { fileURLToPath } from "node:url";
152
152
  import { Command } from "commander";
153
153
 
154
- // src/commands/check.ts
154
+ // src/commands/check/index.ts
155
+ import {
156
+ DocCov,
157
+ enrichSpec,
158
+ NodeFileSystem,
159
+ parseExamplesFlag,
160
+ resolveTarget
161
+ } from "@doccov/sdk";
162
+ import chalk6 from "chalk";
163
+
164
+ // src/utils/filter-options.ts
165
+ import { mergeFilters, parseListFlag } from "@doccov/sdk";
166
+ import chalk from "chalk";
167
+ var parseVisibilityFlag = (value) => {
168
+ if (!value)
169
+ return;
170
+ const validTags = ["public", "beta", "alpha", "internal"];
171
+ const parsed = parseListFlag(value);
172
+ if (!parsed)
173
+ return;
174
+ const result = [];
175
+ for (const tag of parsed) {
176
+ const lower = tag.toLowerCase();
177
+ if (validTags.includes(lower)) {
178
+ result.push(lower);
179
+ }
180
+ }
181
+ return result.length > 0 ? result : undefined;
182
+ };
183
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk.cyan(value)).join(", ")}`;
184
+ var mergeFilterOptions = (config, cliOptions) => {
185
+ const messages = [];
186
+ if (config?.include) {
187
+ messages.push(formatList("include filters from config", config.include));
188
+ }
189
+ if (config?.exclude) {
190
+ messages.push(formatList("exclude filters from config", config.exclude));
191
+ }
192
+ if (cliOptions.include) {
193
+ messages.push(formatList("apply include filters from CLI", cliOptions.include));
194
+ }
195
+ if (cliOptions.exclude) {
196
+ messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
197
+ }
198
+ if (cliOptions.visibility) {
199
+ messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
200
+ }
201
+ const resolved = mergeFilters(config, cliOptions);
202
+ if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
203
+ return { messages };
204
+ }
205
+ const source = resolved.source === "override" ? "cli" : resolved.source;
206
+ return {
207
+ include: resolved.include,
208
+ exclude: resolved.exclude,
209
+ visibility: cliOptions.visibility,
210
+ source,
211
+ messages
212
+ };
213
+ };
214
+
215
+ // src/utils/progress.ts
216
+ import chalk2 from "chalk";
217
+ class StepProgress {
218
+ steps;
219
+ currentStep = 0;
220
+ startTime;
221
+ stepStartTime;
222
+ constructor(steps) {
223
+ this.steps = steps;
224
+ this.startTime = Date.now();
225
+ this.stepStartTime = Date.now();
226
+ }
227
+ start(stepIndex) {
228
+ this.currentStep = stepIndex ?? 0;
229
+ this.stepStartTime = Date.now();
230
+ this.render();
231
+ }
232
+ next() {
233
+ this.completeCurrentStep();
234
+ this.currentStep++;
235
+ if (this.currentStep < this.steps.length) {
236
+ this.stepStartTime = Date.now();
237
+ this.render();
238
+ }
239
+ }
240
+ complete(message) {
241
+ this.completeCurrentStep();
242
+ if (message) {
243
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
244
+ console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
245
+ }
246
+ }
247
+ render() {
248
+ const step = this.steps[this.currentStep];
249
+ if (!step)
250
+ return;
251
+ const label = step.activeLabel ?? step.label;
252
+ const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
253
+ process.stdout.write(`\r${prefix} ${chalk2.cyan(label)}...`);
254
+ }
255
+ completeCurrentStep() {
256
+ const step = this.steps[this.currentStep];
257
+ if (!step)
258
+ return;
259
+ const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
260
+ const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
261
+ process.stdout.write(`\r${" ".repeat(80)}\r`);
262
+ console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
263
+ }
264
+ }
265
+
266
+ // src/utils/validation.ts
267
+ function clampPercentage(value, fallback = 80) {
268
+ if (Number.isNaN(value))
269
+ return fallback;
270
+ return Math.min(100, Math.max(0, Math.round(value)));
271
+ }
272
+ function resolveThreshold(cliValue, configValue) {
273
+ const raw = cliValue ?? configValue;
274
+ return raw !== undefined ? clampPercentage(Number(raw)) : undefined;
275
+ }
276
+
277
+ // src/commands/check/fix-handler.ts
155
278
  import * as fs2 from "node:fs";
156
- import * as path4 from "node:path";
279
+ import * as path3 from "node:path";
157
280
  import {
158
281
  applyEdits,
159
282
  categorizeDrifts,
160
283
  createSourceFile,
161
- DocCov,
162
- enrichSpec,
163
284
  findJSDocLocation,
164
285
  generateFixesForExport,
165
- generateReport,
166
286
  mergeFixes,
167
- NodeFileSystem,
168
- parseExamplesFlag,
169
287
  parseJSDocToPatch,
170
- parseMarkdownFiles,
171
- resolveTarget,
172
- serializeJSDoc,
173
- validateExamples
288
+ serializeJSDoc
174
289
  } from "@doccov/sdk";
290
+ import chalk3 from "chalk";
291
+
292
+ // src/commands/check/utils.ts
293
+ import * as fs from "node:fs";
294
+ import * as path2 from "node:path";
175
295
  import {
176
- DRIFT_CATEGORIES as DRIFT_CATEGORIES2
177
- } from "@openpkg-ts/spec";
178
- import chalk4 from "chalk";
296
+ DRIFT_CATEGORIES,
297
+ parseMarkdownFiles
298
+ } from "@doccov/sdk";
179
299
  import { glob } from "glob";
300
+ function collectDriftsFromExports(exports) {
301
+ const results = [];
302
+ for (const exp of exports) {
303
+ for (const drift of exp.docs?.drift ?? []) {
304
+ results.push({ export: exp, drift });
305
+ }
306
+ }
307
+ return results;
308
+ }
309
+ function groupByExport(drifts) {
310
+ const map = new Map;
311
+ for (const { export: exp, drift } of drifts) {
312
+ const existing = map.get(exp) ?? [];
313
+ existing.push(drift);
314
+ map.set(exp, existing);
315
+ }
316
+ return map;
317
+ }
318
+ function collectDrift(exportsList) {
319
+ const drifts = [];
320
+ for (const entry of exportsList) {
321
+ const drift = entry.docs?.drift;
322
+ if (!drift || drift.length === 0) {
323
+ continue;
324
+ }
325
+ for (const d of drift) {
326
+ drifts.push({
327
+ name: entry.name,
328
+ type: d.type,
329
+ issue: d.issue ?? "Documentation drift detected.",
330
+ suggestion: d.suggestion,
331
+ category: DRIFT_CATEGORIES[d.type]
332
+ });
333
+ }
334
+ }
335
+ return drifts;
336
+ }
337
+ function collect(value, previous) {
338
+ return previous.concat([value]);
339
+ }
340
+ async function loadMarkdownFiles(patterns, cwd) {
341
+ const files = [];
342
+ for (const pattern of patterns) {
343
+ const matches = await glob(pattern, { nodir: true, cwd });
344
+ for (const filePath of matches) {
345
+ try {
346
+ const fullPath = path2.resolve(cwd, filePath);
347
+ const content = fs.readFileSync(fullPath, "utf-8");
348
+ files.push({ path: filePath, content });
349
+ } catch {}
350
+ }
351
+ }
352
+ return parseMarkdownFiles(files);
353
+ }
354
+
355
+ // src/commands/check/fix-handler.ts
356
+ async function handleFixes(spec, options, deps) {
357
+ const { isPreview, targetDir } = options;
358
+ const { log, error } = deps;
359
+ const fixedDriftKeys = new Set;
360
+ const allDrifts = collectDriftsFromExports(spec.exports ?? []);
361
+ if (allDrifts.length === 0) {
362
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
363
+ }
364
+ const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
365
+ if (fixable.length === 0) {
366
+ log(chalk3.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
367
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
368
+ }
369
+ log("");
370
+ log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
371
+ if (nonFixable.length > 0) {
372
+ log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
373
+ }
374
+ log("");
375
+ const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
376
+ const edits = [];
377
+ const editsByFile = new Map;
378
+ for (const [exp, drifts] of groupedDrifts) {
379
+ const edit = generateEditForExport(exp, drifts, targetDir, log);
380
+ if (!edit)
381
+ continue;
382
+ for (const drift of drifts) {
383
+ fixedDriftKeys.add(`${exp.name}:${drift.issue}`);
384
+ }
385
+ edits.push(edit.edit);
386
+ const fileEdits = editsByFile.get(edit.filePath) ?? [];
387
+ fileEdits.push({
388
+ export: exp,
389
+ edit: edit.edit,
390
+ fixes: edit.fixes,
391
+ existingPatch: edit.existingPatch
392
+ });
393
+ editsByFile.set(edit.filePath, fileEdits);
394
+ }
395
+ if (edits.length === 0) {
396
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
397
+ }
398
+ if (isPreview) {
399
+ displayPreview(editsByFile, targetDir, log);
400
+ return { fixedDriftKeys, editsApplied: 0, filesModified: 0 };
401
+ }
402
+ const applyResult = await applyEdits(edits);
403
+ if (applyResult.errors.length > 0) {
404
+ for (const err of applyResult.errors) {
405
+ error(chalk3.red(` ${err.file}: ${err.error}`));
406
+ }
407
+ }
408
+ const totalFixes = Array.from(editsByFile.values()).reduce((sum, fileEdits) => sum + fileEdits.reduce((s, e) => s + e.fixes.length, 0), 0);
409
+ log("");
410
+ log(chalk3.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
411
+ for (const [filePath, fileEdits] of editsByFile) {
412
+ const relativePath = path3.relative(targetDir, filePath);
413
+ const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
414
+ log(chalk3.dim(` ${relativePath} (${fixCount} fixes)`));
415
+ }
416
+ return {
417
+ fixedDriftKeys,
418
+ editsApplied: totalFixes,
419
+ filesModified: applyResult.filesModified
420
+ };
421
+ }
422
+ function generateEditForExport(exp, drifts, targetDir, log) {
423
+ if (!exp.source?.file) {
424
+ log(chalk3.gray(` Skipping ${exp.name}: no source location`));
425
+ return null;
426
+ }
427
+ if (exp.source.file.endsWith(".d.ts")) {
428
+ log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
429
+ return null;
430
+ }
431
+ const filePath = path3.resolve(targetDir, exp.source.file);
432
+ if (!fs2.existsSync(filePath)) {
433
+ log(chalk3.gray(` Skipping ${exp.name}: file not found`));
434
+ return null;
435
+ }
436
+ const sourceFile = createSourceFile(filePath);
437
+ const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
438
+ if (!location) {
439
+ log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
440
+ return null;
441
+ }
442
+ let existingPatch = {};
443
+ if (location.hasExisting && location.existingJSDoc) {
444
+ existingPatch = parseJSDocToPatch(location.existingJSDoc);
445
+ }
446
+ const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
447
+ const fixes = generateFixesForExport(expWithDrift, existingPatch);
448
+ if (fixes.length === 0)
449
+ return null;
450
+ const mergedPatch = mergeFixes(fixes, existingPatch);
451
+ const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
452
+ const edit = {
453
+ filePath,
454
+ symbolName: exp.name,
455
+ startLine: location.startLine,
456
+ endLine: location.endLine,
457
+ hasExisting: location.hasExisting,
458
+ existingJSDoc: location.existingJSDoc,
459
+ newJSDoc,
460
+ indent: location.indent
461
+ };
462
+ return { filePath, edit, fixes, existingPatch };
463
+ }
464
+ function displayPreview(editsByFile, targetDir, log) {
465
+ log(chalk3.bold("Preview - changes that would be made:"));
466
+ log("");
467
+ for (const [filePath, fileEdits] of editsByFile) {
468
+ const relativePath = path3.relative(targetDir, filePath);
469
+ for (const { export: exp, edit, fixes } of fileEdits) {
470
+ log(chalk3.cyan(`${relativePath}:${edit.startLine + 1}`));
471
+ log(chalk3.bold(` ${exp.name}`));
472
+ log("");
473
+ if (edit.hasExisting && edit.existingJSDoc) {
474
+ const oldLines = edit.existingJSDoc.split(`
475
+ `);
476
+ const newLines = edit.newJSDoc.split(`
477
+ `);
478
+ for (const line of oldLines) {
479
+ log(chalk3.red(` - ${line}`));
480
+ }
481
+ for (const line of newLines) {
482
+ log(chalk3.green(` + ${line}`));
483
+ }
484
+ } else {
485
+ const newLines = edit.newJSDoc.split(`
486
+ `);
487
+ for (const line of newLines) {
488
+ log(chalk3.green(` + ${line}`));
489
+ }
490
+ }
491
+ log("");
492
+ log(chalk3.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
493
+ log("");
494
+ }
495
+ }
496
+ const totalFixes = Array.from(editsByFile.values()).reduce((sum, fileEdits) => sum + fileEdits.reduce((s, e) => s + e.fixes.length, 0), 0);
497
+ log(chalk3.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
498
+ log(chalk3.gray("Run with --fix to apply these changes."));
499
+ }
500
+
501
+ // src/commands/check/output.ts
502
+ import { generateReport } from "@doccov/sdk";
503
+ import chalk5 from "chalk";
180
504
 
181
505
  // src/reports/changelog-renderer.ts
182
506
  function renderChangelog(data, options = {}) {
@@ -246,7 +570,7 @@ function renderChangelog(data, options = {}) {
246
570
  `);
247
571
  }
248
572
  // src/reports/diff-markdown.ts
249
- import * as path2 from "node:path";
573
+ import * as path4 from "node:path";
250
574
  function bar(pct, width = 10) {
251
575
  const filled = Math.round(pct / 100 * width);
252
576
  return "█".repeat(filled) + "░".repeat(width - filled);
@@ -397,7 +721,7 @@ function renderDocsImpactSection(lines, docsImpact, limit) {
397
721
  lines.push("| File | Issues | Details |");
398
722
  lines.push("|------|--------|---------|");
399
723
  for (const file of impactedFiles.slice(0, limit)) {
400
- const filename = path2.basename(file.file);
724
+ const filename = path4.basename(file.file);
401
725
  const issueCount = file.references.length;
402
726
  const firstRef = file.references[0];
403
727
  const detail = firstRef ? `L${firstRef.line}: ${firstRef.memberName ?? firstRef.exportName}` : "-";
@@ -488,7 +812,7 @@ ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "ne
488
812
  return output;
489
813
  }
490
814
  // src/reports/markdown.ts
491
- import { DRIFT_CATEGORY_LABELS } from "@openpkg-ts/spec";
815
+ import { DRIFT_CATEGORY_LABELS } from "@doccov/sdk";
492
816
  function bar2(pct, width = 10) {
493
817
  const filled = Math.round(pct / 100 * width);
494
818
  return "█".repeat(filled) + "░".repeat(width - filled);
@@ -784,8 +1108,20 @@ function buildFileLink(file, opts) {
784
1108
  }
785
1109
  return `\`${file}\``;
786
1110
  }
1111
+ function getExportKeyword(kind) {
1112
+ switch (kind) {
1113
+ case "type":
1114
+ return "type";
1115
+ case "interface":
1116
+ return "interface";
1117
+ case "class":
1118
+ return "class";
1119
+ default:
1120
+ return "function";
1121
+ }
1122
+ }
787
1123
  function formatExportSignature(exp) {
788
- const prefix = `export ${exp.kind === "type" ? "type" : exp.kind === "interface" ? "interface" : exp.kind === "class" ? "class" : "function"}`;
1124
+ const prefix = `export ${getExportKeyword(exp.kind)}`;
789
1125
  if (exp.kind === "function" && exp.signatures?.[0]) {
790
1126
  const sig = exp.signatures[0];
791
1127
  const params = sig.parameters?.map((p) => `${p.name}${p.required === false ? "?" : ""}`).join(", ") ?? "";
@@ -853,7 +1189,7 @@ function renderFixGuidance(diff, opts) {
853
1189
 
854
1190
  **Quick fix:** Run \`npx doccov check --fix\` to auto-fix ${opts.fixableDriftCount} issue(s).` : "";
855
1191
  sections.push(`**For doc drift:**
856
- ` + "Update JSDoc to match current code signatures." + fixableNote);
1192
+ Update JSDoc to match current code signatures.${fixableNote}`);
857
1193
  }
858
1194
  if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
859
1195
  sections.push(`**For stale docs:**
@@ -886,10 +1222,10 @@ function renderDetailsTable(lines, diff) {
886
1222
  }
887
1223
  }
888
1224
  // src/reports/stats.ts
889
- import { isFixableDrift } from "@doccov/sdk";
890
1225
  import {
891
- DRIFT_CATEGORIES
892
- } from "@openpkg-ts/spec";
1226
+ DRIFT_CATEGORIES as DRIFT_CATEGORIES2,
1227
+ isFixableDrift
1228
+ } from "@doccov/sdk";
893
1229
  function computeStats(spec) {
894
1230
  const exports = spec.exports ?? [];
895
1231
  const signals = {
@@ -934,7 +1270,7 @@ function computeStats(spec) {
934
1270
  suggestion: d.suggestion
935
1271
  };
936
1272
  driftIssues.push(item);
937
- const category = DRIFT_CATEGORIES[d.type] ?? "semantic";
1273
+ const category = DRIFT_CATEGORIES2[d.type] ?? "semantic";
938
1274
  driftByCategory[category].push(item);
939
1275
  }
940
1276
  }
@@ -983,21 +1319,21 @@ function computeStats(spec) {
983
1319
  };
984
1320
  }
985
1321
  // src/reports/writer.ts
986
- import * as fs from "node:fs";
987
- import * as path3 from "node:path";
1322
+ import * as fs3 from "node:fs";
1323
+ import * as path5 from "node:path";
988
1324
  import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
989
- import chalk from "chalk";
1325
+ import chalk4 from "chalk";
990
1326
  function writeReport(options) {
991
1327
  const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
992
- const reportPath = outputPath ? path3.resolve(cwd, outputPath) : path3.resolve(cwd, getReportPath(format));
993
- const dir = path3.dirname(reportPath);
994
- if (!fs.existsSync(dir)) {
995
- fs.mkdirSync(dir, { recursive: true });
1328
+ const reportPath = outputPath ? path5.resolve(cwd, outputPath) : path5.resolve(cwd, getReportPath(format));
1329
+ const dir = path5.dirname(reportPath);
1330
+ if (!fs3.existsSync(dir)) {
1331
+ fs3.mkdirSync(dir, { recursive: true });
996
1332
  }
997
- fs.writeFileSync(reportPath, content);
998
- const relativePath = path3.relative(cwd, reportPath);
1333
+ fs3.writeFileSync(reportPath, content);
1334
+ const relativePath = path5.relative(cwd, reportPath);
999
1335
  if (!silent) {
1000
- console.log(chalk.green(`✓ Wrote ${format} report to ${relativePath}`));
1336
+ console.log(chalk4.green(`✓ Wrote ${format} report to ${relativePath}`));
1001
1337
  }
1002
1338
  return { path: reportPath, format, relativePath };
1003
1339
  }
@@ -1020,149 +1356,257 @@ function writeReports(options) {
1020
1356
  }));
1021
1357
  return results;
1022
1358
  }
1023
- // src/utils/filter-options.ts
1024
- import { mergeFilters, parseListFlag } from "@doccov/sdk";
1025
- import chalk2 from "chalk";
1026
- var parseVisibilityFlag = (value) => {
1027
- if (!value)
1028
- return;
1029
- const validTags = ["public", "beta", "alpha", "internal"];
1030
- const parsed = parseListFlag(value);
1031
- if (!parsed)
1032
- return;
1033
- const result = [];
1034
- for (const tag of parsed) {
1035
- const lower = tag.toLowerCase();
1036
- if (validTags.includes(lower)) {
1037
- result.push(lower);
1359
+ // src/commands/check/output.ts
1360
+ function displayTextOutput(options, deps) {
1361
+ const {
1362
+ spec,
1363
+ coverageScore,
1364
+ minCoverage,
1365
+ maxDrift,
1366
+ driftExports,
1367
+ typecheckErrors,
1368
+ staleRefs,
1369
+ exampleResult,
1370
+ specWarnings,
1371
+ specInfos
1372
+ } = options;
1373
+ const { log } = deps;
1374
+ const totalExportsForDrift = spec.exports?.length ?? 0;
1375
+ const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
1376
+ const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
1377
+ const coverageFailed = coverageScore < minCoverage;
1378
+ const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1379
+ const hasTypecheckErrors = typecheckErrors.length > 0;
1380
+ if (specWarnings.length > 0 || specInfos.length > 0) {
1381
+ log("");
1382
+ for (const diag of specWarnings) {
1383
+ log(chalk5.yellow(`⚠ ${diag.message}`));
1384
+ if (diag.suggestion) {
1385
+ log(chalk5.gray(` ${diag.suggestion}`));
1386
+ }
1387
+ }
1388
+ for (const diag of specInfos) {
1389
+ log(chalk5.cyan(`ℹ ${diag.message}`));
1390
+ if (diag.suggestion) {
1391
+ log(chalk5.gray(` ${diag.suggestion}`));
1392
+ }
1038
1393
  }
1039
1394
  }
1040
- return result.length > 0 ? result : undefined;
1041
- };
1042
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk2.cyan(value)).join(", ")}`;
1043
- var mergeFilterOptions = (config, cliOptions) => {
1044
- const messages = [];
1045
- if (config?.include) {
1046
- messages.push(formatList("include filters from config", config.include));
1395
+ const pkgName = spec.meta?.name ?? "unknown";
1396
+ const pkgVersion = spec.meta?.version ?? "";
1397
+ const totalExports = spec.exports?.length ?? 0;
1398
+ log("");
1399
+ log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1400
+ log("");
1401
+ log(` Exports: ${totalExports}`);
1402
+ if (coverageFailed) {
1403
+ log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1404
+ } else {
1405
+ log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1047
1406
  }
1048
- if (config?.exclude) {
1049
- messages.push(formatList("exclude filters from config", config.exclude));
1407
+ if (maxDrift !== undefined) {
1408
+ if (driftFailed) {
1409
+ log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1410
+ } else {
1411
+ log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1412
+ }
1413
+ } else {
1414
+ log(` Drift: ${driftScore}%`);
1415
+ }
1416
+ if (exampleResult) {
1417
+ const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
1418
+ if (typecheckCount > 0) {
1419
+ log(chalk5.yellow(` Examples: ${typecheckCount} type error(s)`));
1420
+ for (const err of typecheckErrors.slice(0, 5)) {
1421
+ const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
1422
+ log(chalk5.dim(` ${err.exportName} ${loc}`));
1423
+ log(chalk5.red(` ${err.error.message}`));
1424
+ }
1425
+ if (typecheckErrors.length > 5) {
1426
+ log(chalk5.dim(` ... and ${typecheckErrors.length - 5} more`));
1427
+ }
1428
+ } else {
1429
+ log(chalk5.green(` Examples: ✓ validated`));
1430
+ }
1050
1431
  }
1051
- if (cliOptions.include) {
1052
- messages.push(formatList("apply include filters from CLI", cliOptions.include));
1432
+ const hasStaleRefs = staleRefs.length > 0;
1433
+ if (hasStaleRefs) {
1434
+ log(chalk5.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
1435
+ for (const ref of staleRefs.slice(0, 5)) {
1436
+ log(chalk5.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
1437
+ }
1438
+ if (staleRefs.length > 5) {
1439
+ log(chalk5.dim(` ... and ${staleRefs.length - 5} more`));
1440
+ }
1053
1441
  }
1054
- if (cliOptions.exclude) {
1055
- messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
1442
+ log("");
1443
+ const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
1444
+ if (!failed) {
1445
+ const thresholdParts = [];
1446
+ thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
1447
+ if (maxDrift !== undefined) {
1448
+ thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1449
+ }
1450
+ log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1451
+ return true;
1056
1452
  }
1057
- if (cliOptions.visibility) {
1058
- messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
1453
+ if (hasTypecheckErrors) {
1454
+ log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
1059
1455
  }
1060
- const resolved = mergeFilters(config, cliOptions);
1061
- if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
1062
- return { messages };
1456
+ if (hasStaleRefs) {
1457
+ log(chalk5.red(`✗ ${staleRefs.length} stale references in docs`));
1063
1458
  }
1064
- const source = resolved.source === "override" ? "cli" : resolved.source;
1065
- return {
1066
- include: resolved.include,
1067
- exclude: resolved.exclude,
1068
- visibility: cliOptions.visibility,
1069
- source,
1070
- messages
1071
- };
1072
- };
1073
-
1074
- // src/utils/progress.ts
1075
- import chalk3 from "chalk";
1076
- class StepProgress {
1077
- steps;
1078
- currentStep = 0;
1079
- startTime;
1080
- stepStartTime;
1081
- constructor(steps) {
1082
- this.steps = steps;
1083
- this.startTime = Date.now();
1084
- this.stepStartTime = Date.now();
1459
+ log("");
1460
+ log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
1461
+ return false;
1462
+ }
1463
+ function handleNonTextOutput(options, deps) {
1464
+ const {
1465
+ format,
1466
+ spec,
1467
+ rawSpec,
1468
+ coverageScore,
1469
+ minCoverage,
1470
+ maxDrift,
1471
+ driftExports,
1472
+ typecheckErrors,
1473
+ limit,
1474
+ stdout,
1475
+ outputPath,
1476
+ cwd
1477
+ } = options;
1478
+ const { log } = deps;
1479
+ const stats = computeStats(spec);
1480
+ const report = generateReport(rawSpec);
1481
+ const jsonContent = JSON.stringify(report, null, 2);
1482
+ let formatContent;
1483
+ switch (format) {
1484
+ case "json":
1485
+ formatContent = jsonContent;
1486
+ break;
1487
+ case "markdown":
1488
+ formatContent = renderMarkdown(stats, { limit });
1489
+ break;
1490
+ case "html":
1491
+ formatContent = renderHtml(stats, { limit });
1492
+ break;
1493
+ case "github":
1494
+ formatContent = renderGithubSummary(stats, {
1495
+ coverageScore,
1496
+ driftCount: driftExports.length
1497
+ });
1498
+ break;
1499
+ default:
1500
+ throw new Error(`Unknown format: ${format}`);
1085
1501
  }
1086
- start(stepIndex) {
1087
- this.currentStep = stepIndex ?? 0;
1088
- this.stepStartTime = Date.now();
1089
- this.render();
1502
+ if (stdout) {
1503
+ log(formatContent);
1504
+ } else {
1505
+ writeReports({
1506
+ format,
1507
+ formatContent,
1508
+ jsonContent,
1509
+ outputPath,
1510
+ cwd
1511
+ });
1090
1512
  }
1091
- next() {
1092
- this.completeCurrentStep();
1093
- this.currentStep++;
1094
- if (this.currentStep < this.steps.length) {
1095
- this.stepStartTime = Date.now();
1096
- this.render();
1513
+ const totalExportsForDrift = spec.exports?.length ?? 0;
1514
+ const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
1515
+ const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
1516
+ const coverageFailed = coverageScore < minCoverage;
1517
+ const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1518
+ const hasTypecheckErrors = typecheckErrors.length > 0;
1519
+ return !(coverageFailed || driftFailed || hasTypecheckErrors);
1520
+ }
1521
+
1522
+ // src/commands/check/validation.ts
1523
+ import { validateExamples } from "@doccov/sdk";
1524
+ async function runExampleValidation(spec, options) {
1525
+ const { validations, targetDir, timeout = 5000, installTimeout = 60000 } = options;
1526
+ const typecheckErrors = [];
1527
+ const runtimeDrifts = [];
1528
+ const result = await validateExamples(spec.exports ?? [], {
1529
+ validations,
1530
+ packagePath: targetDir,
1531
+ exportNames: (spec.exports ?? []).map((e) => e.name),
1532
+ timeout,
1533
+ installTimeout
1534
+ });
1535
+ if (result.typecheck) {
1536
+ for (const err of result.typecheck.errors) {
1537
+ typecheckErrors.push({
1538
+ exportName: err.exportName,
1539
+ error: err.error
1540
+ });
1097
1541
  }
1098
1542
  }
1099
- complete(message) {
1100
- this.completeCurrentStep();
1101
- if (message) {
1102
- const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
1103
- console.log(`${chalk3.green("")} ${message} ${chalk3.dim(`(${elapsed}s)`)}`);
1543
+ if (result.run) {
1544
+ for (const drift of result.run.drifts) {
1545
+ runtimeDrifts.push({
1546
+ name: drift.exportName,
1547
+ type: "example-runtime-error",
1548
+ issue: drift.issue,
1549
+ suggestion: drift.suggestion,
1550
+ category: "example"
1551
+ });
1104
1552
  }
1105
1553
  }
1106
- render() {
1107
- const step = this.steps[this.currentStep];
1108
- if (!step)
1109
- return;
1110
- const label = step.activeLabel ?? step.label;
1111
- const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1112
- process.stdout.write(`\r${prefix} ${chalk3.cyan(label)}...`);
1113
- }
1114
- completeCurrentStep() {
1115
- const step = this.steps[this.currentStep];
1116
- if (!step)
1117
- return;
1118
- const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
1119
- const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1120
- process.stdout.write(`\r${" ".repeat(80)}\r`);
1121
- console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
1122
- }
1123
- }
1124
-
1125
- // src/utils/validation.ts
1126
- function clampPercentage(value, fallback = 80) {
1127
- if (Number.isNaN(value))
1128
- return fallback;
1129
- return Math.min(100, Math.max(0, Math.round(value)));
1554
+ return { result, typecheckErrors, runtimeDrifts };
1130
1555
  }
1131
- function resolveThreshold(cliValue, configValue) {
1132
- const raw = cliValue ?? configValue;
1133
- return raw !== undefined ? clampPercentage(Number(raw)) : undefined;
1556
+ async function validateMarkdownDocs(options) {
1557
+ const { docsPatterns, targetDir, exportNames } = options;
1558
+ const staleRefs = [];
1559
+ if (docsPatterns.length === 0) {
1560
+ return staleRefs;
1561
+ }
1562
+ const markdownFiles = await loadMarkdownFiles(docsPatterns, targetDir);
1563
+ if (markdownFiles.length === 0) {
1564
+ return staleRefs;
1565
+ }
1566
+ const exportSet = new Set(exportNames);
1567
+ for (const mdFile of markdownFiles) {
1568
+ for (const block of mdFile.codeBlocks) {
1569
+ const codeLines = block.code.split(`
1570
+ `);
1571
+ for (let i = 0;i < codeLines.length; i++) {
1572
+ const line = codeLines[i];
1573
+ const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
1574
+ if (importMatch) {
1575
+ const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
1576
+ for (const imp of imports) {
1577
+ if (imp && !exportSet.has(imp)) {
1578
+ staleRefs.push({
1579
+ file: mdFile.path,
1580
+ line: block.lineStart + i,
1581
+ exportName: imp,
1582
+ context: line.trim()
1583
+ });
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ return staleRefs;
1134
1591
  }
1135
1592
 
1136
- // src/commands/check.ts
1593
+ // src/commands/check/index.ts
1137
1594
  var defaultDependencies = {
1138
1595
  createDocCov: (options) => new DocCov(options),
1139
1596
  log: console.log,
1140
1597
  error: console.error
1141
1598
  };
1142
- function collectDriftsFromExports(exports) {
1143
- const results = [];
1144
- for (const exp of exports) {
1145
- for (const drift of exp.docs?.drift ?? []) {
1146
- results.push({ export: exp, drift });
1147
- }
1148
- }
1149
- return results;
1150
- }
1151
- function groupByExport(drifts) {
1152
- const map = new Map;
1153
- for (const { export: exp, drift } of drifts) {
1154
- const existing = map.get(exp) ?? [];
1155
- existing.push(drift);
1156
- map.set(exp, existing);
1157
- }
1158
- return map;
1159
- }
1160
1599
  function registerCheckCommand(program, dependencies = {}) {
1161
1600
  const { createDocCov, log, error } = {
1162
1601
  ...defaultDependencies,
1163
1602
  ...dependencies
1164
1603
  };
1165
- program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--preview", "Preview fixes with diff output (implies --fix)").option("--dry-run", "Alias for --preview").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").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
1604
+ program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--preview", "Preview fixes with diff output (implies --fix)").option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--update-snapshot", "Force regenerate .doccov/report.json").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)", (value) => {
1605
+ const n = parseInt(value, 10);
1606
+ if (Number.isNaN(n) || n < 1)
1607
+ throw new Error("--max-type-depth must be a positive integer");
1608
+ return n;
1609
+ }).option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
1166
1610
  try {
1167
1611
  let validations = parseExamplesFlag(options.examples);
1168
1612
  let hasExamples = validations.length > 0;
@@ -1206,19 +1650,18 @@ function registerCheckCommand(program, dependencies = {}) {
1206
1650
  };
1207
1651
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
1208
1652
  if (resolvedFilters.visibility) {
1209
- log(chalk4.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1653
+ log(chalk6.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1210
1654
  }
1211
1655
  steps.next();
1212
1656
  const resolveExternalTypes = !options.skipResolve;
1213
- let specResult;
1214
1657
  const doccov = createDocCov({
1215
1658
  resolveExternalTypes,
1216
- maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
1659
+ maxDepth: options.maxTypeDepth,
1217
1660
  useCache: options.cache !== false,
1218
1661
  cwd: options.cwd
1219
1662
  });
1220
1663
  const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
1221
- specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1664
+ const specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1222
1665
  if (!specResult) {
1223
1666
  throw new Error("Failed to analyze documentation coverage.");
1224
1667
  }
@@ -1228,384 +1671,84 @@ function registerCheckCommand(program, dependencies = {}) {
1228
1671
  steps.next();
1229
1672
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
1230
1673
  const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
1231
- const isPreview = options.preview || options.dryRun;
1232
- const shouldFix = options.fix || options.write || isPreview;
1674
+ const isPreview = options.preview;
1675
+ const shouldFix = options.fix || isPreview;
1233
1676
  let exampleResult;
1234
- const typecheckErrors = [];
1235
- const runtimeDrifts = [];
1677
+ let typecheckErrors = [];
1678
+ let runtimeDrifts = [];
1236
1679
  if (hasExamples) {
1237
- exampleResult = await validateExamples(spec.exports ?? [], {
1680
+ const validation = await runExampleValidation(spec, {
1238
1681
  validations,
1239
- packagePath: targetDir,
1240
- exportNames: (spec.exports ?? []).map((e) => e.name),
1241
- timeout: 5000,
1242
- installTimeout: 60000
1682
+ targetDir
1243
1683
  });
1244
- if (exampleResult.typecheck) {
1245
- for (const err of exampleResult.typecheck.errors) {
1246
- typecheckErrors.push({
1247
- exportName: err.exportName,
1248
- error: err.error
1249
- });
1250
- }
1251
- }
1252
- if (exampleResult.run) {
1253
- for (const drift of exampleResult.run.drifts) {
1254
- runtimeDrifts.push({
1255
- name: drift.exportName,
1256
- type: "example-runtime-error",
1257
- issue: drift.issue,
1258
- suggestion: drift.suggestion,
1259
- category: "example"
1260
- });
1261
- }
1262
- }
1684
+ exampleResult = validation.result;
1685
+ typecheckErrors = validation.typecheckErrors;
1686
+ runtimeDrifts = validation.runtimeDrifts;
1263
1687
  steps.next();
1264
1688
  }
1265
- const staleRefs = [];
1266
1689
  let docsPatterns = options.docs;
1267
1690
  if (docsPatterns.length === 0 && config?.docs?.include) {
1268
1691
  docsPatterns = config.docs.include;
1269
1692
  }
1270
- if (docsPatterns.length > 0) {
1271
- const markdownFiles = await loadMarkdownFiles(docsPatterns, targetDir);
1272
- if (markdownFiles.length > 0) {
1273
- const exportNames = (spec.exports ?? []).map((e) => e.name);
1274
- const exportSet = new Set(exportNames);
1275
- for (const mdFile of markdownFiles) {
1276
- for (const block of mdFile.codeBlocks) {
1277
- const codeLines = block.code.split(`
1278
- `);
1279
- for (let i = 0;i < codeLines.length; i++) {
1280
- const line = codeLines[i];
1281
- const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
1282
- if (importMatch) {
1283
- const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
1284
- for (const imp of imports) {
1285
- if (imp && !exportSet.has(imp)) {
1286
- staleRefs.push({
1287
- file: mdFile.path,
1288
- line: block.lineStart + i,
1289
- exportName: imp,
1290
- context: line.trim()
1291
- });
1292
- }
1293
- }
1294
- }
1295
- }
1296
- }
1297
- }
1298
- }
1299
- }
1693
+ const staleRefs = await validateMarkdownDocs({
1694
+ docsPatterns,
1695
+ targetDir,
1696
+ exportNames: (spec.exports ?? []).map((e) => e.name)
1697
+ });
1300
1698
  const coverageScore = spec.docs?.coverageScore ?? 0;
1301
1699
  const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
1302
1700
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
1303
- const fixedDriftKeys = new Set;
1304
1701
  if (shouldFix && driftExports.length > 0) {
1305
- const allDrifts = collectDriftsFromExports(spec.exports ?? []);
1306
- if (allDrifts.length > 0) {
1307
- const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1308
- if (fixable.length === 0) {
1309
- log(chalk4.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1310
- } else {
1311
- log("");
1312
- log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
1313
- if (nonFixable.length > 0) {
1314
- log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1315
- }
1316
- log("");
1317
- const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
1318
- const edits = [];
1319
- const editsByFile = new Map;
1320
- for (const [exp, drifts] of groupedDrifts) {
1321
- if (!exp.source?.file) {
1322
- log(chalk4.gray(` Skipping ${exp.name}: no source location`));
1323
- continue;
1324
- }
1325
- if (exp.source.file.endsWith(".d.ts")) {
1326
- log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
1327
- continue;
1328
- }
1329
- const filePath = path4.resolve(targetDir, exp.source.file);
1330
- if (!fs2.existsSync(filePath)) {
1331
- log(chalk4.gray(` Skipping ${exp.name}: file not found`));
1332
- continue;
1333
- }
1334
- const sourceFile = createSourceFile(filePath);
1335
- const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1336
- if (!location) {
1337
- log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
1338
- continue;
1339
- }
1340
- let existingPatch = {};
1341
- if (location.hasExisting && location.existingJSDoc) {
1342
- existingPatch = parseJSDocToPatch(location.existingJSDoc);
1343
- }
1344
- const expWithDrift = { ...exp, docs: { ...exp.docs, drift: drifts } };
1345
- const fixes = generateFixesForExport(expWithDrift, existingPatch);
1346
- if (fixes.length === 0)
1347
- continue;
1348
- for (const drift of drifts) {
1349
- fixedDriftKeys.add(`${exp.name}:${drift.issue}`);
1350
- }
1351
- const mergedPatch = mergeFixes(fixes, existingPatch);
1352
- const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
1353
- const edit = {
1354
- filePath,
1355
- symbolName: exp.name,
1356
- startLine: location.startLine,
1357
- endLine: location.endLine,
1358
- hasExisting: location.hasExisting,
1359
- existingJSDoc: location.existingJSDoc,
1360
- newJSDoc,
1361
- indent: location.indent
1362
- };
1363
- edits.push(edit);
1364
- const fileEdits = editsByFile.get(filePath) ?? [];
1365
- fileEdits.push({ export: exp, edit, fixes, existingPatch });
1366
- editsByFile.set(filePath, fileEdits);
1367
- }
1368
- if (edits.length > 0) {
1369
- if (isPreview) {
1370
- log(chalk4.bold("Preview - changes that would be made:"));
1371
- log("");
1372
- for (const [filePath, fileEdits] of editsByFile) {
1373
- const relativePath = path4.relative(targetDir, filePath);
1374
- for (const { export: exp, edit, fixes } of fileEdits) {
1375
- log(chalk4.cyan(`${relativePath}:${edit.startLine + 1}`));
1376
- log(chalk4.bold(` ${exp.name}`));
1377
- log("");
1378
- if (edit.hasExisting && edit.existingJSDoc) {
1379
- const oldLines = edit.existingJSDoc.split(`
1380
- `);
1381
- const newLines = edit.newJSDoc.split(`
1382
- `);
1383
- for (const line of oldLines) {
1384
- log(chalk4.red(` - ${line}`));
1385
- }
1386
- for (const line of newLines) {
1387
- log(chalk4.green(` + ${line}`));
1388
- }
1389
- } else {
1390
- const newLines = edit.newJSDoc.split(`
1391
- `);
1392
- for (const line of newLines) {
1393
- log(chalk4.green(` + ${line}`));
1394
- }
1395
- }
1396
- log("");
1397
- log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
1398
- log("");
1399
- }
1400
- }
1401
- const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
1402
- log(chalk4.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
1403
- log(chalk4.gray("Run with --fix to apply these changes."));
1404
- } else {
1405
- const applyResult = await applyEdits(edits);
1406
- if (applyResult.errors.length > 0) {
1407
- for (const err of applyResult.errors) {
1408
- error(chalk4.red(` ${err.file}: ${err.error}`));
1409
- }
1410
- }
1411
- const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
1412
- log("");
1413
- log(chalk4.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
1414
- for (const [filePath, fileEdits] of editsByFile) {
1415
- const relativePath = path4.relative(targetDir, filePath);
1416
- const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
1417
- log(chalk4.dim(` ${relativePath} (${fixCount} fixes)`));
1418
- }
1419
- }
1420
- }
1421
- }
1422
- }
1702
+ const fixResult = await handleFixes(spec, { isPreview, targetDir }, { log, error });
1423
1703
  if (!isPreview) {
1424
- driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1704
+ driftExports = driftExports.filter((d) => !fixResult.fixedDriftKeys.has(`${d.name}:${d.issue}`));
1425
1705
  }
1426
1706
  }
1427
1707
  steps.complete("Check complete");
1428
1708
  if (format !== "text") {
1429
- const limit = parseInt(options.limit, 10) || 20;
1430
- const stats = computeStats(spec);
1431
- const report = generateReport(specResult.spec);
1432
- const jsonContent = JSON.stringify(report, null, 2);
1433
- let formatContent;
1434
- switch (format) {
1435
- case "json":
1436
- formatContent = jsonContent;
1437
- break;
1438
- case "markdown":
1439
- formatContent = renderMarkdown(stats, { limit });
1440
- break;
1441
- case "html":
1442
- formatContent = renderHtml(stats, { limit });
1443
- break;
1444
- case "github":
1445
- formatContent = renderGithubSummary(stats, {
1446
- coverageScore,
1447
- driftCount: driftExports.length
1448
- });
1449
- break;
1450
- default:
1451
- throw new Error(`Unknown format: ${format}`);
1452
- }
1453
- if (options.stdout) {
1454
- log(formatContent);
1455
- } else {
1456
- writeReports({
1457
- format,
1458
- formatContent,
1459
- jsonContent,
1460
- outputPath: options.output,
1461
- cwd: options.cwd
1462
- });
1463
- }
1464
- const totalExportsForDrift2 = spec.exports?.length ?? 0;
1465
- const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
1466
- const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
1467
- const coverageFailed2 = coverageScore < minCoverage;
1468
- const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
1469
- const hasTypecheckErrors2 = typecheckErrors.length > 0;
1470
- if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
1709
+ const passed2 = handleNonTextOutput({
1710
+ format,
1711
+ spec,
1712
+ rawSpec: specResult.spec,
1713
+ coverageScore,
1714
+ minCoverage,
1715
+ maxDrift,
1716
+ driftExports,
1717
+ typecheckErrors,
1718
+ limit: parseInt(options.limit, 10) || 20,
1719
+ stdout: options.stdout,
1720
+ outputPath: options.output,
1721
+ cwd: options.cwd
1722
+ }, { log });
1723
+ if (!passed2) {
1471
1724
  process.exit(1);
1472
1725
  }
1473
1726
  return;
1474
1727
  }
1475
- const totalExportsForDrift = spec.exports?.length ?? 0;
1476
- const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
1477
- const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
1478
- const coverageFailed = coverageScore < minCoverage;
1479
- const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1480
- const hasTypecheckErrors = typecheckErrors.length > 0;
1481
- if (specWarnings.length > 0 || specInfos.length > 0) {
1482
- log("");
1483
- for (const diag of specWarnings) {
1484
- log(chalk4.yellow(`⚠ ${diag.message}`));
1485
- if (diag.suggestion) {
1486
- log(chalk4.gray(` ${diag.suggestion}`));
1487
- }
1488
- }
1489
- for (const diag of specInfos) {
1490
- log(chalk4.cyan(`ℹ ${diag.message}`));
1491
- if (diag.suggestion) {
1492
- log(chalk4.gray(` ${diag.suggestion}`));
1493
- }
1494
- }
1495
- }
1496
- const pkgName = spec.meta?.name ?? "unknown";
1497
- const pkgVersion = spec.meta?.version ?? "";
1498
- const totalExports = spec.exports?.length ?? 0;
1499
- log("");
1500
- log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1501
- log("");
1502
- log(` Exports: ${totalExports}`);
1503
- if (coverageFailed) {
1504
- log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1505
- } else {
1506
- log(chalk4.green(` Coverage: ✓ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1507
- }
1508
- if (maxDrift !== undefined) {
1509
- if (driftFailed) {
1510
- log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1511
- } else {
1512
- log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1513
- }
1514
- } else {
1515
- log(` Drift: ${driftScore}%`);
1516
- }
1517
- if (exampleResult) {
1518
- const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
1519
- if (typecheckCount > 0) {
1520
- log(chalk4.yellow(` Examples: ${typecheckCount} type error(s)`));
1521
- for (const err of typecheckErrors.slice(0, 5)) {
1522
- const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
1523
- log(chalk4.dim(` ${err.exportName} ${loc}`));
1524
- log(chalk4.red(` ${err.error.message}`));
1525
- }
1526
- if (typecheckErrors.length > 5) {
1527
- log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
1528
- }
1529
- } else {
1530
- log(chalk4.green(` Examples: ✓ validated`));
1531
- }
1532
- }
1533
- const hasStaleRefs = staleRefs.length > 0;
1534
- if (hasStaleRefs) {
1535
- log(chalk4.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
1536
- for (const ref of staleRefs.slice(0, 5)) {
1537
- log(chalk4.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
1538
- }
1539
- if (staleRefs.length > 5) {
1540
- log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
1541
- }
1542
- }
1543
- log("");
1544
- const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
1545
- if (!failed) {
1546
- const thresholdParts = [];
1547
- thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
1548
- if (maxDrift !== undefined) {
1549
- thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1550
- }
1551
- log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1552
- return;
1553
- }
1554
- if (hasTypecheckErrors) {
1555
- log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
1556
- }
1557
- if (hasStaleRefs) {
1558
- log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
1728
+ const passed = displayTextOutput({
1729
+ spec,
1730
+ coverageScore,
1731
+ minCoverage,
1732
+ maxDrift,
1733
+ driftExports,
1734
+ typecheckErrors,
1735
+ staleRefs,
1736
+ exampleResult,
1737
+ specWarnings,
1738
+ specInfos
1739
+ }, { log });
1740
+ if (!passed) {
1741
+ process.exit(1);
1559
1742
  }
1560
- log("");
1561
- log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
1562
- process.exit(1);
1563
1743
  } catch (commandError) {
1564
- error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1744
+ error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1565
1745
  process.exit(1);
1566
1746
  }
1567
1747
  });
1568
1748
  }
1569
- function collectDrift(exportsList) {
1570
- const drifts = [];
1571
- for (const entry of exportsList) {
1572
- const drift = entry.docs?.drift;
1573
- if (!drift || drift.length === 0) {
1574
- continue;
1575
- }
1576
- for (const d of drift) {
1577
- drifts.push({
1578
- name: entry.name,
1579
- type: d.type,
1580
- issue: d.issue ?? "Documentation drift detected.",
1581
- suggestion: d.suggestion,
1582
- category: DRIFT_CATEGORIES2[d.type]
1583
- });
1584
- }
1585
- }
1586
- return drifts;
1587
- }
1588
- function collect(value, previous) {
1589
- return previous.concat([value]);
1590
- }
1591
- async function loadMarkdownFiles(patterns, cwd) {
1592
- const files = [];
1593
- for (const pattern of patterns) {
1594
- const matches = await glob(pattern, { nodir: true, cwd });
1595
- for (const filePath of matches) {
1596
- try {
1597
- const fullPath = path4.resolve(cwd, filePath);
1598
- const content = fs2.readFileSync(fullPath, "utf-8");
1599
- files.push({ path: filePath, content });
1600
- } catch {}
1601
- }
1602
- }
1603
- return parseMarkdownFiles(files);
1604
- }
1605
-
1606
1749
  // src/commands/diff.ts
1607
- import * as fs3 from "node:fs";
1608
- import * as path5 from "node:path";
1750
+ import * as fs4 from "node:fs";
1751
+ import * as path6 from "node:path";
1609
1752
  import {
1610
1753
  diffSpecWithDocs,
1611
1754
  ensureSpecCoverage,
@@ -1616,10 +1759,10 @@ import {
1616
1759
  parseMarkdownFiles as parseMarkdownFiles2
1617
1760
  } from "@doccov/sdk";
1618
1761
  import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
1619
- import chalk5 from "chalk";
1762
+ import chalk7 from "chalk";
1620
1763
  import { glob as glob2 } from "glob";
1621
1764
  var defaultDependencies2 = {
1622
- readFileSync: fs3.readFileSync,
1765
+ readFileSync: fs4.readFileSync,
1623
1766
  log: console.log,
1624
1767
  error: console.error
1625
1768
  };
@@ -1657,12 +1800,12 @@ function registerDiffCommand(program, dependencies = {}) {
1657
1800
  const baseHash = hashString(JSON.stringify(baseSpec));
1658
1801
  const headHash = hashString(JSON.stringify(headSpec));
1659
1802
  const cacheEnabled = options.cache !== false;
1660
- const cachedReportPath = path5.resolve(options.cwd, getDiffReportPath(baseHash, headHash, "json"));
1803
+ const cachedReportPath = path6.resolve(options.cwd, getDiffReportPath(baseHash, headHash, "json"));
1661
1804
  let diff;
1662
1805
  let fromCache = false;
1663
- if (cacheEnabled && fs3.existsSync(cachedReportPath)) {
1806
+ if (cacheEnabled && fs4.existsSync(cachedReportPath)) {
1664
1807
  try {
1665
- const cached = JSON.parse(fs3.readFileSync(cachedReportPath, "utf-8"));
1808
+ const cached = JSON.parse(fs4.readFileSync(cachedReportPath, "utf-8"));
1666
1809
  diff = cached;
1667
1810
  fromCache = true;
1668
1811
  } catch {
@@ -1689,9 +1832,9 @@ function registerDiffCommand(program, dependencies = {}) {
1689
1832
  }, null, 2));
1690
1833
  } else {
1691
1834
  log("");
1692
- log(chalk5.bold("Semver Recommendation"));
1835
+ log(chalk7.bold("Semver Recommendation"));
1693
1836
  log(` Current version: ${currentVersion}`);
1694
- log(` Recommended: ${chalk5.cyan(nextVersion)} (${chalk5.yellow(recommendation.bump.toUpperCase())})`);
1837
+ log(` Recommended: ${chalk7.cyan(nextVersion)} (${chalk7.yellow(recommendation.bump.toUpperCase())})`);
1695
1838
  log(` Reason: ${recommendation.reason}`);
1696
1839
  }
1697
1840
  return;
@@ -1699,8 +1842,8 @@ function registerDiffCommand(program, dependencies = {}) {
1699
1842
  const format = options.format ?? "text";
1700
1843
  const limit = parseInt(options.limit, 10) || 10;
1701
1844
  const checks = getStrictChecks(options.strict);
1702
- const baseName = path5.basename(baseFile);
1703
- const headName = path5.basename(headFile);
1845
+ const baseName = path6.basename(baseFile);
1846
+ const headName = path6.basename(headFile);
1704
1847
  const reportData = {
1705
1848
  baseName,
1706
1849
  headName,
@@ -1720,8 +1863,8 @@ function registerDiffCommand(program, dependencies = {}) {
1720
1863
  silent: true
1721
1864
  });
1722
1865
  }
1723
- const cacheNote = fromCache ? chalk5.cyan(" (cached)") : "";
1724
- log(chalk5.dim(`Report: ${jsonPath}`) + cacheNote);
1866
+ const cacheNote = fromCache ? chalk7.cyan(" (cached)") : "";
1867
+ log(chalk7.dim(`Report: ${jsonPath}`) + cacheNote);
1725
1868
  }
1726
1869
  break;
1727
1870
  case "json": {
@@ -1779,7 +1922,10 @@ function registerDiffCommand(program, dependencies = {}) {
1779
1922
  sha: options.sha,
1780
1923
  minCoverage,
1781
1924
  limit,
1782
- semverBump: { bump: semverRecommendation.bump, reason: semverRecommendation.reason }
1925
+ semverBump: {
1926
+ bump: semverRecommendation.bump,
1927
+ reason: semverRecommendation.reason
1928
+ }
1783
1929
  });
1784
1930
  log(content);
1785
1931
  break;
@@ -1813,18 +1959,18 @@ function registerDiffCommand(program, dependencies = {}) {
1813
1959
  checks
1814
1960
  });
1815
1961
  if (failures.length > 0) {
1816
- log(chalk5.red(`
1962
+ log(chalk7.red(`
1817
1963
  ✗ Check failed`));
1818
1964
  for (const f of failures) {
1819
- log(chalk5.red(` - ${f}`));
1965
+ log(chalk7.red(` - ${f}`));
1820
1966
  }
1821
1967
  process.exitCode = 1;
1822
1968
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1823
- log(chalk5.green(`
1969
+ log(chalk7.green(`
1824
1970
  ✓ All checks passed`));
1825
1971
  }
1826
1972
  } catch (commandError) {
1827
- error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1973
+ error(chalk7.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1828
1974
  process.exitCode = 1;
1829
1975
  }
1830
1976
  });
@@ -1838,7 +1984,7 @@ async function loadMarkdownFiles2(patterns) {
1838
1984
  const matches = await glob2(pattern, { nodir: true });
1839
1985
  for (const filePath of matches) {
1840
1986
  try {
1841
- const content = fs3.readFileSync(filePath, "utf-8");
1987
+ const content = fs4.readFileSync(filePath, "utf-8");
1842
1988
  files.push({ path: filePath, content });
1843
1989
  } catch {}
1844
1990
  }
@@ -1851,7 +1997,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1851
1997
  if (!docsPatterns || docsPatterns.length === 0) {
1852
1998
  if (config?.docs?.include) {
1853
1999
  docsPatterns = config.docs.include;
1854
- log(chalk5.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
2000
+ log(chalk7.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1855
2001
  }
1856
2002
  }
1857
2003
  if (docsPatterns && docsPatterns.length > 0) {
@@ -1860,8 +2006,8 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1860
2006
  return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
1861
2007
  }
1862
2008
  function loadSpec(filePath, readFileSync3) {
1863
- const resolvedPath = path5.resolve(filePath);
1864
- if (!fs3.existsSync(resolvedPath)) {
2009
+ const resolvedPath = path6.resolve(filePath);
2010
+ if (!fs4.existsSync(resolvedPath)) {
1865
2011
  throw new Error(`File not found: ${filePath}`);
1866
2012
  }
1867
2013
  try {
@@ -1874,37 +2020,37 @@ function loadSpec(filePath, readFileSync3) {
1874
2020
  }
1875
2021
  function printSummary(diff, baseName, headName, fromCache, log) {
1876
2022
  log("");
1877
- const cacheIndicator = fromCache ? chalk5.cyan(" (cached)") : "";
1878
- log(chalk5.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
2023
+ const cacheIndicator = fromCache ? chalk7.cyan(" (cached)") : "";
2024
+ log(chalk7.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1879
2025
  log("─".repeat(40));
1880
2026
  log("");
1881
- const coverageColor = diff.coverageDelta > 0 ? chalk5.green : diff.coverageDelta < 0 ? chalk5.red : chalk5.gray;
2027
+ const coverageColor = diff.coverageDelta > 0 ? chalk7.green : diff.coverageDelta < 0 ? chalk7.red : chalk7.gray;
1882
2028
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1883
2029
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1884
2030
  const breakingCount = diff.breaking.length;
1885
2031
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1886
2032
  if (breakingCount > 0) {
1887
- const severityNote = highSeverity > 0 ? chalk5.red(` (${highSeverity} high severity)`) : "";
1888
- log(` Breaking: ${chalk5.red(breakingCount)} changes${severityNote}`);
2033
+ const severityNote = highSeverity > 0 ? chalk7.red(` (${highSeverity} high severity)`) : "";
2034
+ log(` Breaking: ${chalk7.red(breakingCount)} changes${severityNote}`);
1889
2035
  } else {
1890
- log(` Breaking: ${chalk5.green("0")} changes`);
2036
+ log(` Breaking: ${chalk7.green("0")} changes`);
1891
2037
  }
1892
2038
  const newCount = diff.nonBreaking.length;
1893
2039
  const undocCount = diff.newUndocumented.length;
1894
2040
  if (newCount > 0) {
1895
- const undocNote = undocCount > 0 ? chalk5.yellow(` (${undocCount} undocumented)`) : "";
1896
- log(` New: ${chalk5.green(newCount)} exports${undocNote}`);
2041
+ const undocNote = undocCount > 0 ? chalk7.yellow(` (${undocCount} undocumented)`) : "";
2042
+ log(` New: ${chalk7.green(newCount)} exports${undocNote}`);
1897
2043
  }
1898
2044
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1899
2045
  const parts = [];
1900
2046
  if (diff.driftIntroduced > 0)
1901
- parts.push(chalk5.red(`+${diff.driftIntroduced}`));
2047
+ parts.push(chalk7.red(`+${diff.driftIntroduced}`));
1902
2048
  if (diff.driftResolved > 0)
1903
- parts.push(chalk5.green(`-${diff.driftResolved}`));
2049
+ parts.push(chalk7.green(`-${diff.driftResolved}`));
1904
2050
  log(` Drift: ${parts.join(", ")}`);
1905
2051
  }
1906
2052
  const recommendation = recommendSemverBump(diff);
1907
- const bumpColor = recommendation.bump === "major" ? chalk5.red : recommendation.bump === "minor" ? chalk5.yellow : chalk5.green;
2053
+ const bumpColor = recommendation.bump === "major" ? chalk7.red : recommendation.bump === "minor" ? chalk7.yellow : chalk7.green;
1908
2054
  log(` Semver: ${bumpColor(recommendation.bump.toUpperCase())} (${recommendation.reason})`);
1909
2055
  log("");
1910
2056
  }
@@ -1986,7 +2132,7 @@ function printGitHubAnnotations(diff, log) {
1986
2132
 
1987
2133
  // src/commands/info.ts
1988
2134
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1989
- import chalk6 from "chalk";
2135
+ import chalk8 from "chalk";
1990
2136
  function registerInfoCommand(program) {
1991
2137
  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) => {
1992
2138
  try {
@@ -2008,28 +2154,28 @@ function registerInfoCommand(program) {
2008
2154
  const spec = enrichSpec2(specResult.spec);
2009
2155
  const stats = computeStats(spec);
2010
2156
  console.log("");
2011
- console.log(chalk6.bold(`${stats.packageName}@${stats.version}`));
2157
+ console.log(chalk8.bold(`${stats.packageName}@${stats.version}`));
2012
2158
  console.log("");
2013
- console.log(` Exports: ${chalk6.bold(stats.totalExports.toString())}`);
2014
- console.log(` Coverage: ${chalk6.bold(`${stats.coverageScore}%`)}`);
2015
- console.log(` Drift: ${chalk6.bold(`${stats.driftScore}%`)}`);
2159
+ console.log(` Exports: ${chalk8.bold(stats.totalExports.toString())}`);
2160
+ console.log(` Coverage: ${chalk8.bold(`${stats.coverageScore}%`)}`);
2161
+ console.log(` Drift: ${chalk8.bold(`${stats.driftScore}%`)}`);
2016
2162
  console.log("");
2017
2163
  } catch (err) {
2018
- console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
2164
+ console.error(chalk8.red("Error:"), err instanceof Error ? err.message : err);
2019
2165
  process.exit(1);
2020
2166
  }
2021
2167
  });
2022
2168
  }
2023
2169
 
2024
2170
  // src/commands/init.ts
2025
- import * as fs4 from "node:fs";
2026
- import * as path6 from "node:path";
2027
- import chalk7 from "chalk";
2171
+ import * as fs5 from "node:fs";
2172
+ import * as path7 from "node:path";
2173
+ import chalk9 from "chalk";
2028
2174
  var defaultDependencies3 = {
2029
- fileExists: fs4.existsSync,
2030
- writeFileSync: fs4.writeFileSync,
2031
- readFileSync: fs4.readFileSync,
2032
- mkdirSync: fs4.mkdirSync,
2175
+ fileExists: fs5.existsSync,
2176
+ writeFileSync: fs5.writeFileSync,
2177
+ readFileSync: fs5.readFileSync,
2178
+ mkdirSync: fs5.mkdirSync,
2033
2179
  log: console.log,
2034
2180
  error: console.error
2035
2181
  };
@@ -2039,56 +2185,56 @@ function registerInitCommand(program, dependencies = {}) {
2039
2185
  ...dependencies
2040
2186
  };
2041
2187
  program.command("init").description("Initialize DocCov: config, GitHub Action, and badge").option("--cwd <dir>", "Working directory", process.cwd()).option("--skip-action", "Skip GitHub Action workflow creation").action((options) => {
2042
- const cwd = path6.resolve(options.cwd);
2188
+ const cwd = path7.resolve(options.cwd);
2043
2189
  const existing = findExistingConfig(cwd, fileExists2);
2044
2190
  if (existing) {
2045
- error(chalk7.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
2191
+ error(chalk9.red(`A DocCov config already exists at ${path7.relative(cwd, existing) || "./doccov.config.*"}.`));
2046
2192
  process.exitCode = 1;
2047
2193
  return;
2048
2194
  }
2049
2195
  const packageType = detectPackageType(cwd, fileExists2, readFileSync4);
2050
2196
  const targetFormat = packageType === "module" ? "ts" : "mts";
2051
2197
  const fileName = `doccov.config.${targetFormat}`;
2052
- const outputPath = path6.join(cwd, fileName);
2198
+ const outputPath = path7.join(cwd, fileName);
2053
2199
  if (fileExists2(outputPath)) {
2054
- error(chalk7.red(`Cannot create ${fileName}; file already exists.`));
2200
+ error(chalk9.red(`Cannot create ${fileName}; file already exists.`));
2055
2201
  process.exitCode = 1;
2056
2202
  return;
2057
2203
  }
2058
2204
  const template = buildConfigTemplate();
2059
2205
  writeFileSync3(outputPath, template, { encoding: "utf8" });
2060
- log(chalk7.green(`✓ Created ${fileName}`));
2206
+ log(chalk9.green(`✓ Created ${fileName}`));
2061
2207
  if (!options.skipAction) {
2062
- const workflowDir = path6.join(cwd, ".github", "workflows");
2063
- const workflowPath = path6.join(workflowDir, "doccov.yml");
2208
+ const workflowDir = path7.join(cwd, ".github", "workflows");
2209
+ const workflowPath = path7.join(workflowDir, "doccov.yml");
2064
2210
  if (!fileExists2(workflowPath)) {
2065
2211
  mkdirSync3(workflowDir, { recursive: true });
2066
2212
  writeFileSync3(workflowPath, buildWorkflowTemplate(), { encoding: "utf8" });
2067
- log(chalk7.green(`✓ Created .github/workflows/doccov.yml`));
2213
+ log(chalk9.green(`✓ Created .github/workflows/doccov.yml`));
2068
2214
  } else {
2069
- log(chalk7.yellow(` Skipped .github/workflows/doccov.yml (already exists)`));
2215
+ log(chalk9.yellow(` Skipped .github/workflows/doccov.yml (already exists)`));
2070
2216
  }
2071
2217
  }
2072
2218
  const repoInfo = detectRepoInfo(cwd, fileExists2, readFileSync4);
2073
2219
  log("");
2074
- log(chalk7.bold("Add this badge to your README:"));
2220
+ log(chalk9.bold("Add this badge to your README:"));
2075
2221
  log("");
2076
2222
  if (repoInfo) {
2077
- log(chalk7.cyan(`[![DocCov](https://doccov.dev/badge/${repoInfo.owner}/${repoInfo.repo})](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
2223
+ log(chalk9.cyan(`[![DocCov](https://doccov.dev/badge/${repoInfo.owner}/${repoInfo.repo})](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
2078
2224
  } else {
2079
- log(chalk7.cyan(`[![DocCov](https://doccov.dev/badge/OWNER/REPO)](https://doccov.dev/OWNER/REPO)`));
2080
- log(chalk7.dim(" Replace OWNER/REPO with your GitHub repo"));
2225
+ log(chalk9.cyan(`[![DocCov](https://doccov.dev/badge/OWNER/REPO)](https://doccov.dev/OWNER/REPO)`));
2226
+ log(chalk9.dim(" Replace OWNER/REPO with your GitHub repo"));
2081
2227
  }
2082
2228
  log("");
2083
- log(chalk7.dim("Run `doccov check` to verify your documentation coverage"));
2229
+ log(chalk9.dim("Run `doccov check` to verify your documentation coverage"));
2084
2230
  });
2085
2231
  }
2086
2232
  var findExistingConfig = (cwd, fileExists2) => {
2087
- let current = path6.resolve(cwd);
2088
- const { root } = path6.parse(current);
2233
+ let current = path7.resolve(cwd);
2234
+ const { root } = path7.parse(current);
2089
2235
  while (true) {
2090
2236
  for (const candidate of DOCCOV_CONFIG_FILENAMES) {
2091
- const candidatePath = path6.join(current, candidate);
2237
+ const candidatePath = path7.join(current, candidate);
2092
2238
  if (fileExists2(candidatePath)) {
2093
2239
  return candidatePath;
2094
2240
  }
@@ -2096,7 +2242,7 @@ var findExistingConfig = (cwd, fileExists2) => {
2096
2242
  if (current === root) {
2097
2243
  break;
2098
2244
  }
2099
- current = path6.dirname(current);
2245
+ current = path7.dirname(current);
2100
2246
  }
2101
2247
  return null;
2102
2248
  };
@@ -2118,17 +2264,17 @@ var detectPackageType = (cwd, fileExists2, readFileSync4) => {
2118
2264
  return;
2119
2265
  };
2120
2266
  var findNearestPackageJson = (cwd, fileExists2) => {
2121
- let current = path6.resolve(cwd);
2122
- const { root } = path6.parse(current);
2267
+ let current = path7.resolve(cwd);
2268
+ const { root } = path7.parse(current);
2123
2269
  while (true) {
2124
- const candidate = path6.join(current, "package.json");
2270
+ const candidate = path7.join(current, "package.json");
2125
2271
  if (fileExists2(candidate)) {
2126
2272
  return candidate;
2127
2273
  }
2128
2274
  if (current === root) {
2129
2275
  break;
2130
2276
  }
2131
- current = path6.dirname(current);
2277
+ current = path7.dirname(current);
2132
2278
  }
2133
2279
  return null;
2134
2280
  };
@@ -2190,7 +2336,7 @@ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
2190
2336
  }
2191
2337
  } catch {}
2192
2338
  }
2193
- const gitConfigPath = path6.join(cwd, ".git", "config");
2339
+ const gitConfigPath = path7.join(cwd, ".git", "config");
2194
2340
  if (fileExists2(gitConfigPath)) {
2195
2341
  try {
2196
2342
  const config = readFileSync4(gitConfigPath, "utf8");
@@ -2204,18 +2350,26 @@ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
2204
2350
  };
2205
2351
 
2206
2352
  // src/commands/spec.ts
2207
- import * as fs5 from "node:fs";
2208
- import * as path7 from "node:path";
2209
- import { detectPackageManager, DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2353
+ import * as fs6 from "node:fs";
2354
+ import * as path8 from "node:path";
2355
+ import {
2356
+ buildDocCovSpec,
2357
+ DocCov as DocCov3,
2358
+ detectPackageManager,
2359
+ NodeFileSystem as NodeFileSystem3,
2360
+ renderApiSurface,
2361
+ resolveTarget as resolveTarget3
2362
+ } from "@doccov/sdk";
2363
+ import { validateDocCovSpec } from "@doccov/spec";
2210
2364
  import { normalize, validateSpec } from "@openpkg-ts/spec";
2211
- import chalk8 from "chalk";
2365
+ import chalk10 from "chalk";
2212
2366
  // package.json
2213
- var version = "0.21.0";
2367
+ var version = "0.23.0";
2214
2368
 
2215
2369
  // src/commands/spec.ts
2216
2370
  var defaultDependencies4 = {
2217
2371
  createDocCov: (options) => new DocCov3(options),
2218
- writeFileSync: fs5.writeFileSync,
2372
+ writeFileSync: fs6.writeFileSync,
2219
2373
  log: console.log,
2220
2374
  error: console.error
2221
2375
  };
@@ -2224,8 +2378,8 @@ function getArrayLength(value) {
2224
2378
  }
2225
2379
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2226
2380
  const location = diagnostic.location;
2227
- const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
2228
- const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2381
+ const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
2382
+ const locationText = location && relativePath ? chalk10.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2229
2383
  const locationPrefix = locationText ? `${locationText} ` : "";
2230
2384
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
2231
2385
  }
@@ -2234,15 +2388,19 @@ function registerSpecCommand(program, dependencies = {}) {
2234
2388
  ...defaultDependencies4,
2235
2389
  ...dependencies
2236
2390
  };
2237
- 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("-f, --format <format>", "Output format: json (default) or api-surface", "json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--runtime", "Enable Standard Schema runtime extraction (richer output for Zod, Valibot, etc.)").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
2391
+ program.command("spec [entry]").description("Generate OpenPkg + DocCov specifications").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <dir>", "Output directory", ".doccov").option("-f, --format <format>", "Output format: json (default) or api-surface", "json").option("--openpkg-only", "Only generate openpkg.json (skip coverage analysis)").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--runtime", "Enable Standard Schema runtime extraction (richer output for Zod, Valibot, etc.)").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
2238
2392
  try {
2239
- const steps = new StepProgress([
2393
+ const stepsList = [
2240
2394
  { label: "Resolved target", activeLabel: "Resolving target" },
2241
2395
  { label: "Loaded config", activeLabel: "Loading config" },
2242
2396
  { label: "Generated spec", activeLabel: "Generating spec" },
2243
- { label: "Validated schema", activeLabel: "Validating schema" },
2244
- { label: "Wrote output", activeLabel: "Writing output" }
2245
- ]);
2397
+ { label: "Validated schema", activeLabel: "Validating schema" }
2398
+ ];
2399
+ if (!options.openpkgOnly) {
2400
+ stepsList.push({ label: "Built coverage analysis", activeLabel: "Building coverage" });
2401
+ }
2402
+ stepsList.push({ label: "Wrote output", activeLabel: "Writing output" });
2403
+ const steps = new StepProgress(stepsList);
2246
2404
  steps.start();
2247
2405
  const fileSystem = new NodeFileSystem3(options.cwd);
2248
2406
  const resolved = await resolveTarget3(fileSystem, {
@@ -2256,7 +2414,7 @@ function registerSpecCommand(program, dependencies = {}) {
2256
2414
  try {
2257
2415
  config = await loadDocCovConfig(targetDir);
2258
2416
  } catch (configError) {
2259
- error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2417
+ error(chalk10.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2260
2418
  process.exit(1);
2261
2419
  }
2262
2420
  steps.next();
@@ -2275,7 +2433,7 @@ function registerSpecCommand(program, dependencies = {}) {
2275
2433
  schemaExtraction: options.runtime ? "hybrid" : "static"
2276
2434
  });
2277
2435
  const generationInput = {
2278
- entryPoint: path7.relative(targetDir, entryFile),
2436
+ entryPoint: path8.relative(targetDir, entryFile),
2279
2437
  entryPointSource: entryPointInfo.source,
2280
2438
  isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
2281
2439
  generatorName: "@doccov/cli",
@@ -2300,95 +2458,123 @@ function registerSpecCommand(program, dependencies = {}) {
2300
2458
  const normalized = normalize(result.spec);
2301
2459
  const validation = validateSpec(normalized);
2302
2460
  if (!validation.ok) {
2303
- error(chalk8.red("Spec failed schema validation"));
2461
+ error(chalk10.red("Spec failed schema validation"));
2304
2462
  for (const err of validation.errors) {
2305
- error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2463
+ error(chalk10.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2306
2464
  }
2307
2465
  process.exit(1);
2308
2466
  }
2309
2467
  steps.next();
2468
+ let doccovSpec = null;
2469
+ if (!options.openpkgOnly) {
2470
+ doccovSpec = buildDocCovSpec({
2471
+ openpkgPath: "openpkg.json",
2472
+ openpkg: normalized,
2473
+ packagePath: targetDir
2474
+ });
2475
+ const doccovValidation = validateDocCovSpec(doccovSpec);
2476
+ if (!doccovValidation.ok) {
2477
+ error(chalk10.red("DocCov spec failed schema validation"));
2478
+ for (const err of doccovValidation.errors) {
2479
+ error(chalk10.red(`doccov: ${err.instancePath || "/"} ${err.message}`));
2480
+ }
2481
+ process.exit(1);
2482
+ }
2483
+ steps.next();
2484
+ }
2310
2485
  const format = options.format ?? "json";
2311
- const outputPath = path7.resolve(options.cwd, options.output);
2486
+ const outputDir = path8.resolve(options.cwd, options.output);
2487
+ fs6.mkdirSync(outputDir, { recursive: true });
2312
2488
  if (format === "api-surface") {
2313
2489
  const apiSurface = renderApiSurface(normalized);
2314
- writeFileSync4(outputPath, apiSurface);
2315
- steps.complete(`Generated ${options.output} (API surface)`);
2490
+ const apiSurfacePath = path8.join(outputDir, "api-surface.txt");
2491
+ writeFileSync4(apiSurfacePath, apiSurface);
2492
+ steps.complete(`Generated ${options.output}/ (API surface)`);
2316
2493
  } else {
2317
- writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2318
- steps.complete(`Generated ${options.output}`);
2494
+ const openpkgPath = path8.join(outputDir, "openpkg.json");
2495
+ writeFileSync4(openpkgPath, JSON.stringify(normalized, null, 2));
2496
+ if (doccovSpec) {
2497
+ const doccovPath = path8.join(outputDir, "doccov.json");
2498
+ writeFileSync4(doccovPath, JSON.stringify(doccovSpec, null, 2));
2499
+ steps.complete(`Generated ${options.output}/`);
2500
+ log(chalk10.gray(` openpkg.json: ${getArrayLength(normalized.exports)} exports`));
2501
+ log(chalk10.gray(` doccov.json: ${doccovSpec.summary.score}% coverage, ${doccovSpec.summary.drift.total} drift issues`));
2502
+ } else {
2503
+ steps.complete(`Generated ${options.output}/openpkg.json`);
2504
+ log(chalk10.gray(` ${getArrayLength(normalized.exports)} exports`));
2505
+ log(chalk10.gray(` ${getArrayLength(normalized.types)} types`));
2506
+ }
2319
2507
  }
2320
- log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2321
- log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
2322
2508
  const schemaExtraction = normalized.generation?.analysis?.schemaExtraction;
2323
2509
  if (options.runtime && (!schemaExtraction?.runtimeCount || schemaExtraction.runtimeCount === 0)) {
2324
2510
  const pm = await detectPackageManager(fileSystem);
2325
2511
  const buildCmd = pm.name === "npm" ? "npm run build" : `${pm.name} run build`;
2326
2512
  log("");
2327
- log(chalk8.yellow("⚠ Runtime extraction requested but no schemas extracted."));
2328
- log(chalk8.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
2513
+ log(chalk10.yellow("⚠ Runtime extraction requested but no schemas extracted."));
2514
+ log(chalk10.yellow(` Ensure project is built (${buildCmd}) and dist/ exists.`));
2329
2515
  }
2330
2516
  if (options.verbose && normalized.generation) {
2331
2517
  const gen = normalized.generation;
2332
2518
  log("");
2333
- log(chalk8.bold("Generation Info"));
2334
- log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
2335
- log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2336
- log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
2337
- log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2338
- log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2339
- log(chalk8.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
2519
+ log(chalk10.bold("Generation Info"));
2520
+ log(chalk10.gray(` Timestamp: ${gen.timestamp}`));
2521
+ log(chalk10.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2522
+ log(chalk10.gray(` Entry point: ${gen.analysis.entryPoint}`));
2523
+ log(chalk10.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2524
+ log(chalk10.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2525
+ log(chalk10.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
2340
2526
  if (gen.analysis.maxTypeDepth) {
2341
- log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2527
+ log(chalk10.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2342
2528
  }
2343
2529
  if (gen.analysis.schemaExtraction) {
2344
2530
  const se = gen.analysis.schemaExtraction;
2345
- log(chalk8.gray(` Schema extraction: ${se.method}`));
2531
+ log(chalk10.gray(` Schema extraction: ${se.method}`));
2346
2532
  if (se.runtimeCount) {
2347
- log(chalk8.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
2533
+ log(chalk10.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
2348
2534
  }
2349
2535
  }
2350
2536
  log("");
2351
- log(chalk8.bold("Environment"));
2352
- log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2537
+ log(chalk10.bold("Environment"));
2538
+ log(chalk10.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2353
2539
  if (gen.environment.packageManager) {
2354
- log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
2540
+ log(chalk10.gray(` Package manager: ${gen.environment.packageManager}`));
2355
2541
  }
2356
2542
  if (gen.environment.isMonorepo) {
2357
- log(chalk8.gray(` Monorepo: yes`));
2543
+ log(chalk10.gray(` Monorepo: yes`));
2358
2544
  }
2359
2545
  if (gen.environment.targetPackage) {
2360
- log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
2546
+ log(chalk10.gray(` Target package: ${gen.environment.targetPackage}`));
2361
2547
  }
2362
2548
  if (gen.issues.length > 0) {
2363
2549
  log("");
2364
- log(chalk8.bold("Issues"));
2550
+ log(chalk10.bold("Issues"));
2365
2551
  for (const issue of gen.issues) {
2366
- const prefix = issue.severity === "error" ? chalk8.red(">") : issue.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2552
+ const prefix = issue.severity === "error" ? chalk10.red(">") : issue.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
2367
2553
  log(`${prefix} [${issue.code}] ${issue.message}`);
2368
2554
  if (issue.suggestion) {
2369
- log(chalk8.gray(` ${issue.suggestion}`));
2555
+ log(chalk10.gray(` ${issue.suggestion}`));
2370
2556
  }
2371
2557
  }
2372
2558
  }
2373
2559
  }
2374
2560
  if (options.showDiagnostics && result.diagnostics.length > 0) {
2375
2561
  log("");
2376
- log(chalk8.bold("Diagnostics"));
2562
+ log(chalk10.bold("Diagnostics"));
2377
2563
  for (const diagnostic of result.diagnostics) {
2378
- const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2564
+ const prefix = diagnostic.severity === "error" ? chalk10.red(">") : diagnostic.severity === "warning" ? chalk10.yellow(">") : chalk10.cyan(">");
2379
2565
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2380
2566
  }
2381
2567
  }
2382
2568
  } catch (commandError) {
2383
- error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2569
+ error(chalk10.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2384
2570
  process.exit(1);
2385
2571
  }
2386
2572
  });
2387
2573
  }
2388
2574
 
2389
2575
  // src/commands/trends.ts
2390
- import * as fs6 from "node:fs";
2391
- import * as path8 from "node:path";
2576
+ import * as fs7 from "node:fs";
2577
+ import * as path9 from "node:path";
2392
2578
  import {
2393
2579
  formatDelta as formatDelta2,
2394
2580
  getExtendedTrend,
@@ -2396,11 +2582,11 @@ import {
2396
2582
  loadSnapshots,
2397
2583
  pruneByTier,
2398
2584
  pruneHistory,
2399
- renderSparkline,
2400
2585
  RETENTION_DAYS,
2586
+ renderSparkline,
2401
2587
  saveSnapshot
2402
2588
  } from "@doccov/sdk";
2403
- import chalk9 from "chalk";
2589
+ import chalk11 from "chalk";
2404
2590
  function formatDate(timestamp) {
2405
2591
  const date = new Date(timestamp);
2406
2592
  return date.toLocaleDateString("en-US", {
@@ -2413,19 +2599,19 @@ function formatDate(timestamp) {
2413
2599
  }
2414
2600
  function getColorForScore(score) {
2415
2601
  if (score >= 90)
2416
- return chalk9.green;
2602
+ return chalk11.green;
2417
2603
  if (score >= 70)
2418
- return chalk9.yellow;
2604
+ return chalk11.yellow;
2419
2605
  if (score >= 50)
2420
- return chalk9.hex("#FFA500");
2421
- return chalk9.red;
2606
+ return chalk11.hex("#FFA500");
2607
+ return chalk11.red;
2422
2608
  }
2423
2609
  function formatSnapshot(snapshot) {
2424
2610
  const color = getColorForScore(snapshot.coverageScore);
2425
2611
  const date = formatDate(snapshot.timestamp);
2426
2612
  const version2 = snapshot.version ? ` v${snapshot.version}` : "";
2427
- const commit = snapshot.commit ? chalk9.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
2428
- return `${chalk9.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
2613
+ const commit = snapshot.commit ? chalk11.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
2614
+ return `${chalk11.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
2429
2615
  }
2430
2616
  function formatWeekDate(timestamp) {
2431
2617
  const date = new Date(timestamp);
@@ -2433,55 +2619,55 @@ function formatWeekDate(timestamp) {
2433
2619
  }
2434
2620
  function formatVelocity(velocity) {
2435
2621
  if (velocity > 0)
2436
- return chalk9.green(`+${velocity}%/day`);
2622
+ return chalk11.green(`+${velocity}%/day`);
2437
2623
  if (velocity < 0)
2438
- return chalk9.red(`${velocity}%/day`);
2439
- return chalk9.gray("0%/day");
2624
+ return chalk11.red(`${velocity}%/day`);
2625
+ return chalk11.gray("0%/day");
2440
2626
  }
2441
2627
  function registerTrendsCommand(program) {
2442
2628
  program.command("trends").description("Show coverage trends over time").option("--cwd <dir>", "Working directory", process.cwd()).option("-n, --limit <count>", "Number of snapshots to show", "10").option("--prune <count>", "Prune history to keep only N snapshots").option("--record", "Record current coverage to history").option("--json", "Output as JSON").option("--extended", "Show extended trend analysis (velocity, projections)").option("--tier <tier>", "Retention tier: free (7d), team (30d), pro (90d)", "pro").option("--weekly", "Show weekly summary breakdown").action(async (options) => {
2443
- const cwd = path8.resolve(options.cwd);
2629
+ const cwd = path9.resolve(options.cwd);
2444
2630
  const tier = options.tier ?? "pro";
2445
2631
  if (options.prune) {
2446
2632
  const keepCount = parseInt(options.prune, 10);
2447
- if (!isNaN(keepCount)) {
2633
+ if (!Number.isNaN(keepCount)) {
2448
2634
  const deleted = pruneHistory(cwd, keepCount);
2449
- console.log(chalk9.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2635
+ console.log(chalk11.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2450
2636
  } else {
2451
2637
  const deleted = pruneByTier(cwd, tier);
2452
- console.log(chalk9.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2638
+ console.log(chalk11.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2453
2639
  }
2454
2640
  return;
2455
2641
  }
2456
2642
  if (options.record) {
2457
- const specPath = path8.resolve(cwd, "openpkg.json");
2458
- if (!fs6.existsSync(specPath)) {
2459
- console.error(chalk9.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2643
+ const specPath = path9.resolve(cwd, "openpkg.json");
2644
+ if (!fs7.existsSync(specPath)) {
2645
+ console.error(chalk11.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2460
2646
  process.exit(1);
2461
2647
  }
2462
2648
  try {
2463
- const specContent = fs6.readFileSync(specPath, "utf-8");
2649
+ const specContent = fs7.readFileSync(specPath, "utf-8");
2464
2650
  const spec = JSON.parse(specContent);
2465
2651
  const trend = getTrend(spec, cwd);
2466
2652
  saveSnapshot(trend.current, cwd);
2467
- console.log(chalk9.green("Recorded coverage snapshot:"));
2653
+ console.log(chalk11.green("Recorded coverage snapshot:"));
2468
2654
  console.log(formatSnapshot(trend.current));
2469
2655
  if (trend.delta !== undefined) {
2470
2656
  const deltaStr = formatDelta2(trend.delta);
2471
- const deltaColor = trend.delta > 0 ? chalk9.green : trend.delta < 0 ? chalk9.red : chalk9.gray;
2472
- console.log(chalk9.gray("Change from previous:"), deltaColor(deltaStr));
2657
+ const deltaColor = trend.delta > 0 ? chalk11.green : trend.delta < 0 ? chalk11.red : chalk11.gray;
2658
+ console.log(chalk11.gray("Change from previous:"), deltaColor(deltaStr));
2473
2659
  }
2474
2660
  return;
2475
2661
  } catch (error) {
2476
- console.error(chalk9.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2662
+ console.error(chalk11.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2477
2663
  process.exit(1);
2478
2664
  }
2479
2665
  }
2480
2666
  const snapshots = loadSnapshots(cwd);
2481
2667
  const limit = parseInt(options.limit ?? "10", 10);
2482
2668
  if (snapshots.length === 0) {
2483
- console.log(chalk9.yellow("No coverage history found."));
2484
- console.log(chalk9.gray("Run `doccov trends --record` to save the current coverage."));
2669
+ console.log(chalk11.yellow("No coverage history found."));
2670
+ console.log(chalk11.gray("Run `doccov trends --record` to save the current coverage."));
2485
2671
  return;
2486
2672
  }
2487
2673
  if (options.json) {
@@ -2496,28 +2682,28 @@ function registerTrendsCommand(program) {
2496
2682
  }
2497
2683
  const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
2498
2684
  const sparkline = renderSparkline(sparklineData);
2499
- console.log(chalk9.bold("Coverage Trends"));
2500
- console.log(chalk9.gray(`Package: ${snapshots[0].package}`));
2501
- console.log(chalk9.gray(`Sparkline: ${sparkline}`));
2685
+ console.log(chalk11.bold("Coverage Trends"));
2686
+ console.log(chalk11.gray(`Package: ${snapshots[0].package}`));
2687
+ console.log(chalk11.gray(`Sparkline: ${sparkline}`));
2502
2688
  console.log("");
2503
2689
  if (snapshots.length >= 2) {
2504
2690
  const oldest = snapshots[snapshots.length - 1];
2505
2691
  const newest = snapshots[0];
2506
2692
  const overallDelta = newest.coverageScore - oldest.coverageScore;
2507
2693
  const deltaStr = formatDelta2(overallDelta);
2508
- const deltaColor = overallDelta > 0 ? chalk9.green : overallDelta < 0 ? chalk9.red : chalk9.gray;
2509
- console.log(chalk9.gray("Overall trend:"), deltaColor(deltaStr), chalk9.gray(`(${snapshots.length} snapshots)`));
2694
+ const deltaColor = overallDelta > 0 ? chalk11.green : overallDelta < 0 ? chalk11.red : chalk11.gray;
2695
+ console.log(chalk11.gray("Overall trend:"), deltaColor(deltaStr), chalk11.gray(`(${snapshots.length} snapshots)`));
2510
2696
  console.log("");
2511
2697
  }
2512
2698
  if (options.extended) {
2513
- const specPath = path8.resolve(cwd, "openpkg.json");
2514
- if (fs6.existsSync(specPath)) {
2699
+ const specPath = path9.resolve(cwd, "openpkg.json");
2700
+ if (fs7.existsSync(specPath)) {
2515
2701
  try {
2516
- const specContent = fs6.readFileSync(specPath, "utf-8");
2702
+ const specContent = fs7.readFileSync(specPath, "utf-8");
2517
2703
  const spec = JSON.parse(specContent);
2518
2704
  const extended = getExtendedTrend(spec, cwd, { tier });
2519
- console.log(chalk9.bold("Extended Analysis"));
2520
- console.log(chalk9.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
2705
+ console.log(chalk11.bold("Extended Analysis"));
2706
+ console.log(chalk11.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
2521
2707
  console.log("");
2522
2708
  console.log(" Velocity:");
2523
2709
  console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
@@ -2526,55 +2712,55 @@ function registerTrendsCommand(program) {
2526
2712
  console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
2527
2713
  }
2528
2714
  console.log("");
2529
- const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk9.green : chalk9.red;
2715
+ const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk11.green : chalk11.red;
2530
2716
  console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
2531
- console.log(` All-time high: ${chalk9.green(`${extended.allTimeHigh}%`)}`);
2532
- console.log(` All-time low: ${chalk9.red(`${extended.allTimeLow}%`)}`);
2717
+ console.log(` All-time high: ${chalk11.green(`${extended.allTimeHigh}%`)}`);
2718
+ console.log(` All-time low: ${chalk11.red(`${extended.allTimeLow}%`)}`);
2533
2719
  if (extended.dataRange) {
2534
2720
  const startDate = formatWeekDate(extended.dataRange.start);
2535
2721
  const endDate = formatWeekDate(extended.dataRange.end);
2536
- console.log(chalk9.gray(` Data range: ${startDate} - ${endDate}`));
2722
+ console.log(chalk11.gray(` Data range: ${startDate} - ${endDate}`));
2537
2723
  }
2538
2724
  console.log("");
2539
2725
  if (options.weekly && extended.weeklySummaries.length > 0) {
2540
- console.log(chalk9.bold("Weekly Summary"));
2726
+ console.log(chalk11.bold("Weekly Summary"));
2541
2727
  const weekLimit = Math.min(extended.weeklySummaries.length, 8);
2542
2728
  for (let i = 0;i < weekLimit; i++) {
2543
2729
  const week = extended.weeklySummaries[i];
2544
2730
  const weekStart = formatWeekDate(week.weekStart);
2545
2731
  const weekEnd = formatWeekDate(week.weekEnd);
2546
- const deltaColor = week.delta > 0 ? chalk9.green : week.delta < 0 ? chalk9.red : chalk9.gray;
2732
+ const deltaColor = week.delta > 0 ? chalk11.green : week.delta < 0 ? chalk11.red : chalk11.gray;
2547
2733
  const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
2548
2734
  console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
2549
2735
  }
2550
2736
  if (extended.weeklySummaries.length > weekLimit) {
2551
- console.log(chalk9.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2737
+ console.log(chalk11.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2552
2738
  }
2553
2739
  console.log("");
2554
2740
  }
2555
2741
  } catch {
2556
- console.log(chalk9.yellow("Could not load openpkg.json for extended analysis"));
2742
+ console.log(chalk11.yellow("Could not load openpkg.json for extended analysis"));
2557
2743
  console.log("");
2558
2744
  }
2559
2745
  }
2560
2746
  }
2561
- console.log(chalk9.bold("History"));
2747
+ console.log(chalk11.bold("History"));
2562
2748
  const displaySnapshots = snapshots.slice(0, limit);
2563
2749
  for (let i = 0;i < displaySnapshots.length; i++) {
2564
2750
  const snapshot = displaySnapshots[i];
2565
- const prefix = i === 0 ? chalk9.cyan("→") : " ";
2751
+ const prefix = i === 0 ? chalk11.cyan("→") : " ";
2566
2752
  console.log(`${prefix} ${formatSnapshot(snapshot)}`);
2567
2753
  }
2568
2754
  if (snapshots.length > limit) {
2569
- console.log(chalk9.gray(` ... and ${snapshots.length - limit} more`));
2755
+ console.log(chalk11.gray(` ... and ${snapshots.length - limit} more`));
2570
2756
  }
2571
2757
  });
2572
2758
  }
2573
2759
 
2574
2760
  // src/cli.ts
2575
2761
  var __filename2 = fileURLToPath(import.meta.url);
2576
- var __dirname2 = path9.dirname(__filename2);
2577
- var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
2762
+ var __dirname2 = path10.dirname(__filename2);
2763
+ var packageJson = JSON.parse(readFileSync5(path10.join(__dirname2, "../package.json"), "utf-8"));
2578
2764
  var program = new Command;
2579
2765
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2580
2766
  registerCheckCommand(program);