@doccov/cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,22 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
2
 
21
3
  // src/config/doccov-config.ts
22
4
  import { access } from "node:fs/promises";
@@ -178,14 +160,14 @@ ${formatIssues(issues)}`);
178
160
  // src/config/index.ts
179
161
  var defineConfig = (config) => config;
180
162
  // src/cli.ts
181
- import { readFileSync as readFileSync4 } from "node:fs";
182
- import * as path9 from "node:path";
163
+ import { readFileSync as readFileSync3 } from "node:fs";
164
+ import * as path8 from "node:path";
183
165
  import { fileURLToPath } from "node:url";
184
166
  import { Command } from "commander";
185
167
 
186
168
  // src/commands/check.ts
187
169
  import * as fs2 from "node:fs";
188
- import * as path3 from "node:path";
170
+ import * as path4 from "node:path";
189
171
  import {
190
172
  applyEdits,
191
173
  categorizeDrifts,
@@ -208,6 +190,223 @@ import {
208
190
  } from "@openpkg-ts/spec";
209
191
  import chalk2 from "chalk";
210
192
 
193
+ // src/reports/diff-markdown.ts
194
+ import * as path2 from "node:path";
195
+ function bar(pct, width = 10) {
196
+ const filled = Math.round(pct / 100 * width);
197
+ return "█".repeat(filled) + "░".repeat(width - filled);
198
+ }
199
+ function formatDelta(delta) {
200
+ if (delta > 0)
201
+ return `+${delta}%`;
202
+ if (delta < 0)
203
+ return `${delta}%`;
204
+ return "0%";
205
+ }
206
+ function renderDiffMarkdown(data, options = {}) {
207
+ const limit = options.limit ?? 10;
208
+ const lines = [];
209
+ lines.push(`# DocCov Diff Report`);
210
+ lines.push("");
211
+ lines.push(`**Comparing:** \`${data.baseName}\` → \`${data.headName}\``);
212
+ lines.push("");
213
+ lines.push("## Coverage Summary");
214
+ lines.push("");
215
+ const deltaIndicator = data.coverageDelta > 0 ? "↑" : data.coverageDelta < 0 ? "↓" : "→";
216
+ lines.push(`| Metric | Base | Head | Delta |`);
217
+ lines.push(`|--------|------|------|-------|`);
218
+ lines.push(`| Coverage | ${data.oldCoverage}% | ${data.newCoverage}% | ${deltaIndicator} ${formatDelta(data.coverageDelta)} |`);
219
+ lines.push(`| Breaking Changes | - | ${data.breaking.length} | - |`);
220
+ lines.push(`| New Exports | - | ${data.nonBreaking.length} | - |`);
221
+ lines.push(`| New Undocumented | - | ${data.newUndocumented.length} | - |`);
222
+ if (data.driftIntroduced > 0 || data.driftResolved > 0) {
223
+ const driftDelta = data.driftIntroduced > 0 ? `+${data.driftIntroduced}` : data.driftResolved > 0 ? `-${data.driftResolved}` : "0";
224
+ lines.push(`| Drift | - | - | ${driftDelta} |`);
225
+ }
226
+ if (data.breaking.length > 0) {
227
+ lines.push("");
228
+ lines.push("## Breaking Changes");
229
+ lines.push("");
230
+ const categorized = data.categorizedBreaking ?? [];
231
+ const highSeverity = categorized.filter((c) => c.severity === "high");
232
+ const otherSeverity = categorized.filter((c) => c.severity !== "high");
233
+ if (highSeverity.length > 0) {
234
+ lines.push("### High Severity");
235
+ lines.push("");
236
+ lines.push("| Name | Kind | Reason |");
237
+ lines.push("|------|------|--------|");
238
+ for (const c of highSeverity.slice(0, limit)) {
239
+ lines.push(`| \`${c.name}\` | ${c.kind} | ${c.reason} |`);
240
+ }
241
+ if (highSeverity.length > limit) {
242
+ lines.push(`| ... | | ${highSeverity.length - limit} more |`);
243
+ }
244
+ lines.push("");
245
+ }
246
+ if (otherSeverity.length > 0) {
247
+ lines.push("### Medium/Low Severity");
248
+ lines.push("");
249
+ lines.push("| Name | Kind | Reason |");
250
+ lines.push("|------|------|--------|");
251
+ for (const c of otherSeverity.slice(0, limit)) {
252
+ lines.push(`| \`${c.name}\` | ${c.kind} | ${c.reason} |`);
253
+ }
254
+ if (otherSeverity.length > limit) {
255
+ lines.push(`| ... | | ${otherSeverity.length - limit} more |`);
256
+ }
257
+ lines.push("");
258
+ }
259
+ }
260
+ if (data.memberChanges && data.memberChanges.length > 0) {
261
+ lines.push("");
262
+ lines.push("## Member Changes");
263
+ lines.push("");
264
+ const byClass = groupMemberChangesByClass(data.memberChanges);
265
+ for (const [className, changes] of byClass) {
266
+ lines.push(`### ${className}`);
267
+ lines.push("");
268
+ lines.push("| Member | Change | Details |");
269
+ lines.push("|--------|--------|---------|");
270
+ for (const mc of changes.slice(0, limit)) {
271
+ const details = getChangeDetails(mc);
272
+ lines.push(`| \`${mc.memberName}()\` | ${mc.changeType} | ${details} |`);
273
+ }
274
+ if (changes.length > limit) {
275
+ lines.push(`| ... | | ${changes.length - limit} more |`);
276
+ }
277
+ lines.push("");
278
+ }
279
+ }
280
+ if (data.nonBreaking.length > 0) {
281
+ lines.push("");
282
+ lines.push("## New Exports");
283
+ lines.push("");
284
+ const undocSet = new Set(data.newUndocumented);
285
+ lines.push("| Export | Documented |");
286
+ lines.push("|--------|------------|");
287
+ for (const name of data.nonBreaking.slice(0, limit)) {
288
+ const documented = undocSet.has(name) ? "No" : "Yes";
289
+ lines.push(`| \`${name}\` | ${documented} |`);
290
+ }
291
+ if (data.nonBreaking.length > limit) {
292
+ lines.push(`| ... | ${data.nonBreaking.length - limit} more |`);
293
+ }
294
+ }
295
+ if (data.docsImpact) {
296
+ renderDocsImpactSection(lines, data.docsImpact, limit);
297
+ }
298
+ lines.push("");
299
+ lines.push("---");
300
+ lines.push("*Generated by [DocCov](https://doccov.com)*");
301
+ return lines.join(`
302
+ `);
303
+ }
304
+ function groupMemberChangesByClass(changes) {
305
+ const byClass = new Map;
306
+ for (const mc of changes) {
307
+ const list = byClass.get(mc.className) ?? [];
308
+ list.push(mc);
309
+ byClass.set(mc.className, list);
310
+ }
311
+ return byClass;
312
+ }
313
+ function getChangeDetails(mc) {
314
+ if (mc.changeType === "removed") {
315
+ return mc.suggestion ? `→ ${mc.suggestion}` : "removed";
316
+ }
317
+ if (mc.changeType === "signature-changed") {
318
+ if (mc.oldSignature && mc.newSignature) {
319
+ return `\`${mc.oldSignature}\` → \`${mc.newSignature}\``;
320
+ }
321
+ return "signature changed";
322
+ }
323
+ if (mc.changeType === "added") {
324
+ return "new";
325
+ }
326
+ return "-";
327
+ }
328
+ function renderDocsImpactSection(lines, docsImpact, limit) {
329
+ const { impactedFiles, missingDocs, stats, allUndocumented } = docsImpact;
330
+ lines.push("");
331
+ lines.push("## Documentation Impact");
332
+ lines.push("");
333
+ lines.push(`Scanned **${stats.filesScanned}** files, **${stats.codeBlocksFound}** code blocks, **${stats.referencesFound}** references.`);
334
+ lines.push("");
335
+ if (impactedFiles.length === 0 && missingDocs.length === 0) {
336
+ lines.push("No documentation updates required.");
337
+ return;
338
+ }
339
+ if (impactedFiles.length > 0) {
340
+ lines.push("### Files Requiring Updates");
341
+ lines.push("");
342
+ lines.push("| File | Issues | Details |");
343
+ lines.push("|------|--------|---------|");
344
+ for (const file of impactedFiles.slice(0, limit)) {
345
+ const filename = path2.basename(file.file);
346
+ const issueCount = file.references.length;
347
+ const firstRef = file.references[0];
348
+ const detail = firstRef ? `L${firstRef.line}: ${firstRef.memberName ?? firstRef.exportName}` : "-";
349
+ lines.push(`| \`${filename}\` | ${issueCount} | ${detail} |`);
350
+ }
351
+ if (impactedFiles.length > limit) {
352
+ lines.push(`| ... | | ${impactedFiles.length - limit} more files |`);
353
+ }
354
+ lines.push("");
355
+ }
356
+ if (missingDocs.length > 0) {
357
+ lines.push("### New Exports Missing Documentation");
358
+ lines.push("");
359
+ lines.push("| Export |");
360
+ lines.push("|--------|");
361
+ for (const name of missingDocs.slice(0, limit)) {
362
+ lines.push(`| \`${name}\` |`);
363
+ }
364
+ if (missingDocs.length > limit) {
365
+ lines.push(`| ... ${missingDocs.length - limit} more |`);
366
+ }
367
+ lines.push("");
368
+ }
369
+ if (allUndocumented && allUndocumented.length > 0 && stats.totalExports > 0) {
370
+ const docPercent = Math.round((stats.totalExports - allUndocumented.length) / stats.totalExports * 100);
371
+ lines.push("### Documentation Coverage");
372
+ lines.push("");
373
+ lines.push(`**${stats.documentedExports}/${stats.totalExports}** exports documented (${docPercent}%) \`${bar(docPercent)}\``);
374
+ lines.push("");
375
+ }
376
+ }
377
+
378
+ // src/reports/diff-html.ts
379
+ function escapeHtml(s) {
380
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
381
+ }
382
+ function renderDiffHtml(data, options = {}) {
383
+ const md = renderDiffMarkdown(data, options);
384
+ return `<!DOCTYPE html>
385
+ <html lang="en">
386
+ <head>
387
+ <meta charset="UTF-8">
388
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
389
+ <title>DocCov Diff: ${escapeHtml(data.baseName)} → ${escapeHtml(data.headName)}</title>
390
+ <style>
391
+ :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --success: #3fb950; --warning: #d29922; --danger: #f85149; }
392
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
393
+ h1, h2, h3 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
394
+ h1 { color: var(--accent); }
395
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
396
+ th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
397
+ th { background: #161b22; }
398
+ code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
399
+ a { color: var(--accent); }
400
+ .delta-positive { color: var(--success); }
401
+ .delta-negative { color: var(--danger); }
402
+ .delta-neutral { color: var(--fg); }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
407
+ </body>
408
+ </html>`;
409
+ }
211
410
  // src/reports/github.ts
212
411
  function renderGithubSummary(stats, options = {}) {
213
412
  const coverageScore = options.coverageScore ?? stats.coverageScore;
@@ -235,7 +434,7 @@ ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "ne
235
434
  }
236
435
  // src/reports/markdown.ts
