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