@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 +559 -1001
- package/dist/config/index.js +0 -19
- package/package.json +3 -3
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
|
|
182
|
-
import * as
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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}%** \`${
|
|
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}% \`${
|
|
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
|
|
533
|
+
function escapeHtml2(s) {
|
|
335
534
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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: ${
|
|
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;">${
|
|
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
|
|
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 ?
|
|
476
|
-
const dir =
|
|
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 =
|
|
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 ?
|
|
830
|
+
const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
|
|
621
831
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
622
|
-
const maxDrift = 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 =
|
|
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 =
|
|
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
|
|
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
|
|
1037
|
-
"regression",
|
|
1038
|
-
"drift",
|
|
1039
|
-
"
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
|
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
|
|
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
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
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
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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 "
|
|
1093
|
-
|
|
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 "
|
|
1096
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1438
|
+
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1192
1439
|
log("");
|
|
1193
|
-
|
|
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
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
parts.
|
|
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
|
|
1290
|
-
if (!
|
|
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
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
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
|
-
|
|
1312
|
-
|
|
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 (
|
|
1333
|
-
|
|
1505
|
+
if (checks.has("regression") && diff.coverageDelta < 0) {
|
|
1506
|
+
failures.push(`Coverage regressed by ${Math.abs(diff.coverageDelta)}%`);
|
|
1334
1507
|
}
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
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 (
|
|
1350
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
1656
|
-
const { root } =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1690
|
-
const { root } =
|
|
1692
|
+
let current = path6.resolve(cwd);
|
|
1693
|
+
const { root } = path6.parse(current);
|
|
1691
1694
|
while (true) {
|
|
1692
|
-
const candidate =
|
|
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 =
|
|
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/
|
|
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
|
|
1787
|
-
import {
|
|
1788
|
-
import {
|
|
1789
|
-
|
|
1790
|
-
|
|
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
|
|
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
|
|
2263
|
-
var formatList = (label, values) => `${label}: ${values.map((value) =>
|
|
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
|
|
2293
|
-
createDocCov: (options) => new
|
|
2294
|
-
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 ?
|
|
2304
|
-
const locationText = location && relativePath ?
|
|
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:
|
|
2310
|
-
...
|
|
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
|
|
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(
|
|
1834
|
+
log(chalk7.gray(`Found package at ${packageInfo.path}`));
|
|
2324
1835
|
}
|
|
2325
1836
|
if (!entry) {
|
|
2326
|
-
log(
|
|
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(
|
|
1843
|
+
log(chalk7.gray(`Loaded configuration from ${path7.relative(targetDir, config.filePath)}`));
|
|
2333
1844
|
}
|
|
2334
1845
|
} catch (configError) {
|
|
2335
|
-
error(
|
|
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(
|
|
1855
|
+
log(chalk7.gray(`${message}`));
|
|
2345
1856
|
}
|
|
2346
1857
|
const resolveExternalTypes = !options.skipResolve;
|
|
2347
|
-
process.stdout.write(
|
|
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(
|
|
1887
|
+
process.stdout.write(chalk7.gray(`> Using cached spec
|
|
2366
1888
|
`));
|
|
2367
1889
|
} else {
|
|
2368
|
-
process.stdout.write(
|
|
1890
|
+
process.stdout.write(chalk7.green(`> Generated OpenPkg spec
|
|
2369
1891
|
`));
|
|
2370
1892
|
}
|
|
2371
1893
|
} catch (generationError) {
|
|
2372
|
-
process.stdout.write(
|
|
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(
|
|
1904
|
+
error(chalk7.red("Spec failed schema validation"));
|
|
2383
1905
|
for (const err of validation.errors) {
|
|
2384
|
-
error(
|
|
1906
|
+
error(chalk7.red(`schema: ${err.instancePath || "/"} ${err.message}`));
|
|
2385
1907
|
}
|
|
2386
1908
|
process.exit(1);
|
|
2387
1909
|
}
|
|
2388
|
-
const outputPath =
|
|
2389
|
-
|
|
2390
|
-
log(
|
|
2391
|
-
log(
|
|
2392
|
-
log(
|
|
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(
|
|
1954
|
+
log(chalk7.bold("Diagnostics"));
|
|
2396
1955
|
for (const diagnostic of result.diagnostics) {
|
|
2397
|
-
const prefix = diagnostic.severity === "error" ?
|
|
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(
|
|
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 =
|
|
2411
|
-
var packageJson = JSON.parse(
|
|
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
|
});
|