237
436
  import { DRIFT_CATEGORY_LABELS } from "@openpkg-ts/spec";
238
- function bar(pct, width = 10) {
437
+ function bar2(pct, width = 10) {
239
438
  const filled = Math.round(pct / 100 * width);
240
439
  return "█".repeat(filled) + "░".repeat(width - filled);
241
440
  }
@@ -244,7 +443,7 @@ function renderMarkdown(stats, options = {}) {
244
443
  const lines = [];
245
444
  lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
246
445
  lines.push("");
247
- lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar(stats.coverageScore)}\``);
446
+ lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar2(stats.coverageScore)}\``);
248
447
  lines.push("");
249
448
  lines.push("| Metric | Value |");
250
449
  lines.push("|--------|-------|");
@@ -259,7 +458,7 @@ function renderMarkdown(stats, options = {}) {
259
458
  lines.push("| Signal | Coverage |");
260
459
  lines.push("|--------|----------|");
261
460
  for (const [sig, s] of Object.entries(stats.signalCoverage)) {
262
- lines.push(`| ${sig} | ${s.pct}% \`${bar(s.pct, 8)}\` |`);
461
+ lines.push(`| ${sig} | ${s.pct}% \`${bar2(s.pct, 8)}\` |`);
263
462
  }
264
463
  if (stats.byKind.length > 0) {
265
464
  lines.push("");
@@ -331,7 +530,7 @@ function renderMarkdown(stats, options = {}) {
331
530
  }
332
531
 
333
532
  // src/reports/html.ts
334
- function escapeHtml(s) {
533
+ function escapeHtml2(s) {
335
534
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
336
535
  }
337
536
  function renderHtml(stats, options = {}) {
@@ -341,7 +540,7 @@ function renderHtml(stats, options = {}) {
341
540
  <head>
342
541
  <meta charset="UTF-8">
343
542
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
344
- <title>DocCov Report: ${escapeHtml(stats.packageName)}</title>
543
+ <title>DocCov Report: ${escapeHtml2(stats.packageName)}</title>
345
544
  <style>
346
545
  :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
347
546
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
@@ -354,7 +553,7 @@ function renderHtml(stats, options = {}) {
354
553
  </style>
355
554
  </head>
356
555
  <body>
357
- <pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
556
+ <pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml2(md)}</pre>
358
557
  </body>
359
558
  </html>`;
360
559
  }
@@ -467,18 +666,18 @@ function computeStats(spec) {
467
666
  }
468
667
  // src/reports/writer.ts
469
668
  import * as fs from "node:fs";
470
- import * as path2 from "node:path";
669
+ import * as path3 from "node:path";
471
670
  import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
472
671
  import chalk from "chalk";
473
672
  function writeReport(options) {
474
673
  const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
475
- const reportPath = outputPath ? path2.resolve(cwd, outputPath) : path2.resolve(cwd, getReportPath(format));
476
- const dir = path2.dirname(reportPath);
674
+ const reportPath = outputPath ? path3.resolve(cwd, outputPath) : path3.resolve(cwd, getReportPath(format));
675
+ const dir = path3.dirname(reportPath);
477
676
  if (!fs.existsSync(dir)) {
478
677
  fs.mkdirSync(dir, { recursive: true });
479
678
  }
480
679
  fs.writeFileSync(reportPath, content);
481
- const relativePath = path2.relative(cwd, reportPath);
680
+ const relativePath = path3.relative(cwd, reportPath);
482
681
  if (!silent) {
483
682
  console.log(chalk.green(`✓ Wrote ${format} report to ${relativePath}`));
484
683
  }
@@ -571,6 +770,17 @@ async function parseAssertionsWithLLM(code) {
571
770
  }
572
771
  }
573
772
 
773
+ // src/utils/validation.ts
774
+ function clampPercentage(value, fallback = 80) {
775
+ if (Number.isNaN(value))
776
+ return fallback;
777
+ return Math.min(100, Math.max(0, Math.round(value)));
778
+ }
779
+ function resolveThreshold(cliValue, configValue) {
780
+ const raw = cliValue ?? configValue;
781
+ return raw !== undefined ? clampPercentage(Number(raw)) : undefined;
782
+ }
783
+
574
784
  // src/commands/check.ts
575
785
  var defaultDependencies = {
576
786
  createDocCov: (options) => new DocCov(options),
@@ -617,9 +827,9 @@ function registerCheckCommand(program, dependencies = {}) {
617
827
  }
618
828
  const config = await loadDocCovConfig(targetDir);
619
829
  const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
620
- const minCoverage = minCoverageRaw !== undefined ? clampCoverage(minCoverageRaw) : undefined;
830
+ const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
621
831
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
622
- const maxDrift = maxDriftRaw !== undefined ? clampCoverage(maxDriftRaw) : undefined;
832
+ const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
623
833
  const resolveExternalTypes = !options.skipResolve;
624
834
  let specResult;
625
835
  const doccov = createDocCov({
@@ -711,7 +921,7 @@ function registerCheckCommand(program, dependencies = {}) {
711
921
  log(chalk2.gray(` Skipping ${exp.name}: declaration file`));
712
922
  continue;
713
923
  }
714
- const filePath = path3.resolve(targetDir, exp.source.file);
924
+ const filePath = path4.resolve(targetDir, exp.source.file);
715
925
  if (!fs2.existsSync(filePath)) {
716
926
  log(chalk2.gray(` Skipping ${exp.name}: file not found`));
717
927
  continue;
@@ -755,7 +965,7 @@ function registerCheckCommand(program, dependencies = {}) {
755
965
  log(chalk2.bold("Dry run - changes that would be made:"));
756
966
  log("");
757
967
  for (const [filePath, fileEdits] of editsByFile) {
758
- const relativePath = path3.relative(targetDir, filePath);
968
+ const relativePath = path4.relative(targetDir, filePath);
759
969
  log(chalk2.cyan(` ${relativePath}:`));
760
970
  for (const { export: exp, edit, fixes } of fileEdits) {
761
971
  const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
@@ -929,12 +1139,6 @@ function registerCheckCommand(program, dependencies = {}) {
929
1139
  }
930
1140
  });
931
1141
  }
932
- function clampCoverage(value) {
933
- if (Number.isNaN(value)) {
934
- return 80;
935
- }
936
- return Math.min(100, Math.max(0, Math.round(value)));
937
- }
938
1142
  function collectDrift(exportsList) {
939
1143
  const drifts = [];
940
1144
  for (const entry of exportsList) {
@@ -957,11 +1161,14 @@ function collectDrift(exportsList) {
957
1161
 
958
1162
  // src/commands/diff.ts
959
1163
  import * as fs3 from "node:fs";
960
- import * as path4 from "node:path";
1164
+ import * as path5 from "node:path";
961
1165
  import {
962
1166
  diffSpecWithDocs,
1167
+ ensureSpecCoverage,
1168
+ getDiffReportPath,
963
1169
  getDocsImpactSummary,
964
1170
  hasDocsImpact,
1171
+ hashString,
965
1172
  parseMarkdownFiles
966
1173
  } from "@doccov/sdk";
967
1174
  import chalk3 from "chalk";
@@ -1033,126 +1240,151 @@ var defaultDependencies2 = {
1033
1240
  log: console.log,
1034
1241
  error: console.error
1035
1242
  };
1036
- var VALID_STRICT_OPTIONS = [
1037
- "regression",
1038
- "drift",
1039
- "docs-impact",
1040
- "breaking",
1041
- "undocumented",
1042
- "all"
1043
- ];
1044
- function parseStrictOptions(value) {
1045
- if (!value)
1243
+ var STRICT_PRESETS = {
1244
+ ci: new Set(["breaking", "regression"]),
1245
+ release: new Set(["breaking", "regression", "drift", "docs-impact", "undocumented"]),
1246
+ quality: new Set(["drift", "undocumented"])
1247
+ };
1248
+ function getStrictChecks(preset) {
1249
+ if (!preset)
1046
1250
  return new Set;
1047
- const options = value.split(",").map((s) => s.trim().toLowerCase());
1048
- const result = new Set;
1049
- for (const opt of options) {
1050
- if (opt === "all") {
1051
- for (const o of VALID_STRICT_OPTIONS) {
1052
- if (o !== "all")
1053
- result.add(o);
1054
- }
1055
- } else if (VALID_STRICT_OPTIONS.includes(opt)) {
1056
- result.add(opt);
1057
- }
1251
+ const checks = STRICT_PRESETS[preset];
1252
+ if (!checks) {
1253
+ throw new Error(`Unknown --strict preset: ${preset}. Valid: ci, release, quality`);
1058
1254
  }
1059
- return result;
1255
+ return checks;
1060
1256
  }
1061
1257
  function registerDiffCommand(program, dependencies = {}) {
1062
1258
  const { readFileSync: readFileSync2, log, error } = {
1063
1259
  ...defaultDependencies2,
1064
1260
  ...dependencies
1065
1261
  };
1066
- program.command("diff <base> <head>").description("Compare two OpenPkg specs and report coverage delta").option("--format <format>", "Output format: text, json, github, report", "text").option("--strict <options>", "Fail on conditions (comma-separated): regression, drift, docs-impact, breaking, undocumented, all").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--output <format>", "DEPRECATED: Use --format instead").option("--fail-on-regression", "DEPRECATED: Use --strict regression").option("--fail-on-drift", "DEPRECATED: Use --strict drift").option("--fail-on-docs-impact", "DEPRECATED: Use --strict docs-impact").action(async (base, head, options) => {
1262
+ program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
1067
1263
  try {
1068
- const baseSpec = loadSpec(base, readFileSync2);
1069
- const headSpec = loadSpec(head, readFileSync2);
1070
- let markdownFiles;
1071
- let docsPatterns = options.docs;
1072
- if (!docsPatterns || docsPatterns.length === 0) {
1073
- const configResult = await loadDocCovConfig(process.cwd());
1074
- if (configResult?.docs?.include) {
1075
- docsPatterns = configResult.docs.include;
1076
- log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1077
- }
1264
+ const baseFile = options.base ?? baseArg;
1265
+ const headFile = options.head ?? headArg;
1266
+ if (!baseFile || !headFile) {
1267
+ throw new Error(`Both base and head specs are required.
1268
+ ` + `Usage: doccov diff <base> <head>
1269
+ ` + " or: doccov diff --base main.json --head feature.json");
1078
1270
  }
1079
- if (docsPatterns && docsPatterns.length > 0) {
1080
- markdownFiles = await loadMarkdownFiles(docsPatterns);
1271
+ const baseSpec = loadSpec(baseFile, readFileSync2);
1272
+ const headSpec = loadSpec(headFile, readFileSync2);
1273
+ const config = await loadDocCovConfig(options.cwd);
1274
+ const baseHash = hashString(JSON.stringify(baseSpec));
1275
+ const headHash = hashString(JSON.stringify(headSpec));
1276
+ const cacheEnabled = options.cache !== false;
1277
+ const cachedReportPath = path5.resolve(options.cwd, getDiffReportPath(baseHash, headHash, "json"));
1278
+ let diff;
1279
+ let fromCache = false;
1280
+ if (cacheEnabled && fs3.existsSync(cachedReportPath)) {
1281
+ try {
1282
+ const cached = JSON.parse(fs3.readFileSync(cachedReportPath, "utf-8"));
1283
+ diff = cached;
1284
+ fromCache = true;
1285
+ } catch {
1286
+ diff = await generateDiff(baseSpec, headSpec, options, config, log);
1287
+ }
1288
+ } else {
1289
+ diff = await generateDiff(baseSpec, headSpec, options, config, log);
1081
1290
  }
1082
- const diff = diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
1083
- const format = options.format ?? options.output ?? "text";
1084
- const strictOptions = parseStrictOptions(options.strict);
1085
- if (options.failOnRegression)
1086
- strictOptions.add("regression");
1087
- if (options.failOnDrift)
1088
- strictOptions.add("drift");
1089
- if (options.failOnDocsImpact)
1090
- strictOptions.add("docs-impact");
1291
+ const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
1292
+ const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
1293
+ const format = options.format ?? "text";
1294
+ const limit = parseInt(options.limit, 10) || 10;
1295
+ const checks = getStrictChecks(options.strict);
1296
+ const baseName = path5.basename(baseFile);
1297
+ const headName = path5.basename(headFile);
1298
+ const reportData = {
1299
+ baseName,
1300
+ headName,
1301
+ ...diff
1302
+ };
1091
1303
  switch (format) {
1092
- case "json":
1093
- log(JSON.stringify(diff, null, 2));
1304
+ case "text":
1305
+ printSummary(diff, baseName, headName, fromCache, log);
1306
+ if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
1307
+ await printAISummary(diff, log);
1308
+ }
1309
+ if (!options.stdout) {
1310
+ const jsonPath = getDiffReportPath(baseHash, headHash, "json");
1311
+ if (!fromCache) {
1312
+ writeReport({
1313
+ format: "json",
1314
+ content: JSON.stringify(diff, null, 2),
1315
+ cwd: options.cwd,
1316
+ outputPath: jsonPath,
1317
+ silent: true
1318
+ });
1319
+ }
1320
+ const cacheNote = fromCache ? chalk3.cyan(" (cached)") : "";
1321
+ log(chalk3.dim(`Report: ${jsonPath}`) + cacheNote);
1322
+ }
1094
1323
  break;
1095
- case "github":
1096
- printGitHubAnnotations(diff, log);
1324
+ case "json": {
1325
+ const content = JSON.stringify(diff, null, 2);
1326
+ if (options.stdout) {
1327
+ log(content);
1328
+ } else {
1329
+ const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "json");
1330
+ writeReport({
1331
+ format: "json",
1332
+ content,
1333
+ outputPath,
1334
+ cwd: options.cwd
1335
+ });
1336
+ }
1097
1337
  break;
1098
- case "report":
1099
- log(generateHTMLReport(diff));
1338
+ }
1339
+ case "markdown": {
1340
+ const content = renderDiffMarkdown(reportData, { limit });
1341
+ if (options.stdout) {
1342
+ log(content);
1343
+ } else {
1344
+ const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "markdown");
1345
+ writeReport({
1346
+ format: "markdown",
1347
+ content,
1348
+ outputPath,
1349
+ cwd: options.cwd
1350
+ });
1351
+ }
1100
1352
  break;
1101
- default:
1102
- printTextDiff(diff, log, error);
1103
- if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
1104
- if (!isAIDocsAnalysisAvailable()) {
1105
- log(chalk3.yellow(`
1106
- AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1107
- } else {
1108
- log(chalk3.gray(`
1109
- Generating AI summary...`));
1110
- const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1111
- file: f.file,
1112
- exportName: r.exportName,
1113
- changeType: r.changeType,
1114
- context: r.context
1115
- })));
1116
- const summary = await generateImpactSummary(impacts);
1117
- if (summary) {
1118
- log("");
1119
- log(chalk3.bold("AI Summary"));
1120
- log(chalk3.cyan(` ${summary}`));
1121
- }
1122
- }
1353
+ }
1354
+ case "html": {
1355
+ const content = renderDiffHtml(reportData, { limit });
1356
+ if (options.stdout) {
1357
+ log(content);
1358
+ } else {
1359
+ const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "html");
1360
+ writeReport({
1361
+ format: "html",
1362
+ content,
1363
+ outputPath,
1364
+ cwd: options.cwd
1365
+ });
1123
1366
  }
1124
1367
  break;
1368
+ }
1369
+ case "github":
1370
+ printGitHubAnnotations(diff, log);
1371
+ break;
1125
1372
  }
1126
- if (strictOptions.has("regression") && diff.coverageDelta < 0) {
1127
- error(chalk3.red(`
1128
- Coverage regressed by ${Math.abs(diff.coverageDelta)}%`));
1129
- process.exitCode = 1;
1130
- return;
1131
- }
1132
- if (strictOptions.has("drift") && diff.driftIntroduced > 0) {
1133
- error(chalk3.red(`
1134
- ${diff.driftIntroduced} new drift issue(s) introduced`));
1135
- process.exitCode = 1;
1136
- return;
1137
- }
1138
- if (strictOptions.has("docs-impact") && hasDocsImpact(diff)) {
1139
- const summary = getDocsImpactSummary(diff);
1140
- error(chalk3.red(`
1141
- ${summary.totalIssues} docs issue(s) require attention`));
1142
- process.exitCode = 1;
1143
- return;
1144
- }
1145
- if (strictOptions.has("breaking") && diff.breaking.length > 0) {
1146
- error(chalk3.red(`
1147
- ${diff.breaking.length} breaking change(s) detected`));
1148
- process.exitCode = 1;
1149
- return;
1150
- }
1151
- if (strictOptions.has("undocumented") && diff.newUndocumented.length > 0) {
1152
- error(chalk3.red(`
1153
- ${diff.newUndocumented.length} new undocumented export(s)`));
1373
+ const failures = validateDiff(diff, headSpec, {
1374
+ minCoverage,
1375
+ maxDrift,
1376
+ checks
1377
+ });
1378
+ if (failures.length > 0) {
1379
+ log(chalk3.red(`
1380
+ ✗ Check failed`));
1381
+ for (const f of failures) {
1382
+ log(chalk3.red(` - ${f}`));
1383
+ }
1154
1384
  process.exitCode = 1;
1155
- return;
1385
+ } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1386
+ log(chalk3.green(`
1387
+ ✓ All checks passed`));
1156
1388
  }
1157
1389
  } catch (commandError) {
1158
1390
  error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
@@ -1176,193 +1408,117 @@ async function loadMarkdownFiles(patterns) {
1176
1408
  }
1177
1409
  return parseMarkdownFiles(files);
1178
1410
  }
1411
+ async function generateDiff(baseSpec, headSpec, options, config, log) {
1412
+ let markdownFiles;
1413
+ let docsPatterns = options.docs;
1414
+ if (!docsPatterns || docsPatterns.length === 0) {
1415
+ if (config?.docs?.include) {
1416
+ docsPatterns = config.docs.include;
1417
+ log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1418
+ }
1419
+ }
1420
+ if (docsPatterns && docsPatterns.length > 0) {
1421
+ markdownFiles = await loadMarkdownFiles(docsPatterns);
1422
+ }
1423
+ return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
1424
+ }
1179
1425
  function loadSpec(filePath, readFileSync2) {
1180
- const resolvedPath = path4.resolve(filePath);
1426
+ const resolvedPath = path5.resolve(filePath);
1181
1427
  if (!fs3.existsSync(resolvedPath)) {
1182
1428
  throw new Error(`File not found: ${filePath}`);
1183
1429
  }
1184
1430
  try {
1185
1431
  const content = readFileSync2(resolvedPath, "utf-8");
1186
- return JSON.parse(content);
1432
+ const spec = JSON.parse(content);
1433
+ return ensureSpecCoverage(spec);
1187
1434
  } catch (parseError) {
1188
1435
  throw new Error(`Failed to parse ${filePath}: ${parseError instanceof Error ? parseError.message : parseError}`);
1189
1436
  }
1190
1437
  }
1191
- function printTextDiff(diff, log, _error) {
1438
+ function printSummary(diff, baseName, headName, fromCache, log) {
1192
1439
  log("");
1193
- log(chalk3.bold("DocCov Diff Report"));
1440
+ const cacheIndicator = fromCache ? chalk3.cyan(" (cached)") : "";
1441
+ log(chalk3.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1194
1442
  log("─".repeat(40));
1195
- printCoverage(diff, log);
1196
- printAPIChanges(diff, log);
1197
- if (diff.docsImpact) {
1198
- printDocsRequiringUpdates(diff, log);
1199
- }
1200
1443
  log("");
1201
- }
1202
- function printCoverage(diff, log) {
1203
1444
  const coverageColor = diff.coverageDelta > 0 ? chalk3.green : diff.coverageDelta < 0 ? chalk3.red : chalk3.gray;
1204
- const coverageSymbol = diff.coverageDelta > 0 ? "" : diff.coverageDelta < 0 ? "" : "→";
1205
- const deltaStr = diff.coverageDelta > 0 ? `+${diff.coverageDelta}` : String(diff.coverageDelta);
1206
- log("");
1207
- log(chalk3.bold("Coverage"));
1208
- log(` ${diff.oldCoverage}% ${coverageSymbol} ${diff.newCoverage}% ${coverageColor(`(${deltaStr}%)`)}`);
1209
- }
1210
- function printAPIChanges(diff, log) {
1211
- const hasChanges = diff.breaking.length > 0 || diff.nonBreaking.length > 0 || diff.memberChanges && diff.memberChanges.length > 0;
1212
- if (!hasChanges)
1213
- return;
1214
- log("");
1215
- log(chalk3.bold("API Changes"));
1216
- const membersByClass = groupMemberChangesByClass(diff.memberChanges ?? []);
1217
- const classesWithMembers = new Set(membersByClass.keys());
1218
- for (const [className, changes] of membersByClass) {
1219
- const categorized = diff.categorizedBreaking?.find((c) => c.id === className);
1220
- const isHighSeverity = categorized?.severity === "high";
1221
- const label = isHighSeverity ? chalk3.red(" [BREAKING]") : chalk3.yellow(" [CHANGED]");
1222
- log(chalk3.cyan(` ${className}`) + label);
1223
- const removed = changes.filter((c) => c.changeType === "removed");
1224
- for (const mc of removed) {
1225
- const suggestion = mc.suggestion ? chalk3.gray(` → ${mc.suggestion}`) : "";
1226
- log(chalk3.red(` ✖ ${mc.memberName}()`) + suggestion);
1227
- }
1228
- const changed = changes.filter((c) => c.changeType === "signature-changed");
1229
- for (const mc of changed) {
1230
- log(chalk3.yellow(` ~ ${mc.memberName}() signature changed`));
1231
- if (mc.oldSignature && mc.newSignature && mc.oldSignature !== mc.newSignature) {
1232
- log(chalk3.gray(` was: ${mc.oldSignature}`));
1233
- log(chalk3.gray(` now: ${mc.newSignature}`));
1234
- }
1235
- }
1236
- const added = changes.filter((c) => c.changeType === "added");
1237
- if (added.length > 0) {
1238
- const addedNames = added.map((a) => `${a.memberName}()`).join(", ");
1239
- log(chalk3.green(` + ${addedNames}`));
1240
- }
1241
- }
1242
- const nonClassBreaking = (diff.categorizedBreaking ?? []).filter((c) => !classesWithMembers.has(c.id));
1243
- const typeChanges = nonClassBreaking.filter((c) => c.kind === "interface" || c.kind === "type" || c.kind === "enum");
1244
- const functionChanges = nonClassBreaking.filter((c) => c.kind === "function");
1245
- const otherChanges = nonClassBreaking.filter((c) => !["interface", "type", "enum", "function"].includes(c.kind));
1246
- if (functionChanges.length > 0) {
1247
- log("");
1248
- log(chalk3.red(` Function Changes (${functionChanges.length}):`));
1249
- for (const fc of functionChanges.slice(0, 3)) {
1250
- const reason = fc.reason === "removed" ? "removed" : "signature changed";
1251
- log(chalk3.red(` ✖ ${fc.name} (${reason})`));
1252
- }
1253
- if (functionChanges.length > 3) {
1254
- log(chalk3.gray(` ... and ${functionChanges.length - 3} more`));
1255
- }
1256
- }
1257
- if (typeChanges.length > 0) {
1258
- log("");
1259
- log(chalk3.yellow(` Type/Interface Changes (${typeChanges.length}):`));
1260
- const typeNames = typeChanges.slice(0, 5).map((t) => t.name);
1261
- log(chalk3.yellow(` ~ ${typeNames.join(", ")}${typeChanges.length > 5 ? ", ..." : ""}`));
1262
- }
1263
- if (otherChanges.length > 0) {
1264
- log("");
1265
- log(chalk3.gray(` Other Changes (${otherChanges.length}):`));
1266
- const otherNames = otherChanges.slice(0, 3).map((o) => o.name);
1267
- log(chalk3.gray(` ${otherNames.join(", ")}${otherChanges.length > 3 ? ", ..." : ""}`));
1268
- }
1269
- if (diff.nonBreaking.length > 0) {
1270
- const undocCount = diff.newUndocumented.length;
1271
- const undocSuffix = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
1272
- log("");
1273
- log(chalk3.green(` New Exports (${diff.nonBreaking.length})`) + undocSuffix);
1274
- const exportNames = diff.nonBreaking.slice(0, 3);
1275
- log(chalk3.green(` + ${exportNames.join(", ")}${diff.nonBreaking.length > 3 ? ", ..." : ""}`));
1445
+ const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1446
+ log(` Coverage: ${diff.oldCoverage}% ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1447
+ const breakingCount = diff.breaking.length;
1448
+ const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1449
+ if (breakingCount > 0) {
1450
+ const severityNote = highSeverity > 0 ? chalk3.red(` (${highSeverity} high severity)`) : "";
1451
+ log(` Breaking: ${chalk3.red(breakingCount)} changes${severityNote}`);
1452
+ } else {
1453
+ log(` Breaking: ${chalk3.green("0")} changes`);
1454
+ }
1455
+ const newCount = diff.nonBreaking.length;
1456
+ const undocCount = diff.newUndocumented.length;
1457
+ if (newCount > 0) {
1458
+ const undocNote = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
1459
+ log(` New: ${chalk3.green(newCount)} exports${undocNote}`);
1276
1460
  }
1277
1461
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1278
- log("");
1279
1462
  const parts = [];
1280
- if (diff.driftIntroduced > 0) {
1281
- parts.push(chalk3.red(`+${diff.driftIntroduced} drift`));
1282
- }
1283
- if (diff.driftResolved > 0) {
1284
- parts.push(chalk3.green(`-${diff.driftResolved} resolved`));
1285
- }
1286
- log(` Drift: ${parts.join(", ")}`);
1463
+ if (diff.driftIntroduced > 0)
1464
+ parts.push(chalk3.red(`+${diff.driftIntroduced}`));
1465
+ if (diff.driftResolved > 0)
1466
+ parts.push(chalk3.green(`-${diff.driftResolved}`));
1467
+ log(` Drift: ${parts.join(", ")}`);
1287
1468
  }
1469
+ log("");
1288
1470
  }
1289
- function printDocsRequiringUpdates(diff, log) {
1290
- if (!diff.docsImpact)
1471
+ async function printAISummary(diff, log) {
1472
+ if (!isAIDocsAnalysisAvailable()) {
1473
+ log(chalk3.yellow(`
1474
+ ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1291
1475
  return;
1292
- const { impactedFiles, missingDocs, stats } = diff.docsImpact;
1293
- log("");
1294
- log(chalk3.bold("Docs Requiring Updates"));
1295
- log(chalk3.gray(` Scanned ${stats.filesScanned} files, ${stats.codeBlocksFound} code blocks`));
1296
- if (impactedFiles.length === 0 && missingDocs.length === 0) {
1297
- log(chalk3.green(" ✓ No updates needed"));
1476
+ }
1477
+ if (!diff.docsImpact)
1298
1478
  return;
1479
+ log(chalk3.gray(`
1480
+ Generating AI summary...`));
1481
+ const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1482
+ file: f.file,
1483
+ exportName: r.exportName,
1484
+ changeType: r.changeType,
1485
+ context: r.context
1486
+ })));
1487
+ const summary = await generateImpactSummary(impacts);
1488
+ if (summary) {
1489
+ log("");
1490
+ log(chalk3.bold("AI Summary"));
1491
+ log(chalk3.cyan(` ${summary}`));
1299
1492
  }
1300
- const sortedFiles = [...impactedFiles].sort((a, b) => b.references.length - a.references.length);
1301
- const actionableFiles = [];
1302
- const instantiationOnlyFiles = [];
1303
- for (const file of sortedFiles) {
1304
- const hasActionableRefs = file.references.some((r) => r.memberName && !r.isInstantiation || !r.memberName && !r.isInstantiation);
1305
- if (hasActionableRefs) {
1306
- actionableFiles.push(file);
1307
- } else {
1308
- instantiationOnlyFiles.push(file);
1309
- }
1493
+ }
1494
+ function validateDiff(diff, headSpec, options) {
1495
+ const { minCoverage, maxDrift, checks } = options;
1496
+ const failures = [];
1497
+ const headExportsWithDrift = new Set((headSpec.exports ?? []).filter((e) => e.docs?.drift?.length).map((e) => e.name)).size;
1498
+ const headDriftScore = headSpec.exports?.length ? Math.round(headExportsWithDrift / headSpec.exports.length * 100) : 0;
1499
+ if (minCoverage !== undefined && diff.newCoverage < minCoverage) {
1500
+ failures.push(`Coverage ${diff.newCoverage}% below minimum ${minCoverage}%`);
1310
1501
  }
1311
- for (const file of actionableFiles.slice(0, 6)) {
1312
- const filename = path4.basename(file.file);
1313
- const issueCount = file.references.length;
1314
- log("");
1315
- log(chalk3.yellow(` ${filename}`) + chalk3.gray(` (${issueCount} issue${issueCount > 1 ? "s" : ""})`));
1316
- const actionableRefs = file.references.filter((r) => !r.isInstantiation);
1317
- for (const ref of actionableRefs.slice(0, 4)) {
1318
- if (ref.memberName) {
1319
- const action = ref.changeType === "method-removed" ? "→" : "~";
1320
- const hint = ref.replacementSuggestion ?? (ref.changeType === "method-changed" ? "signature changed" : "removed");
1321
- log(chalk3.gray(` L${ref.line}: ${ref.memberName}() ${action} ${hint}`));
1322
- } else {
1323
- const action = ref.changeType === "removed" ? "→" : "~";
1324
- const hint = ref.changeType === "removed" ? "removed" : ref.changeType === "signature-changed" ? "signature changed" : "changed";
1325
- log(chalk3.gray(` L${ref.line}: ${ref.exportName} ${action} ${hint}`));
1326
- }
1327
- }
1328
- if (actionableRefs.length > 4) {
1329
- log(chalk3.gray(` ... and ${actionableRefs.length - 4} more`));
1330
- }
1502
+ if (maxDrift !== undefined && headDriftScore > maxDrift) {
1503
+ failures.push(`Drift ${headDriftScore}% exceeds maximum ${maxDrift}%`);
1331
1504
  }
1332
- if (actionableFiles.length > 6) {
1333
- log(chalk3.gray(` ... and ${actionableFiles.length - 6} more files with method changes`));
1505
+ if (checks.has("regression") && diff.coverageDelta < 0) {
1506
+ failures.push(`Coverage regressed by ${Math.abs(diff.coverageDelta)}%`);
1334
1507
  }
1335
- if (instantiationOnlyFiles.length > 0) {
1336
- log("");
1337
- const fileNames = instantiationOnlyFiles.slice(0, 4).map((f) => path4.basename(f.file));
1338
- const suffix = instantiationOnlyFiles.length > 4 ? ", ..." : "";
1339
- log(chalk3.gray(` ${instantiationOnlyFiles.length} file(s) with class instantiation to review:`));
1340
- log(chalk3.gray(` ${fileNames.join(", ")}${suffix}`));
1508
+ if (checks.has("breaking") && diff.breaking.length > 0) {
1509
+ failures.push(`${diff.breaking.length} breaking change(s)`);
1341
1510
  }
1342
- const { allUndocumented } = diff.docsImpact;
1343
- if (missingDocs.length > 0) {
1344
- log("");
1345
- log(chalk3.yellow(` New exports missing docs (${missingDocs.length}):`));
1346
- const names = missingDocs.slice(0, 4);
1347
- log(chalk3.gray(` ${names.join(", ")}${missingDocs.length > 4 ? ", ..." : ""}`));
1511
+ if (checks.has("drift") && diff.driftIntroduced > 0) {
1512
+ failures.push(`${diff.driftIntroduced} new drift issue(s)`);
1348
1513
  }
1349
- if (allUndocumented && allUndocumented.length > 0) {
1350
- const existingUndocumented = allUndocumented.filter((name) => !missingDocs.includes(name));
1351
- log("");
1352
- log(chalk3.gray(` Total undocumented exports: ${allUndocumented.length}/${stats.totalExports} (${Math.round((1 - allUndocumented.length / stats.totalExports) * 100)}% documented)`));
1353
- if (existingUndocumented.length > 0 && existingUndocumented.length <= 10) {
1354
- log(chalk3.gray(` ${existingUndocumented.slice(0, 6).join(", ")}${existingUndocumented.length > 6 ? ", ..." : ""}`));
1355
- }
1514
+ if (checks.has("undocumented") && diff.newUndocumented.length > 0) {
1515
+ failures.push(`${diff.newUndocumented.length} undocumented export(s)`);
1356
1516
  }
1357
- }
1358
- function groupMemberChangesByClass(memberChanges) {
1359
- const byClass = new Map;
1360
- for (const mc of memberChanges) {
1361
- const list = byClass.get(mc.className) ?? [];
1362
- list.push(mc);
1363
- byClass.set(mc.className, list);
1517
+ if (checks.has("docs-impact") && hasDocsImpact(diff)) {
1518
+ const summary = getDocsImpactSummary(diff);
1519
+ failures.push(`${summary.totalIssues} docs issue(s)`);
1364
1520
  }
1365
- return byClass;
1521
+ return failures;
1366
1522
  }
1367
1523
  function printGitHubAnnotations(diff, log) {
1368
1524
  if (diff.coverageDelta !== 0) {
@@ -1410,159 +1566,6 @@ function printGitHubAnnotations(diff, log) {
1410
1566
  log(`::warning title=Drift Detected::${diff.driftIntroduced} new drift issue(s) introduced`);
1411
1567
  }
1412
1568
  }
1413
- function generateHTMLReport(diff) {
1414
- const coverageClass = diff.coverageDelta > 0 ? "positive" : diff.coverageDelta < 0 ? "negative" : "neutral";
1415
- const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1416
- let html = `<!DOCTYPE html>
1417
- <html lang="en">
1418
- <head>
1419
- <meta charset="UTF-8">
1420
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1421
- <title>DocCov Diff Report</title>
1422
- <style>
1423
- * { box-sizing: border-box; margin: 0; padding: 0; }
1424
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; line-height: 1.5; }
1425
- .container { max-width: 900px; margin: 0 auto; }
1426
- h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #f0f6fc; }
1427
- h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
1428
- .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
1429
- .metric { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
1430
- .metric-label { color: #8b949e; }
1431
- .metric-value { font-weight: 600; }
1432
- .positive { color: #3fb950; }
1433
- .negative { color: #f85149; }
1434
- .neutral { color: #8b949e; }
1435
- .warning { color: #d29922; }
1436
- .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
1437
- .badge-breaking { background: #f8514933; color: #f85149; }
1438
- .badge-changed { background: #d2992233; color: #d29922; }
1439
- .badge-added { background: #3fb95033; color: #3fb950; }
1440
- .file-item { padding: 0.5rem; margin: 0.25rem 0; background: #0d1117; border-radius: 4px; }
1441
- .file-name { font-family: monospace; font-size: 0.9rem; }
1442
- .ref-list { margin-top: 0.5rem; padding-left: 1rem; font-size: 0.85rem; color: #8b949e; }
1443
- .ref-item { margin: 0.25rem 0; }
1444
- ul { list-style: none; }
1445
- li { padding: 0.25rem 0; }
1446
- code { font-family: monospace; background: #0d1117; padding: 0.125rem 0.375rem; border-radius: 3px; font-size: 0.9rem; }
1447
- </style>
1448
- </head>
1449
- <body>
1450
- <div class="container">
1451
- <h1>\uD83D\uDCCA DocCov Diff Report</h1>
1452
-
1453
- <div class="card">
1454
- <div class="metric">
1455
- <span class="metric-label">Coverage</span>
1456
- <span class="metric-value ${coverageClass}">${diff.oldCoverage}% → ${diff.newCoverage}% (${coverageSign}${diff.coverageDelta}%)</span>
1457
- </div>
1458
- <div class="metric">
1459
- <span class="metric-label">Breaking Changes</span>
1460
- <span class="metric-value ${diff.breaking.length > 0 ? "negative" : "neutral"}">${diff.breaking.length}</span>
1461
- </div>
1462
- <div class="metric">
1463
- <span class="metric-label">New Exports</span>
1464
- <span class="metric-value positive">${diff.nonBreaking.length}</span>
1465
- </div>
1466
- <div class="metric">
1467
- <span class="metric-label">Undocumented</span>
1468
- <span class="metric-value ${diff.newUndocumented.length > 0 ? "warning" : "neutral"}">${diff.newUndocumented.length}</span>
1469
- </div>
1470
- </div>`;
1471
- if (diff.breaking.length > 0) {
1472
- html += `
1473
- <h2>Breaking Changes</h2>
1474
- <div class="card">
1475
- <ul>`;
1476
- for (const item of diff.categorizedBreaking ?? []) {
1477
- const badgeClass = item.severity === "high" ? "badge-breaking" : "badge-changed";
1478
- html += `
1479
- <li><code>${item.name}</code> <span class="badge ${badgeClass}">${item.reason}</span></li>`;
1480
- }
1481
- html += `
1482
- </ul>
1483
- </div>`;
1484
- }
1485
- if (diff.memberChanges && diff.memberChanges.length > 0) {
1486
- html += `
1487
- <h2>Member Changes</h2>
1488
- <div class="card">
1489
- <ul>`;
1490
- for (const mc of diff.memberChanges) {
1491
- const badgeClass = mc.changeType === "removed" ? "badge-breaking" : mc.changeType === "added" ? "badge-added" : "badge-changed";
1492
- const suggestion = mc.suggestion ? ` → ${mc.suggestion}` : "";
1493
- html += `
1494
- <li><code>${mc.className}.${mc.memberName}()</code> <span class="badge ${badgeClass}">${mc.changeType}</span>${suggestion}</li>`;
1495
- }
1496
- html += `
1497
- </ul>
1498
- </div>`;
1499
- }
1500
- if (diff.docsImpact && diff.docsImpact.impactedFiles.length > 0) {
1501
- html += `
1502
- <h2>Documentation Impact</h2>
1503
- <div class="card">`;
1504
- for (const file of diff.docsImpact.impactedFiles.slice(0, 10)) {
1505
- const filename = path4.basename(file.file);
1506
- html += `
1507
- <div class="file-item">
1508
- <div class="file-name">\uD83D\uDCC4 ${filename} <span class="neutral">(${file.references.length} issue${file.references.length > 1 ? "s" : ""})</span></div>
1509
- <div class="ref-list">`;
1510
- for (const ref of file.references.slice(0, 5)) {
1511
- const name = ref.memberName ? `${ref.memberName}()` : ref.exportName;
1512
- const change = ref.changeType === "removed" || ref.changeType === "method-removed" ? "removed" : "signature changed";
1513
- html += `
1514
- <div class="ref-item">Line ${ref.line}: <code>${name}</code> ${change}</div>`;
1515
- }
1516
- if (file.references.length > 5) {
1517
- html += `
1518
- <div class="ref-item neutral">... and ${file.references.length - 5} more</div>`;
1519
- }
1520
- html += `
1521
- </div>
1522
- </div>`;
1523
- }
1524
- html += `
1525
- </div>`;
1526
- }
1527
- const hasNewUndocumented = diff.newUndocumented.length > 0;
1528
- const hasAllUndocumented = diff.docsImpact?.allUndocumented && diff.docsImpact.allUndocumented.length > 0;
1529
- if (hasNewUndocumented || hasAllUndocumented) {
1530
- html += `
1531
- <h2>Missing Documentation</h2>
1532
- <div class="card">`;
1533
- if (hasNewUndocumented) {
1534
- html += `
1535
- <p class="warning">New exports missing docs (${diff.newUndocumented.length}):</p>
1536
- <ul>`;
1537
- for (const name of diff.newUndocumented.slice(0, 10)) {
1538
- html += `
1539
- <li><code>${name}</code></li>`;
1540
- }
1541
- if (diff.newUndocumented.length > 10) {
1542
- html += `
1543
- <li class="neutral">... and ${diff.newUndocumented.length - 10} more</li>`;
1544
- }
1545
- html += `
1546
- </ul>`;
1547
- }
1548
- if (diff.docsImpact?.stats) {
1549
- const { stats, allUndocumented } = diff.docsImpact;
1550
- const docPercent = Math.round((1 - (allUndocumented?.length ?? 0) / stats.totalExports) * 100);
1551
- html += `
1552
- <div class="metric" style="margin-top: 1rem; border-top: 1px solid #30363d; padding-top: 1rem;">
1553
- <span class="metric-label">Total Documentation Coverage</span>
1554
- <span class="metric-value ${docPercent >= 80 ? "positive" : docPercent >= 50 ? "warning" : "negative"}">${stats.documentedExports}/${stats.totalExports} (${docPercent}%)</span>
1555
- </div>`;
1556
- }
1557
- html += `
1558
- </div>`;
1559
- }
1560
- html += `
1561
- </div>
1562
- </body>
1563
- </html>`;
1564
- return html;
1565
- }
1566
1569
 
1567
1570
  // src/commands/info.ts
1568
1571
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
@@ -1603,7 +1606,7 @@ function registerInfoCommand(program) {
1603
1606
 
1604
1607
  // src/commands/init.ts
1605
1608
  import * as fs4 from "node:fs";
1606
- import * as path5 from "node:path";
1609
+ import * as path6 from "node:path";
1607
1610
  import chalk5 from "chalk";
1608
1611
  var defaultDependencies3 = {
1609
1612
  fileExists: fs4.existsSync,
@@ -1618,7 +1621,7 @@ function registerInitCommand(program, dependencies = {}) {
1618
1621
  ...dependencies
1619
1622
  };
1620
1623
  program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
1621
- const cwd = path5.resolve(options.cwd);
1624
+ const cwd = path6.resolve(options.cwd);
1622
1625
  const formatOption = String(options.format ?? "auto").toLowerCase();
1623
1626
  if (!isValidFormat(formatOption)) {
1624
1627
  error(chalk5.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
@@ -1627,7 +1630,7 @@ function registerInitCommand(program, dependencies = {}) {
1627
1630
  }
1628
1631
  const existing = findExistingConfig(cwd, fileExists2);
1629
1632
  if (existing) {
1630
- error(chalk5.red(`A DocCov config already exists at ${path5.relative(cwd, existing) || "./doccov.config.*"}.`));
1633
+ error(chalk5.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
1631
1634
  process.exitCode = 1;
1632
1635
  return;
1633
1636
  }
@@ -1637,7 +1640,7 @@ function registerInitCommand(program, dependencies = {}) {
1637
1640
  log(chalk5.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1638
1641
  }
1639
1642
  const fileName = `doccov.config.${targetFormat}`;
1640
- const outputPath = path5.join(cwd, fileName);
1643
+ const outputPath = path6.join(cwd, fileName);
1641
1644
  if (fileExists2(outputPath)) {
1642
1645
  error(chalk5.red(`Cannot create ${fileName}; file already exists.`));
1643
1646
  process.exitCode = 1;
@@ -1645,18 +1648,18 @@ function registerInitCommand(program, dependencies = {}) {
1645
1648
  }
1646
1649
  const template = buildTemplate(targetFormat);
1647
1650
  writeFileSync3(outputPath, template, { encoding: "utf8" });
1648
- log(chalk5.green(`✓ Created ${path5.relative(process.cwd(), outputPath)}`));
1651
+ log(chalk5.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
1649
1652
  });
1650
1653
  }
1651
1654
  var isValidFormat = (value) => {
1652
1655
  return value === "auto" || value === "mjs" || value === "js" || value === "cjs";
1653
1656
  };
1654
1657
  var findExistingConfig = (cwd, fileExists2) => {
1655
- let current = path5.resolve(cwd);
1656
- const { root } = path5.parse(current);
1658
+ let current = path6.resolve(cwd);
1659
+ const { root } = path6.parse(current);
1657
1660
  while (true) {
1658
1661
  for (const candidate of DOCCOV_CONFIG_FILENAMES) {
1659
- const candidatePath = path5.join(current, candidate);
1662
+ const candidatePath = path6.join(current, candidate);
1660
1663
  if (fileExists2(candidatePath)) {
1661
1664
  return candidatePath;
1662
1665
  }
@@ -1664,7 +1667,7 @@ var findExistingConfig = (cwd, fileExists2) => {
1664
1667
  if (current === root) {
1665
1668
  break;
1666
1669
  }
1667
- current = path5.dirname(current);
1670
+ current = path6.dirname(current);
1668
1671
  }
1669
1672
  return null;
1670
1673
  };
@@ -1686,17 +1689,17 @@ var detectPackageType = (cwd, fileExists2, readFileSync3) => {
1686
1689
  return;
1687
1690
  };
1688
1691
  var findNearestPackageJson = (cwd, fileExists2) => {
1689
- let current = path5.resolve(cwd);
1690
- const { root } = path5.parse(current);
1692
+ let current = path6.resolve(cwd);
1693
+ const { root } = path6.parse(current);
1691
1694
  while (true) {
1692
- const candidate = path5.join(current, "package.json");
1695
+ const candidate = path6.join(current, "package.json");
1693
1696
  if (fileExists2(candidate)) {
1694
1697
  return candidate;
1695
1698
  }
1696
1699
  if (current === root) {
1697
1700
  break;
1698
1701
  }
1699
- current = path5.dirname(current);
1702
+ current = path6.dirname(current);
1700
1703
  }
1701
1704
  return null;
1702
1705
  };
@@ -1754,513 +1757,21 @@ var buildTemplate = (format) => {
1754
1757
  `);
1755
1758
  };
1756
1759
 
1757
- // src/commands/scan.ts
1758
- import * as fs6 from "node:fs";
1759
- import * as fsPromises from "node:fs/promises";
1760
- import * as os from "node:os";
1761
- import * as path7 from "node:path";
1762
- import {
1763
- buildCloneUrl,
1764
- buildDisplayUrl,
1765
- DocCov as DocCov3,
1766
- detectBuildInfo,
1767
- detectEntryPoint,
1768
- detectMonorepo,
1769
- detectPackageManager,
1770
- extractSpecSummary,
1771
- findPackageByName,
1772
- formatPackageList,
1773
- getInstallCommand,
1774
- NodeFileSystem as NodeFileSystem3,
1775
- parseGitHubUrl
1776
- } from "@doccov/sdk";
1777
- import {
1778
- DRIFT_CATEGORIES as DRIFT_CATEGORIES3,
1779
- DRIFT_CATEGORY_LABELS as DRIFT_CATEGORY_LABELS2
1780
- } from "@openpkg-ts/spec";
1781
- import chalk6 from "chalk";
1782
- import { simpleGit } from "simple-git";
1783
-
1784
- // src/utils/llm-build-plan.ts
1760
+ // src/commands/spec.ts
1785
1761
  import * as fs5 from "node:fs";
1786
- import * as path6 from "node:path";
1787
- import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
1788
- import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
1789
- import { generateObject as generateObject3 } from "ai";
1790
- import { z as z4 } from "zod";
1791
- var BuildPlanSchema = z4.object({
1792
- installCommand: z4.string().optional().describe("Additional install command if needed"),
1793
- buildCommands: z4.array(z4.string()).describe('Build steps to run, e.g. ["npm run build:wasm"]'),
1794
- entryPoint: z4.string().describe("Path to TS/TSX entry file after build"),
1795
- notes: z4.string().optional().describe("Caveats or warnings")
1796
- });
1797
- var CONTEXT_FILES = [
1798
- "package.json",
1799
- "README.md",
1800
- "README",
1801
- "tsconfig.json",
1802
- "Cargo.toml",
1803
- ".nvmrc",
1804
- ".node-version",
1805
- "pnpm-workspace.yaml",
1806
- "lerna.json",
1807
- "wasm-pack.json"
1808
- ];
1809
- var MAX_FILE_CHARS = 2000;
1810
- function getModel3() {
1811
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1812
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1813
- const anthropic = createAnthropic3();
1814
- return anthropic("claude-sonnet-4-20250514");
1815
- }
1816
- const openai = createOpenAI3();
1817
- return openai("gpt-4o-mini");
1818
- }
1819
- async function gatherContextFiles(repoDir) {
1820
- const sections = [];
1821
- for (const fileName of CONTEXT_FILES) {
1822
- const filePath = path6.join(repoDir, fileName);
1823
- if (fs5.existsSync(filePath)) {
1824
- try {
1825
- let content = fs5.readFileSync(filePath, "utf-8");
1826
- if (content.length > MAX_FILE_CHARS) {
1827
- content = `${content.slice(0, MAX_FILE_CHARS)}
1828
- ... (truncated)`;
1829
- }
1830
- sections.push(`--- ${fileName} ---
1831
- ${content}`);
1832
- } catch {}
1833
- }
1834
- }
1835
- return sections.join(`
1836
-
1837
- `);
1838
- }
1839
- var BUILD_PLAN_PROMPT = (context) => `Analyze this project to determine how to build it for TypeScript API analysis.
1840
-
1841
- The standard entry detection failed. This might be a WASM project, unusual monorepo, or require a build step before the TypeScript entry point exists.
1842
-
1843
- <files>
1844
- ${context}
1845
- </files>
1846
-
1847
- Return:
1848
- - buildCommands: Commands to run in order (e.g., ["npm run build:wasm", "npm run build"]). Empty array if no build needed.
1849
- - entryPoint: Path to the TypeScript entry file AFTER build completes (e.g., "src/index.ts" or "pkg/index.d.ts")
1850
- - installCommand: Additional install command if needed beyond what was already run
1851
- - notes: Any caveats (e.g., "requires Rust/wasm-pack installed")
1852
-
1853
- Important:
1854
- - Look for build scripts in package.json that might generate TypeScript bindings
1855
- - Check README for build instructions
1856
- - For WASM projects, look for wasm-pack or similar tooling
1857
- - The entry point should be a .ts, .tsx, or .d.ts file`;
1858
- async function generateBuildPlan(repoDir) {
1859
- const hasApiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
1860
- if (!hasApiKey) {
1861
- return null;
1862
- }
1863
- const context = await gatherContextFiles(repoDir);
1864
- if (!context.trim()) {
1865
- return null;
1866
- }
1867
- const model = getModel3();
1868
- const { object } = await generateObject3({
1869
- model,
1870
- schema: BuildPlanSchema,
1871
- prompt: BUILD_PLAN_PROMPT(context)
1872
- });
1873
- return object;
1874
- }
1875
-
1876
- // src/commands/scan.ts
1877
- var defaultDependencies4 = {
1878
- createDocCov: (options) => new DocCov3(options),
1879
- log: console.log,
1880
- error: console.error
1881
- };
1882
- function registerScanCommand(program, dependencies = {}) {
1883
- const { createDocCov, log, error } = {
1884
- ...defaultDependencies4,
1885
- ...dependencies
1886
- };
1887
- program.command("scan <url>").description("Analyze docs coverage for any public GitHub repository").option("--ref <branch>", "Branch or tag to analyze").option("--package <name>", "Target package in monorepo").option("--output <format>", "Output format: text or json", "text").option("--no-cleanup", "Keep cloned repo (for debugging)").option("--skip-install", "Skip dependency installation (faster, but may limit type resolution)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--save-spec <path>", "Save full OpenPkg spec to file").action(async (url, options) => {
1888
- let tempDir;
1889
- try {
1890
- const parsed = parseGitHubUrl(url, options.ref ?? "main");
1891
- const cloneUrl = buildCloneUrl(parsed);
1892
- const displayUrl = buildDisplayUrl(parsed);
1893
- log("");
1894
- log(chalk6.bold(`Scanning ${displayUrl}`));
1895
- log(chalk6.gray(`Branch/tag: ${parsed.ref}`));
1896
- log("");
1897
- tempDir = path7.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
1898
- fs6.mkdirSync(tempDir, { recursive: true });
1899
- process.stdout.write(chalk6.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
1900
- `));
1901
- try {
1902
- const git = simpleGit({
1903
- timeout: {
1904
- block: 30000
1905
- }
1906
- });
1907
- const originalEnv = { ...process.env };
1908
- process.env.GIT_TERMINAL_PROMPT = "0";
1909
- process.env.GIT_ASKPASS = "echo";
1910
- try {
1911
- await git.clone(cloneUrl, tempDir, [
1912
- "--depth",
1913
- "1",
1914
- "--branch",
1915
- parsed.ref,
1916
- "--single-branch"
1917
- ]);
1918
- } finally {
1919
- process.env = originalEnv;
1920
- }
1921
- process.stdout.write(chalk6.green(`✓ Cloned ${parsed.owner}/${parsed.repo}
1922
- `));
1923
- } catch (cloneError) {
1924
- process.stdout.write(chalk6.red(`✗ Failed to clone repository
1925
- `));
1926
- const message = cloneError instanceof Error ? cloneError.message : String(cloneError);
1927
- if (message.includes("Authentication failed") || message.includes("could not read Username") || message.includes("terminal prompts disabled") || message.includes("Invalid username or password") || message.includes("Permission denied")) {
1928
- throw new Error(`Authentication required: This repository appears to be private. ` + `Public repositories only are currently supported.
1929
- ` + `Repository: ${displayUrl}`);
1930
- }
1931
- if (message.includes("not found") || message.includes("404")) {
1932
- throw new Error(`Repository not accessible or does not exist: ${displayUrl}
1933
- ` + `Note: Private repositories are not currently supported.`);
1934
- }
1935
- if (message.includes("Could not find remote branch")) {
1936
- throw new Error(`Branch or tag not found: ${parsed.ref}`);
1937
- }
1938
- throw new Error(`Clone failed: ${message}`);
1939
- }
1940
- const fileSystem = new NodeFileSystem3(tempDir);
1941
- if (options.skipInstall) {
1942
- log(chalk6.gray("Skipping dependency installation (--skip-install)"));
1943
- } else {
1944
- process.stdout.write(chalk6.cyan(`> Installing dependencies...
1945
- `));
1946
- const installErrors = [];
1947
- try {
1948
- const { execSync } = await import("node:child_process");
1949
- const pmInfo = await detectPackageManager(fileSystem);
1950
- const installCmd = getInstallCommand(pmInfo);
1951
- const cmdString = installCmd.join(" ");
1952
- let installed = false;
1953
- if (pmInfo.lockfile) {
1954
- try {
1955
- execSync(cmdString, {
1956
- cwd: tempDir,
1957
- stdio: "pipe",
1958
- timeout: 180000
1959
- });
1960
- installed = true;
1961
- } catch (cmdError) {
1962
- const stderr = cmdError?.stderr?.toString() ?? "";
1963
- const msg = cmdError instanceof Error ? cmdError.message : String(cmdError);
1964
- installErrors.push(`[${cmdString}] ${stderr.slice(0, 150) || msg.slice(0, 150)}`);
1965
- }
1966
- }
1967
- if (!installed) {
1968
- try {
1969
- execSync("bun install", {
1970
- cwd: tempDir,
1971
- stdio: "pipe",
1972
- timeout: 120000
1973
- });
1974
- installed = true;
1975
- } catch (bunError) {
1976
- const stderr = bunError?.stderr?.toString() ?? "";
1977
- const msg = bunError instanceof Error ? bunError.message : String(bunError);
1978
- installErrors.push(`[bun install] ${stderr.slice(0, 150) || msg.slice(0, 150)}`);
1979
- try {
1980
- execSync("npm install --legacy-peer-deps --ignore-scripts", {
1981
- cwd: tempDir,
1982
- stdio: "pipe",
1983
- timeout: 180000
1984
- });
1985
- installed = true;
1986
- } catch (npmError) {
1987
- const npmStderr = npmError?.stderr?.toString() ?? "";
1988
- const npmMsg = npmError instanceof Error ? npmError.message : String(npmError);
1989
- installErrors.push(`[npm install] ${npmStderr.slice(0, 150) || npmMsg.slice(0, 150)}`);
1990
- }
1991
- }
1992
- }
1993
- if (installed) {
1994
- process.stdout.write(chalk6.green(`✓ Dependencies installed
1995
- `));
1996
- } else {
1997
- process.stdout.write(chalk6.yellow(`⚠ Could not install dependencies (analysis may be limited)
1998
- `));
1999
- for (const err of installErrors) {
2000
- log(chalk6.gray(` ${err}`));
2001
- }
2002
- }
2003
- } catch (outerError) {
2004
- const msg = outerError instanceof Error ? outerError.message : String(outerError);
2005
- process.stdout.write(chalk6.yellow(`⚠ Could not install dependencies: ${msg.slice(0, 100)}
2006
- `));
2007
- for (const err of installErrors) {
2008
- log(chalk6.gray(` ${err}`));
2009
- }
2010
- }
2011
- }
2012
- let targetDir = tempDir;
2013
- let packageName;
2014
- const mono = await detectMonorepo(fileSystem);
2015
- if (mono.isMonorepo) {
2016
- if (!options.package) {
2017
- error("");
2018
- error(chalk6.red(`Monorepo detected with ${mono.packages.length} packages. Specify target with --package:`));
2019
- error("");
2020
- error(formatPackageList(mono.packages));
2021
- error("");
2022
- throw new Error("Monorepo requires --package flag");
2023
- }
2024
- const pkg = findPackageByName(mono.packages, options.package);
2025
- if (!pkg) {
2026
- error("");
2027
- error(chalk6.red(`Package "${options.package}" not found. Available packages:`));
2028
- error("");
2029
- error(formatPackageList(mono.packages));
2030
- error("");
2031
- throw new Error(`Package not found: ${options.package}`);
2032
- }
2033
- targetDir = path7.join(tempDir, pkg.path);
2034
- packageName = pkg.name;
2035
- log(chalk6.gray(`Analyzing package: ${packageName}`));
2036
- }
2037
- process.stdout.write(chalk6.cyan(`> Detecting entry point...
2038
- `));
2039
- let entryPath;
2040
- const targetFs = mono.isMonorepo ? new NodeFileSystem3(targetDir) : fileSystem;
2041
- let buildFailed = false;
2042
- const runLlmFallback = async (reason) => {
2043
- process.stdout.write(chalk6.cyan(`> ${reason}, trying LLM fallback...
2044
- `));
2045
- const plan = await generateBuildPlan(targetDir);
2046
- if (!plan) {
2047
- return null;
2048
- }
2049
- if (plan.buildCommands.length > 0) {
2050
- const { execSync } = await import("node:child_process");
2051
- for (const cmd of plan.buildCommands) {
2052
- log(chalk6.gray(` Running: ${cmd}`));
2053
- try {
2054
- execSync(cmd, { cwd: targetDir, stdio: "pipe", timeout: 300000 });
2055
- } catch (buildError) {
2056
- buildFailed = true;
2057
- const msg = buildError instanceof Error ? buildError.message : String(buildError);
2058
- if (msg.includes("rustc") || msg.includes("cargo") || msg.includes("wasm-pack")) {
2059
- log(chalk6.yellow(` ⚠ Build requires Rust toolchain (not available)`));
2060
- } else if (msg.includes("rimraf") || msg.includes("command not found")) {
2061
- log(chalk6.yellow(` ⚠ Build failed: missing dependencies`));
2062
- } else {
2063
- log(chalk6.yellow(` ⚠ Build failed: ${msg.slice(0, 80)}`));
2064
- }
2065
- }
2066
- }
2067
- }
2068
- if (plan.notes) {
2069
- log(chalk6.gray(` Note: ${plan.notes}`));
2070
- }
2071
- return plan.entryPoint;
2072
- };
2073
- try {
2074
- const entry = await detectEntryPoint(targetFs);
2075
- const buildInfo = await detectBuildInfo(targetFs);
2076
- const needsBuildStep = entry.isDeclarationOnly && buildInfo.exoticIndicators.wasm;
2077
- if (needsBuildStep) {
2078
- process.stdout.write(chalk6.cyan(`> Detected .d.ts entry with WASM indicators...
2079
- `));
2080
- const llmEntry = await runLlmFallback("WASM project detected");
2081
- if (llmEntry) {
2082
- entryPath = path7.join(targetDir, llmEntry);
2083
- if (buildFailed) {
2084
- process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
2085
- `));
2086
- log(chalk6.gray(" Coverage may be limited - generated .d.ts files typically lack JSDoc"));
2087
- } else {
2088
- process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback - WASM project)
2089
- `));
2090
- }
2091
- } else {
2092
- entryPath = path7.join(targetDir, entry.path);
2093
- process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2094
- `));
2095
- log(chalk6.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
2096
- }
2097
- } else {
2098
- entryPath = path7.join(targetDir, entry.path);
2099
- process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
2100
- `));
2101
- }
2102
- } catch (entryError) {
2103
- const llmEntry = await runLlmFallback("Heuristics failed");
2104
- if (llmEntry) {
2105
- entryPath = path7.join(targetDir, llmEntry);
2106
- process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
2107
- `));
2108
- } else {
2109
- process.stdout.write(chalk6.red(`✗ Could not detect entry point (set OPENAI_API_KEY for smart fallback)
2110
- `));
2111
- throw entryError;
2112
- }
2113
- }
2114
- process.stdout.write(chalk6.cyan(`> Analyzing documentation coverage...
2115
- `));
2116
- let result;
2117
- try {
2118
- const resolveExternalTypes = !options.skipResolve;
2119
- const doccov = createDocCov({ resolveExternalTypes });
2120
- result = await doccov.analyzeFileWithDiagnostics(entryPath);
2121
- process.stdout.write(chalk6.green(`✓ Analysis complete
2122
- `));
2123
- } catch (analysisError) {
2124
- process.stdout.write(chalk6.red(`✗ Analysis failed
2125
- `));
2126
- throw analysisError;
2127
- }
2128
- const spec = result.spec;
2129
- if (options.saveSpec) {
2130
- const specPath = path7.resolve(process.cwd(), options.saveSpec);
2131
- fs6.writeFileSync(specPath, JSON.stringify(spec, null, 2));
2132
- log(chalk6.green(`✓ Saved spec to ${options.saveSpec}`));
2133
- }
2134
- const summary = extractSpecSummary(spec);
2135
- const scanResult = {
2136
- owner: parsed.owner,
2137
- repo: parsed.repo,
2138
- ref: parsed.ref,
2139
- packageName,
2140
- coverage: summary.coverage,
2141
- exportCount: summary.exportCount,
2142
- typeCount: summary.typeCount,
2143
- driftCount: summary.driftCount,
2144
- undocumented: summary.undocumented,
2145
- drift: summary.drift
2146
- };
2147
- if (options.output === "json") {
2148
- log(JSON.stringify(scanResult, null, 2));
2149
- } else {
2150
- printTextResult(scanResult, log);
2151
- }
2152
- } catch (commandError) {
2153
- error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2154
- process.exitCode = 1;
2155
- } finally {
2156
- if (tempDir && options.cleanup !== false) {
2157
- fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
2158
- } else if (tempDir) {
2159
- log(chalk6.gray(`Repo preserved at: ${tempDir}`));
2160
- }
2161
- }
2162
- });
2163
- }
2164
- function categorizeDriftIssues(drift) {
2165
- const byCategory = {
2166
- structural: [],
2167
- semantic: [],
2168
- example: []
2169
- };
2170
- for (const d of drift) {
2171
- const category = DRIFT_CATEGORIES3[d.type] ?? "semantic";
2172
- byCategory[category].push(d);
2173
- }
2174
- return {
2175
- summary: {
2176
- total: drift.length,
2177
- byCategory: {
2178
- structural: byCategory.structural.length,
2179
- semantic: byCategory.semantic.length,
2180
- example: byCategory.example.length
2181
- }
2182
- },
2183
- byCategory
2184
- };
2185
- }
2186
- function formatDriftSummary(summary) {
2187
- if (summary.total === 0) {
2188
- return "No drift detected";
2189
- }
2190
- const parts = [];
2191
- if (summary.byCategory.structural > 0) {
2192
- parts.push(`${summary.byCategory.structural} structural`);
2193
- }
2194
- if (summary.byCategory.semantic > 0) {
2195
- parts.push(`${summary.byCategory.semantic} semantic`);
2196
- }
2197
- if (summary.byCategory.example > 0) {
2198
- parts.push(`${summary.byCategory.example} example`);
2199
- }
2200
- return `${summary.total} issues (${parts.join(", ")})`;
2201
- }
2202
- function printTextResult(result, log) {
2203
- log("");
2204
- log(chalk6.bold("DocCov Scan Results"));
2205
- log("─".repeat(40));
2206
- const repoName = result.packageName ? `${result.owner}/${result.repo} (${result.packageName})` : `${result.owner}/${result.repo}`;
2207
- log(`Repository: ${chalk6.cyan(repoName)}`);
2208
- log(`Branch: ${chalk6.gray(result.ref)}`);
2209
- log("");
2210
- const coverageColor = result.coverage >= 80 ? chalk6.green : result.coverage >= 50 ? chalk6.yellow : chalk6.red;
2211
- log(chalk6.bold("Coverage"));
2212
- log(` ${coverageColor(`${result.coverage}%`)}`);
2213
- log("");
2214
- log(chalk6.bold("Stats"));
2215
- log(` ${result.exportCount} exports`);
2216
- log(` ${result.typeCount} types`);
2217
- log(` ${result.undocumented.length} undocumented`);
2218
- const categorized = categorizeDriftIssues(result.drift);
2219
- const driftColor = result.driftCount > 0 ? chalk6.yellow : chalk6.green;
2220
- log(` ${driftColor(formatDriftSummary(categorized.summary))}`);
2221
- if (result.undocumented.length > 0) {
2222
- log("");
2223
- log(chalk6.bold("Undocumented Exports"));
2224
- for (const name of result.undocumented.slice(0, 10)) {
2225
- log(chalk6.yellow(` ! ${name}`));
2226
- }
2227
- if (result.undocumented.length > 10) {
2228
- log(chalk6.gray(` ... and ${result.undocumented.length - 10} more`));
2229
- }
2230
- }
2231
- if (result.drift.length > 0) {
2232
- log("");
2233
- log(chalk6.bold("Drift Issues"));
2234
- const categories = ["structural", "semantic", "example"];
2235
- for (const category of categories) {
2236
- const issues = categorized.byCategory[category];
2237
- if (issues.length === 0)
2238
- continue;
2239
- const label = DRIFT_CATEGORY_LABELS2[category];
2240
- log("");
2241
- log(chalk6.dim(` ${label} (${issues.length})`));
2242
- for (const d of issues.slice(0, 3)) {
2243
- log(chalk6.red(` • ${d.export}: ${d.issue}`));
2244
- }
2245
- if (issues.length > 3) {
2246
- log(chalk6.gray(` ... and ${issues.length - 3} more`));
2247
- }
2248
- }
2249
- }
2250
- log("");
2251
- }
1762
+ import * as path7 from "node:path";
1763
+ import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
1764
+ import { normalize, validateSpec } from "@openpkg-ts/spec";
1765
+ // package.json
1766
+ var version = "0.13.0";
2252
1767
 
2253
1768
  // src/commands/spec.ts
2254
- import * as fs7 from "node:fs";
2255
- import * as path8 from "node:path";
2256
- import { DocCov as DocCov4, NodeFileSystem as NodeFileSystem4, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2257
- import { normalize, validateSpec } from "@openpkg-ts/spec";
2258
- import chalk8 from "chalk";
1769
+ import chalk7 from "chalk";
2259
1770
 
2260
1771
  // src/utils/filter-options.ts
2261
1772
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
2262
- import chalk7 from "chalk";
2263
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
1773
+ import chalk6 from "chalk";
1774
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk6.cyan(value)).join(", ")}`;
2264
1775
  var mergeFilterOptions = (config, cliOptions) => {
2265
1776
  const messages = [];
2266
1777
  if (config?.include) {
@@ -2289,9 +1800,9 @@ var mergeFilterOptions = (config, cliOptions) => {
2289
1800
  };
2290
1801
 
2291
1802
  // src/commands/spec.ts
2292
- var defaultDependencies5 = {
2293
- createDocCov: (options) => new DocCov4(options),
2294
- writeFileSync: fs7.writeFileSync,
1803
+ var defaultDependencies4 = {
1804
+ createDocCov: (options) => new DocCov3(options),
1805
+ writeFileSync: fs5.writeFileSync,
2295
1806
  log: console.log,
2296
1807
  error: console.error
2297
1808
  };
@@ -2300,19 +1811,19 @@ function getArrayLength(value) {
2300
1811
  }
2301
1812
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2302
1813
  const location = diagnostic.location;
2303
- const relativePath = location?.file ? path8.relative(baseDir, location.file) || location.file : undefined;
2304
- const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
1814
+ const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
1815
+ const locationText = location && relativePath ? chalk7.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2305
1816
  const locationPrefix = locationText ? `${locationText} ` : "";
2306
1817
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
2307
1818
  }
2308
1819
  function registerSpecCommand(program, dependencies = {}) {
2309
- const { createDocCov, writeFileSync: writeFileSync5, log, error } = {
2310
- ...defaultDependencies5,
1820
+ const { createDocCov, writeFileSync: writeFileSync4, log, error } = {
1821
+ ...defaultDependencies4,
2311
1822
  ...dependencies
2312
1823
  };
2313
- program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").action(async (entry, options) => {
1824
+ program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
2314
1825
  try {
2315
- const fileSystem = new NodeFileSystem4(options.cwd);
1826
+ const fileSystem = new NodeFileSystem3(options.cwd);
2316
1827
  const resolved = await resolveTarget3(fileSystem, {
2317
1828
  cwd: options.cwd,
2318
1829
  package: options.package,
@@ -2320,19 +1831,19 @@ function registerSpecCommand(program, dependencies = {}) {
2320
1831
  });
2321
1832
  const { targetDir, entryFile, packageInfo, entryPointInfo } = resolved;
2322
1833
  if (packageInfo) {
2323
- log(chalk8.gray(`Found package at ${packageInfo.path}`));
1834
+ log(chalk7.gray(`Found package at ${packageInfo.path}`));
2324
1835
  }
2325
1836
  if (!entry) {
2326
- log(chalk8.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
1837
+ log(chalk7.gray(`Auto-detected entry point: ${entryPointInfo.path} (from ${entryPointInfo.source})`));
2327
1838
  }
2328
1839
  let config = null;
2329
1840
  try {
2330
1841
  config = await loadDocCovConfig(targetDir);
2331
1842
  if (config?.filePath) {
2332
- log(chalk8.gray(`Loaded configuration from ${path8.relative(targetDir, config.filePath)}`));
1843
+ log(chalk7.gray(`Loaded configuration from ${path7.relative(targetDir, config.filePath)}`));
2333
1844
  }
2334
1845
  } catch (configError) {
2335
- error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
1846
+ error(chalk7.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2336
1847
  process.exit(1);
2337
1848
  }
2338
1849
  const cliFilters = {
@@ -2341,10 +1852,10 @@ function registerSpecCommand(program, dependencies = {}) {
2341
1852
  };
2342
1853
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
2343
1854
  for (const message of resolvedFilters.messages) {
2344
- log(chalk8.gray(`${message}`));
1855
+ log(chalk7.gray(`${message}`));
2345
1856
  }
2346
1857
  const resolveExternalTypes = !options.skipResolve;
2347
- process.stdout.write(chalk8.cyan(`> Generating OpenPkg spec...
1858
+ process.stdout.write(chalk7.cyan(`> Generating OpenPkg spec...
2348
1859
  `));
2349
1860
  let result;
2350
1861
  try {
@@ -2354,22 +1865,33 @@ function registerSpecCommand(program, dependencies = {}) {
2354
1865
  useCache: options.cache !== false,
2355
1866
  cwd: options.cwd
2356
1867
  });
1868
+ const generationInput = {
1869
+ entryPoint: path7.relative(targetDir, entryFile),
1870
+ entryPointSource: entryPointInfo.source,
1871
+ isDeclarationOnly: entryPointInfo.isDeclarationOnly ?? false,
1872
+ generatorName: "@doccov/cli",
1873
+ generatorVersion: version,
1874
+ packageManager: packageInfo?.packageManager,
1875
+ isMonorepo: resolved.isMonorepo,
1876
+ targetPackage: packageInfo?.name
1877
+ };
2357
1878
  const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
2358
1879
  filters: {
2359
1880
  include: resolvedFilters.include,
2360
1881
  exclude: resolvedFilters.exclude
2361
- }
2362
- } : {};
1882
+ },
1883
+ generationInput
1884
+ } : { generationInput };
2363
1885
  result = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
2364
1886
  if (result.fromCache) {
2365
- process.stdout.write(chalk8.gray(`> Using cached spec
1887
+ process.stdout.write(chalk7.gray(`> Using cached spec
2366
1888
  `));
2367
1889
  } else {
2368
- process.stdout.write(chalk8.green(`> Generated OpenPkg spec
1890
+ process.stdout.write(chalk7.green(`> Generated OpenPkg spec
2369
1891
  `));
2370
1892
  }
2371
1893
  } catch (generationError) {
2372
- process.stdout.write(chalk8.red(`> Failed to generate spec
1894
+ process.stdout.write(chalk7.red(`> Failed to generate spec
2373
1895
  `));
2374
1896
  throw generationError;
2375
1897
  }
@@ -2379,27 +1901,64 @@ function registerSpecCommand(program, dependencies = {}) {
2379
1901
  const normalized = normalize(result.spec);
2380
1902
  const validation = validateSpec(normalized);
2381
1903
  if (!validation.ok) {
2382
- error(chalk8.red("Spec failed schema validation"));
1904
+ error(chalk7.red("Spec failed schema validation"));
2383
1905
  for (const err of validation.errors) {
2384
- error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
1906
+ error(chalk7.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2385
1907
  }
2386
1908
  process.exit(1);
2387
1909
  }
2388
- const outputPath = path8.resolve(process.cwd(), options.output);
2389
- writeFileSync5(outputPath, JSON.stringify(normalized, null, 2));
2390
- log(chalk8.green(`> Wrote ${options.output}`));
2391
- log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2392
- log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
1910
+ const outputPath = path7.resolve(process.cwd(), options.output);
1911
+ writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
1912
+ log(chalk7.green(`> Wrote ${options.output}`));
1913
+ log(chalk7.gray(` ${getArrayLength(normalized.exports)} exports`));
1914
+ log(chalk7.gray(` ${getArrayLength(normalized.types)} types`));
1915
+ if (options.verbose && normalized.generation) {
1916
+ const gen = normalized.generation;
1917
+ log("");
1918
+ log(chalk7.bold("Generation Info"));
1919
+ log(chalk7.gray(` Timestamp: ${gen.timestamp}`));
1920
+ log(chalk7.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
1921
+ log(chalk7.gray(` Entry point: ${gen.analysis.entryPoint}`));
1922
+ log(chalk7.gray(` Detected via: ${gen.analysis.entryPointSource}`));
1923
+ log(chalk7.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
1924
+ log(chalk7.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
1925
+ if (gen.analysis.maxTypeDepth) {
1926
+ log(chalk7.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
1927
+ }
1928
+ log("");
1929
+ log(chalk7.bold("Environment"));
1930
+ log(chalk7.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
1931
+ if (gen.environment.packageManager) {
1932
+ log(chalk7.gray(` Package manager: ${gen.environment.packageManager}`));
1933
+ }
1934
+ if (gen.environment.isMonorepo) {
1935
+ log(chalk7.gray(` Monorepo: yes`));
1936
+ }
1937
+ if (gen.environment.targetPackage) {
1938
+ log(chalk7.gray(` Target package: ${gen.environment.targetPackage}`));
1939
+ }
1940
+ if (gen.issues.length > 0) {
1941
+ log("");
1942
+ log(chalk7.bold("Issues"));
1943
+ for (const issue of gen.issues) {
1944
+ const prefix = issue.severity === "error" ? chalk7.red(">") : issue.severity === "warning" ? chalk7.yellow(">") : chalk7.cyan(">");
1945
+ log(`${prefix} [${issue.code}] ${issue.message}`);
1946
+ if (issue.suggestion) {
1947
+ log(chalk7.gray(` ${issue.suggestion}`));
1948
+ }
1949
+ }
1950
+ }
1951
+ }
2393
1952
  if (options.showDiagnostics && result.diagnostics.length > 0) {
2394
1953
  log("");
2395
- log(chalk8.bold("Diagnostics"));
1954
+ log(chalk7.bold("Diagnostics"));
2396
1955
  for (const diagnostic of result.diagnostics) {
2397
- const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
1956
+ const prefix = diagnostic.severity === "error" ? chalk7.red(">") : diagnostic.severity === "warning" ? chalk7.yellow(">") : chalk7.cyan(">");
2398
1957
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2399
1958
  }
2400
1959
  }
2401
1960
  } catch (commandError) {
2402
- error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1961
+ error(chalk7.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2403
1962
  process.exit(1);
2404
1963
  }
2405
1964
  });
@@ -2407,8 +1966,8 @@ function registerSpecCommand(program, dependencies = {}) {
2407
1966
 
2408
1967
  // src/cli.ts
2409
1968
  var __filename2 = fileURLToPath(import.meta.url);
2410
- var __dirname2 = path9.dirname(__filename2);
2411
- var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
1969
+ var __dirname2 = path8.dirname(__filename2);
1970
+ var packageJson = JSON.parse(readFileSync3(path8.join(__dirname2, "../package.json"), "utf-8"));
2412
1971
  var program = new Command;
2413
1972
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2414
1973
  registerCheckCommand(program);
@@ -2416,7 +1975,6 @@ registerInfoCommand(program);
2416
1975
  registerSpecCommand(program);
2417
1976
  registerDiffCommand(program);
2418
1977
  registerInitCommand(program);
2419
- registerScanCommand(program);
2420
1978
  program.command("*", { hidden: true }).action(() => {
2421
1979
  program.outputHelp();
2422
1980
  });