@doccov/cli 0.11.0 → 0.12.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 +485 -464
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -179,13 +179,13 @@ ${formatIssues(issues)}`);
|
|
|
179
179
|
var defineConfig = (config) => config;
|
|
180
180
|
// src/cli.ts
|
|
181
181
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
182
|
-
import * as
|
|
182
|
+
import * as path10 from "node:path";
|
|
183
183
|
import { fileURLToPath } from "node:url";
|
|
184
184
|
import { Command } from "commander";
|
|
185
185
|
|
|
186
186
|
// src/commands/check.ts
|
|
187
187
|
import * as fs2 from "node:fs";
|
|
188
|
-
import * as
|
|
188
|
+
import * as path4 from "node:path";
|
|
189
189
|
import {
|
|
190
190
|
applyEdits,
|
|
191
191
|
categorizeDrifts,
|
|
@@ -208,6 +208,223 @@ import {
|
|
|
208
208
|
} from "@openpkg-ts/spec";
|
|
209
209
|
import chalk2 from "chalk";
|
|
210
210
|
|
|
211
|
+
// src/reports/diff-markdown.ts
|
|
212
|
+
import * as path2 from "node:path";
|
|
213
|
+
function bar(pct, width = 10) {
|
|
214
|
+
const filled = Math.round(pct / 100 * width);
|
|
215
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
216
|
+
}
|
|
217
|
+
function formatDelta(delta) {
|
|
218
|
+
if (delta > 0)
|
|
219
|
+
return `+${delta}%`;
|
|
220
|
+
if (delta < 0)
|
|
221
|
+
return `${delta}%`;
|
|
222
|
+
return "0%";
|
|
223
|
+
}
|
|
224
|
+
function renderDiffMarkdown(data, options = {}) {
|
|
225
|
+
const limit = options.limit ?? 10;
|
|
226
|
+
const lines = [];
|
|
227
|
+
lines.push(`# DocCov Diff Report`);
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(`**Comparing:** \`${data.baseName}\` → \`${data.headName}\``);
|
|
230
|
+
lines.push("");
|
|
231
|
+
lines.push("## Coverage Summary");
|
|
232
|
+
lines.push("");
|
|
233
|
+
const deltaIndicator = data.coverageDelta > 0 ? "↑" : data.coverageDelta < 0 ? "↓" : "→";
|
|
234
|
+
lines.push(`| Metric | Base | Head | Delta |`);
|
|
235
|
+
lines.push(`|--------|------|------|-------|`);
|
|
236
|
+
lines.push(`| Coverage | ${data.oldCoverage}% | ${data.newCoverage}% | ${deltaIndicator} ${formatDelta(data.coverageDelta)} |`);
|
|
237
|
+
lines.push(`| Breaking Changes | - | ${data.breaking.length} | - |`);
|
|
238
|
+
lines.push(`| New Exports | - | ${data.nonBreaking.length} | - |`);
|
|
239
|
+
lines.push(`| New Undocumented | - | ${data.newUndocumented.length} | - |`);
|
|
240
|
+
if (data.driftIntroduced > 0 || data.driftResolved > 0) {
|
|
241
|
+
const driftDelta = data.driftIntroduced > 0 ? `+${data.driftIntroduced}` : data.driftResolved > 0 ? `-${data.driftResolved}` : "0";
|
|
242
|
+
lines.push(`| Drift | - | - | ${driftDelta} |`);
|
|
243
|
+
}
|
|
244
|
+
if (data.breaking.length > 0) {
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.push("## Breaking Changes");
|
|
247
|
+
lines.push("");
|
|
248
|
+
const categorized = data.categorizedBreaking ?? [];
|
|
249
|
+
const highSeverity = categorized.filter((c) => c.severity === "high");
|
|
250
|
+
const otherSeverity = categorized.filter((c) => c.severity !== "high");
|
|
251
|
+
if (highSeverity.length > 0) {
|
|
252
|
+
lines.push("### High Severity");
|
|
253
|
+
lines.push("");
|
|
254
|
+
lines.push("| Name | Kind | Reason |");
|
|
255
|
+
lines.push("|------|------|--------|");
|
|
256
|
+
for (const c of highSeverity.slice(0, limit)) {
|
|
257
|
+
lines.push(`| \`${c.name}\` | ${c.kind} | ${c.reason} |`);
|
|
258
|
+
}
|
|
259
|
+
if (highSeverity.length > limit) {
|
|
260
|
+
lines.push(`| ... | | ${highSeverity.length - limit} more |`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
}
|
|
264
|
+
if (otherSeverity.length > 0) {
|
|
265
|
+
lines.push("### Medium/Low Severity");
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("| Name | Kind | Reason |");
|
|
268
|
+
lines.push("|------|------|--------|");
|
|
269
|
+
for (const c of otherSeverity.slice(0, limit)) {
|
|
270
|
+
lines.push(`| \`${c.name}\` | ${c.kind} | ${c.reason} |`);
|
|
271
|
+
}
|
|
272
|
+
if (otherSeverity.length > limit) {
|
|
273
|
+
lines.push(`| ... | | ${otherSeverity.length - limit} more |`);
|
|
274
|
+
}
|
|
275
|
+
lines.push("");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (data.memberChanges && data.memberChanges.length > 0) {
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push("## Member Changes");
|
|
281
|
+
lines.push("");
|
|
282
|
+
const byClass = groupMemberChangesByClass(data.memberChanges);
|
|
283
|
+
for (const [className, changes] of byClass) {
|
|
284
|
+
lines.push(`### ${className}`);
|
|
285
|
+
lines.push("");
|
|
286
|
+
lines.push("| Member | Change | Details |");
|
|
287
|
+
lines.push("|--------|--------|---------|");
|
|
288
|
+
for (const mc of changes.slice(0, limit)) {
|
|
289
|
+
const details = getChangeDetails(mc);
|
|
290
|
+
lines.push(`| \`${mc.memberName}()\` | ${mc.changeType} | ${details} |`);
|
|
291
|
+
}
|
|
292
|
+
if (changes.length > limit) {
|
|
293
|
+
lines.push(`| ... | | ${changes.length - limit} more |`);
|
|
294
|
+
}
|
|
295
|
+
lines.push("");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (data.nonBreaking.length > 0) {
|
|
299
|
+
lines.push("");
|
|
300
|
+
lines.push("## New Exports");
|
|
301
|
+
lines.push("");
|
|
302
|
+
const undocSet = new Set(data.newUndocumented);
|
|
303
|
+
lines.push("| Export | Documented |");
|
|
304
|
+
lines.push("|--------|------------|");
|
|
305
|
+
for (const name of data.nonBreaking.slice(0, limit)) {
|
|
306
|
+
const documented = undocSet.has(name) ? "No" : "Yes";
|
|
307
|
+
lines.push(`| \`${name}\` | ${documented} |`);
|
|
308
|
+
}
|
|
309
|
+
if (data.nonBreaking.length > limit) {
|
|
310
|
+
lines.push(`| ... | ${data.nonBreaking.length - limit} more |`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (data.docsImpact) {
|
|
314
|
+
renderDocsImpactSection(lines, data.docsImpact, limit);
|
|
315
|
+
}
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push("---");
|
|
318
|
+
lines.push("*Generated by [DocCov](https://doccov.com)*");
|
|
319
|
+
return lines.join(`
|
|
320
|
+
`);
|
|
321
|
+
}
|
|
322
|
+
function groupMemberChangesByClass(changes) {
|
|
323
|
+
const byClass = new Map;
|
|
324
|
+
for (const mc of changes) {
|
|
325
|
+
const list = byClass.get(mc.className) ?? [];
|
|
326
|
+
list.push(mc);
|
|
327
|
+
byClass.set(mc.className, list);
|
|
328
|
+
}
|
|
329
|
+
return byClass;
|
|
330
|
+
}
|
|
331
|
+
function getChangeDetails(mc) {
|
|
332
|
+
if (mc.changeType === "removed") {
|
|
333
|
+
return mc.suggestion ? `→ ${mc.suggestion}` : "removed";
|
|
334
|
+
}
|
|
335
|
+
if (mc.changeType === "signature-changed") {
|
|
336
|
+
if (mc.oldSignature && mc.newSignature) {
|
|
337
|
+
return `\`${mc.oldSignature}\` → \`${mc.newSignature}\``;
|
|
338
|
+
}
|
|
339
|
+
return "signature changed";
|
|
340
|
+
}
|
|
341
|
+
if (mc.changeType === "added") {
|
|
342
|
+
return "new";
|
|
343
|
+
}
|
|
344
|
+
return "-";
|
|
345
|
+
}
|
|
346
|
+
function renderDocsImpactSection(lines, docsImpact, limit) {
|
|
347
|
+
const { impactedFiles, missingDocs, stats, allUndocumented } = docsImpact;
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push("## Documentation Impact");
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push(`Scanned **${stats.filesScanned}** files, **${stats.codeBlocksFound}** code blocks, **${stats.referencesFound}** references.`);
|
|
352
|
+
lines.push("");
|
|
353
|
+
if (impactedFiles.length === 0 && missingDocs.length === 0) {
|
|
354
|
+
lines.push("No documentation updates required.");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (impactedFiles.length > 0) {
|
|
358
|
+
lines.push("### Files Requiring Updates");
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push("| File | Issues | Details |");
|
|
361
|
+
lines.push("|------|--------|---------|");
|
|
362
|
+
for (const file of impactedFiles.slice(0, limit)) {
|
|
363
|
+
const filename = path2.basename(file.file);
|
|
364
|
+
const issueCount = file.references.length;
|
|
365
|
+
const firstRef = file.references[0];
|
|
366
|
+
const detail = firstRef ? `L${firstRef.line}: ${firstRef.memberName ?? firstRef.exportName}` : "-";
|
|
367
|
+
lines.push(`| \`${filename}\` | ${issueCount} | ${detail} |`);
|
|
368
|
+
}
|
|
369
|
+
if (impactedFiles.length > limit) {
|
|
370
|
+
lines.push(`| ... | | ${impactedFiles.length - limit} more files |`);
|
|
371
|
+
}
|
|
372
|
+
lines.push("");
|
|
373
|
+
}
|
|
374
|
+
if (missingDocs.length > 0) {
|
|
375
|
+
lines.push("### New Exports Missing Documentation");
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push("| Export |");
|
|
378
|
+
lines.push("|--------|");
|
|
379
|
+
for (const name of missingDocs.slice(0, limit)) {
|
|
380
|
+
lines.push(`| \`${name}\` |`);
|
|
381
|
+
}
|
|
382
|
+
if (missingDocs.length > limit) {
|
|
383
|
+
lines.push(`| ... ${missingDocs.length - limit} more |`);
|
|
384
|
+
}
|
|
385
|
+
lines.push("");
|
|
386
|
+
}
|
|
387
|
+
if (allUndocumented && allUndocumented.length > 0 && stats.totalExports > 0) {
|
|
388
|
+
const docPercent = Math.round((stats.totalExports - allUndocumented.length) / stats.totalExports * 100);
|
|
389
|
+
lines.push("### Documentation Coverage");
|
|
390
|
+
lines.push("");
|
|
391
|
+
lines.push(`**${stats.documentedExports}/${stats.totalExports}** exports documented (${docPercent}%) \`${bar(docPercent)}\``);
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/reports/diff-html.ts
|
|
397
|
+
function escapeHtml(s) {
|
|
398
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
399
|
+
}
|
|
400
|
+
function renderDiffHtml(data, options = {}) {
|
|
401
|
+
const md = renderDiffMarkdown(data, options);
|
|
402
|
+
return `<!DOCTYPE html>
|
|
403
|
+
<html lang="en">
|
|
404
|
+
<head>
|
|
405
|
+
<meta charset="UTF-8">
|
|
406
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
407
|
+
<title>DocCov Diff: ${escapeHtml(data.baseName)} → ${escapeHtml(data.headName)}</title>
|
|
408
|
+
<style>
|
|
409
|
+
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --success: #3fb950; --warning: #d29922; --danger: #f85149; }
|
|
410
|
+
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; }
|
|
411
|
+
h1, h2, h3 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
412
|
+
h1 { color: var(--accent); }
|
|
413
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
414
|
+
th, td { border: 1px solid var(--border); padding: 0.5rem 1rem; text-align: left; }
|
|
415
|
+
th { background: #161b22; }
|
|
416
|
+
code { background: #161b22; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
|
417
|
+
a { color: var(--accent); }
|
|
418
|
+
.delta-positive { color: var(--success); }
|
|
419
|
+
.delta-negative { color: var(--danger); }
|
|
420
|
+
.delta-neutral { color: var(--fg); }
|
|
421
|
+
</style>
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(md)}</pre>
|
|
425
|
+
</body>
|
|
426
|
+
</html>`;
|
|
427
|
+
}
|
|
211
428
|
// src/reports/github.ts
|
|
212
429
|
function renderGithubSummary(stats, options = {}) {
|
|
213
430
|
const coverageScore = options.coverageScore ?? stats.coverageScore;
|
|
@@ -235,7 +452,7 @@ ${status} Coverage ${coverageScore >= 80 ? "passing" : coverageScore >= 50 ? "ne
|
|
|
235
452
|
}
|
|
236
453
|
// src/reports/markdown.ts
|
|
237
454
|
import { DRIFT_CATEGORY_LABELS } from "@openpkg-ts/spec";
|
|
238
|
-
function
|
|
455
|
+
function bar2(pct, width = 10) {
|
|
239
456
|
const filled = Math.round(pct / 100 * width);
|
|
240
457
|
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
241
458
|
}
|
|
@@ -244,7 +461,7 @@ function renderMarkdown(stats, options = {}) {
|
|
|
244
461
|
const lines = [];
|
|
245
462
|
lines.push(`# DocCov Report: ${stats.packageName}@${stats.version}`);
|
|
246
463
|
lines.push("");
|
|
247
|
-
lines.push(`**Coverage: ${stats.coverageScore}%** \`${
|
|
464
|
+
lines.push(`**Coverage: ${stats.coverageScore}%** \`${bar2(stats.coverageScore)}\``);
|
|
248
465
|
lines.push("");
|
|
249
466
|
lines.push("| Metric | Value |");
|
|
250
467
|
lines.push("|--------|-------|");
|
|
@@ -259,7 +476,7 @@ function renderMarkdown(stats, options = {}) {
|
|
|
259
476
|
lines.push("| Signal | Coverage |");
|
|
260
477
|
lines.push("|--------|----------|");
|
|
261
478
|
for (const [sig, s] of Object.entries(stats.signalCoverage)) {
|
|
262
|
-
lines.push(`| ${sig} | ${s.pct}% \`${
|
|
479
|
+
lines.push(`| ${sig} | ${s.pct}% \`${bar2(s.pct, 8)}\` |`);
|
|
263
480
|
}
|
|
264
481
|
if (stats.byKind.length > 0) {
|
|
265
482
|
lines.push("");
|
|
@@ -331,7 +548,7 @@ function renderMarkdown(stats, options = {}) {
|
|
|
331
548
|
}
|
|
332
549
|
|
|
333
550
|
// src/reports/html.ts
|
|
334
|
-
function
|
|
551
|
+
function escapeHtml2(s) {
|
|
335
552
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
336
553
|
}
|
|
337
554
|
function renderHtml(stats, options = {}) {
|
|
@@ -341,7 +558,7 @@ function renderHtml(stats, options = {}) {
|
|
|
341
558
|
<head>
|
|
342
559
|
<meta charset="UTF-8">
|
|
343
560
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
344
|
-
<title>DocCov Report: ${
|
|
561
|
+
<title>DocCov Report: ${escapeHtml2(stats.packageName)}</title>
|
|
345
562
|
<style>
|
|
346
563
|
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; }
|
|
347
564
|
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 +571,7 @@ function renderHtml(stats, options = {}) {
|
|
|
354
571
|
</style>
|
|
355
572
|
</head>
|
|
356
573
|
<body>
|
|
357
|
-
<pre style="white-space: pre-wrap; font-family: inherit;">${
|
|
574
|
+
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml2(md)}</pre>
|
|
358
575
|
</body>
|
|
359
576
|
</html>`;
|
|
360
577
|
}
|
|
@@ -467,18 +684,18 @@ function computeStats(spec) {
|
|
|
467
684
|
}
|
|
468
685
|
// src/reports/writer.ts
|
|
469
686
|
import * as fs from "node:fs";
|
|
470
|
-
import * as
|
|
687
|
+
import * as path3 from "node:path";
|
|
471
688
|
import { DEFAULT_REPORT_DIR, getReportPath } from "@doccov/sdk";
|
|
472
689
|
import chalk from "chalk";
|
|
473
690
|
function writeReport(options) {
|
|
474
691
|
const { format, content, outputPath, cwd = process.cwd(), silent = false } = options;
|
|
475
|
-
const reportPath = outputPath ?
|
|
476
|
-
const dir =
|
|
692
|
+
const reportPath = outputPath ? path3.resolve(cwd, outputPath) : path3.resolve(cwd, getReportPath(format));
|
|
693
|
+
const dir = path3.dirname(reportPath);
|
|
477
694
|
if (!fs.existsSync(dir)) {
|
|
478
695
|
fs.mkdirSync(dir, { recursive: true });
|
|
479
696
|
}
|
|
480
697
|
fs.writeFileSync(reportPath, content);
|
|
481
|
-
const relativePath =
|
|
698
|
+
const relativePath = path3.relative(cwd, reportPath);
|
|
482
699
|
if (!silent) {
|
|
483
700
|
console.log(chalk.green(`✓ Wrote ${format} report to ${relativePath}`));
|
|
484
701
|
}
|
|
@@ -571,6 +788,17 @@ async function parseAssertionsWithLLM(code) {
|
|
|
571
788
|
}
|
|
572
789
|
}
|
|
573
790
|
|
|
791
|
+
// src/utils/validation.ts
|
|
792
|
+
function clampPercentage(value, fallback = 80) {
|
|
793
|
+
if (Number.isNaN(value))
|
|
794
|
+
return fallback;
|
|
795
|
+
return Math.min(100, Math.max(0, Math.round(value)));
|
|
796
|
+
}
|
|
797
|
+
function resolveThreshold(cliValue, configValue) {
|
|
798
|
+
const raw = cliValue ?? configValue;
|
|
799
|
+
return raw !== undefined ? clampPercentage(Number(raw)) : undefined;
|
|
800
|
+
}
|
|
801
|
+
|
|
574
802
|
// src/commands/check.ts
|
|
575
803
|
var defaultDependencies = {
|
|
576
804
|
createDocCov: (options) => new DocCov(options),
|
|
@@ -617,9 +845,9 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
617
845
|
}
|
|
618
846
|
const config = await loadDocCovConfig(targetDir);
|
|
619
847
|
const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
|
|
620
|
-
const minCoverage = minCoverageRaw !== undefined ?
|
|
848
|
+
const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
|
|
621
849
|
const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
|
|
622
|
-
const maxDrift = maxDriftRaw !== undefined ?
|
|
850
|
+
const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
|
|
623
851
|
const resolveExternalTypes = !options.skipResolve;
|
|
624
852
|
let specResult;
|
|
625
853
|
const doccov = createDocCov({
|
|
@@ -711,7 +939,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
711
939
|
log(chalk2.gray(` Skipping ${exp.name}: declaration file`));
|
|
712
940
|
continue;
|
|
713
941
|
}
|
|
714
|
-
const filePath =
|
|
942
|
+
const filePath = path4.resolve(targetDir, exp.source.file);
|
|
715
943
|
if (!fs2.existsSync(filePath)) {
|
|
716
944
|
log(chalk2.gray(` Skipping ${exp.name}: file not found`));
|
|
717
945
|
continue;
|
|
@@ -755,7 +983,7 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
755
983
|
log(chalk2.bold("Dry run - changes that would be made:"));
|
|
756
984
|
log("");
|
|
757
985
|
for (const [filePath, fileEdits] of editsByFile) {
|
|
758
|
-
const relativePath =
|
|
986
|
+
const relativePath = path4.relative(targetDir, filePath);
|
|
759
987
|
log(chalk2.cyan(` ${relativePath}:`));
|
|
760
988
|
for (const { export: exp, edit, fixes } of fileEdits) {
|
|
761
989
|
const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
|
|
@@ -929,12 +1157,6 @@ function registerCheckCommand(program, dependencies = {}) {
|
|
|
929
1157
|
}
|
|
930
1158
|
});
|
|
931
1159
|
}
|
|
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
1160
|
function collectDrift(exportsList) {
|
|
939
1161
|
const drifts = [];
|
|
940
1162
|
for (const entry of exportsList) {
|
|
@@ -957,11 +1179,14 @@ function collectDrift(exportsList) {
|
|
|
957
1179
|
|
|
958
1180
|
// src/commands/diff.ts
|
|
959
1181
|
import * as fs3 from "node:fs";
|
|
960
|
-
import * as
|
|
1182
|
+
import * as path5 from "node:path";
|
|
961
1183
|
import {
|
|
962
1184
|
diffSpecWithDocs,
|
|
1185
|
+
ensureSpecCoverage,
|
|
1186
|
+
getDiffReportPath,
|
|
963
1187
|
getDocsImpactSummary,
|
|
964
1188
|
hasDocsImpact,
|
|
1189
|
+
hashString,
|
|
965
1190
|
parseMarkdownFiles
|
|
966
1191
|
} from "@doccov/sdk";
|
|
967
1192
|
import chalk3 from "chalk";
|
|
@@ -1033,126 +1258,151 @@ var defaultDependencies2 = {
|
|
|
1033
1258
|
log: console.log,
|
|
1034
1259
|
error: console.error
|
|
1035
1260
|
};
|
|
1036
|
-
var
|
|
1037
|
-
"regression",
|
|
1038
|
-
"drift",
|
|
1039
|
-
"
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
];
|
|
1044
|
-
function parseStrictOptions(value) {
|
|
1045
|
-
if (!value)
|
|
1261
|
+
var STRICT_PRESETS = {
|
|
1262
|
+
ci: new Set(["breaking", "regression"]),
|
|
1263
|
+
release: new Set(["breaking", "regression", "drift", "docs-impact", "undocumented"]),
|
|
1264
|
+
quality: new Set(["drift", "undocumented"])
|
|
1265
|
+
};
|
|
1266
|
+
function getStrictChecks(preset) {
|
|
1267
|
+
if (!preset)
|
|
1046
1268
|
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
|
-
}
|
|
1269
|
+
const checks = STRICT_PRESETS[preset];
|
|
1270
|
+
if (!checks) {
|
|
1271
|
+
throw new Error(`Unknown --strict preset: ${preset}. Valid: ci, release, quality`);
|
|
1058
1272
|
}
|
|
1059
|
-
return
|
|
1273
|
+
return checks;
|
|
1060
1274
|
}
|
|
1061
1275
|
function registerDiffCommand(program, dependencies = {}) {
|
|
1062
1276
|
const { readFileSync: readFileSync2, log, error } = {
|
|
1063
1277
|
...defaultDependencies2,
|
|
1064
1278
|
...dependencies
|
|
1065
1279
|
};
|
|
1066
|
-
program.command("diff
|
|
1280
|
+
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
1281
|
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
|
-
}
|
|
1282
|
+
const baseFile = options.base ?? baseArg;
|
|
1283
|
+
const headFile = options.head ?? headArg;
|
|
1284
|
+
if (!baseFile || !headFile) {
|
|
1285
|
+
throw new Error(`Both base and head specs are required.
|
|
1286
|
+
` + `Usage: doccov diff <base> <head>
|
|
1287
|
+
` + " or: doccov diff --base main.json --head feature.json");
|
|
1078
1288
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1289
|
+
const baseSpec = loadSpec(baseFile, readFileSync2);
|
|
1290
|
+
const headSpec = loadSpec(headFile, readFileSync2);
|
|
1291
|
+
const config = await loadDocCovConfig(options.cwd);
|
|
1292
|
+
const baseHash = hashString(JSON.stringify(baseSpec));
|
|
1293
|
+
const headHash = hashString(JSON.stringify(headSpec));
|
|
1294
|
+
const cacheEnabled = options.cache !== false;
|
|
1295
|
+
const cachedReportPath = path5.resolve(options.cwd, getDiffReportPath(baseHash, headHash, "json"));
|
|
1296
|
+
let diff;
|
|
1297
|
+
let fromCache = false;
|
|
1298
|
+
if (cacheEnabled && fs3.existsSync(cachedReportPath)) {
|
|
1299
|
+
try {
|
|
1300
|
+
const cached = JSON.parse(fs3.readFileSync(cachedReportPath, "utf-8"));
|
|
1301
|
+
diff = cached;
|
|
1302
|
+
fromCache = true;
|
|
1303
|
+
} catch {
|
|
1304
|
+
diff = await generateDiff(baseSpec, headSpec, options, config, log);
|
|
1305
|
+
}
|
|
1306
|
+
} else {
|
|
1307
|
+
diff = await generateDiff(baseSpec, headSpec, options, config, log);
|
|
1081
1308
|
}
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1309
|
+
const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
|
|
1310
|
+
const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
|
|
1311
|
+
const format = options.format ?? "text";
|
|
1312
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
1313
|
+
const checks = getStrictChecks(options.strict);
|
|
1314
|
+
const baseName = path5.basename(baseFile);
|
|
1315
|
+
const headName = path5.basename(headFile);
|
|
1316
|
+
const reportData = {
|
|
1317
|
+
baseName,
|
|
1318
|
+
headName,
|
|
1319
|
+
...diff
|
|
1320
|
+
};
|
|
1091
1321
|
switch (format) {
|
|
1092
|
-
case "
|
|
1093
|
-
|
|
1322
|
+
case "text":
|
|
1323
|
+
printSummary(diff, baseName, headName, fromCache, log);
|
|
1324
|
+
if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
|
|
1325
|
+
await printAISummary(diff, log);
|
|
1326
|
+
}
|
|
1327
|
+
if (!options.stdout) {
|
|
1328
|
+
const jsonPath = getDiffReportPath(baseHash, headHash, "json");
|
|
1329
|
+
if (!fromCache) {
|
|
1330
|
+
writeReport({
|
|
1331
|
+
format: "json",
|
|
1332
|
+
content: JSON.stringify(diff, null, 2),
|
|
1333
|
+
cwd: options.cwd,
|
|
1334
|
+
outputPath: jsonPath,
|
|
1335
|
+
silent: true
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
const cacheNote = fromCache ? chalk3.cyan(" (cached)") : "";
|
|
1339
|
+
log(chalk3.dim(`Report: ${jsonPath}`) + cacheNote);
|
|
1340
|
+
}
|
|
1094
1341
|
break;
|
|
1095
|
-
case "
|
|
1096
|
-
|
|
1342
|
+
case "json": {
|
|
1343
|
+
const content = JSON.stringify(diff, null, 2);
|
|
1344
|
+
if (options.stdout) {
|
|
1345
|
+
log(content);
|
|
1346
|
+
} else {
|
|
1347
|
+
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "json");
|
|
1348
|
+
writeReport({
|
|
1349
|
+
format: "json",
|
|
1350
|
+
content,
|
|
1351
|
+
outputPath,
|
|
1352
|
+
cwd: options.cwd
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1097
1355
|
break;
|
|
1098
|
-
|
|
1099
|
-
|
|
1356
|
+
}
|
|
1357
|
+
case "markdown": {
|
|
1358
|
+
const content = renderDiffMarkdown(reportData, { limit });
|
|
1359
|
+
if (options.stdout) {
|
|
1360
|
+
log(content);
|
|
1361
|
+
} else {
|
|
1362
|
+
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "markdown");
|
|
1363
|
+
writeReport({
|
|
1364
|
+
format: "markdown",
|
|
1365
|
+
content,
|
|
1366
|
+
outputPath,
|
|
1367
|
+
cwd: options.cwd
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1100
1370
|
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
|
-
}
|
|
1371
|
+
}
|
|
1372
|
+
case "html": {
|
|
1373
|
+
const content = renderDiffHtml(reportData, { limit });
|
|
1374
|
+
if (options.stdout) {
|
|
1375
|
+
log(content);
|
|
1376
|
+
} else {
|
|
1377
|
+
const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "html");
|
|
1378
|
+
writeReport({
|
|
1379
|
+
format: "html",
|
|
1380
|
+
content,
|
|
1381
|
+
outputPath,
|
|
1382
|
+
cwd: options.cwd
|
|
1383
|
+
});
|
|
1123
1384
|
}
|
|
1124
1385
|
break;
|
|
1386
|
+
}
|
|
1387
|
+
case "github":
|
|
1388
|
+
printGitHubAnnotations(diff, log);
|
|
1389
|
+
break;
|
|
1125
1390
|
}
|
|
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)`));
|
|
1391
|
+
const failures = validateDiff(diff, headSpec, {
|
|
1392
|
+
minCoverage,
|
|
1393
|
+
maxDrift,
|
|
1394
|
+
checks
|
|
1395
|
+
});
|
|
1396
|
+
if (failures.length > 0) {
|
|
1397
|
+
log(chalk3.red(`
|
|
1398
|
+
✗ Check failed`));
|
|
1399
|
+
for (const f of failures) {
|
|
1400
|
+
log(chalk3.red(` - ${f}`));
|
|
1401
|
+
}
|
|
1154
1402
|
process.exitCode = 1;
|
|
1155
|
-
|
|
1403
|
+
} else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
|
|
1404
|
+
log(chalk3.green(`
|
|
1405
|
+
✓ All checks passed`));
|
|
1156
1406
|
}
|
|
1157
1407
|
} catch (commandError) {
|
|
1158
1408
|
error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
|
|
@@ -1176,193 +1426,117 @@ async function loadMarkdownFiles(patterns) {
|
|
|
1176
1426
|
}
|
|
1177
1427
|
return parseMarkdownFiles(files);
|
|
1178
1428
|
}
|
|
1429
|
+
async function generateDiff(baseSpec, headSpec, options, config, log) {
|
|
1430
|
+
let markdownFiles;
|
|
1431
|
+
let docsPatterns = options.docs;
|
|
1432
|
+
if (!docsPatterns || docsPatterns.length === 0) {
|
|
1433
|
+
if (config?.docs?.include) {
|
|
1434
|
+
docsPatterns = config.docs.include;
|
|
1435
|
+
log(chalk3.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (docsPatterns && docsPatterns.length > 0) {
|
|
1439
|
+
markdownFiles = await loadMarkdownFiles(docsPatterns);
|
|
1440
|
+
}
|
|
1441
|
+
return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
|
|
1442
|
+
}
|
|
1179
1443
|
function loadSpec(filePath, readFileSync2) {
|
|
1180
|
-
const resolvedPath =
|
|
1444
|
+
const resolvedPath = path5.resolve(filePath);
|
|
1181
1445
|
if (!fs3.existsSync(resolvedPath)) {
|
|
1182
1446
|
throw new Error(`File not found: ${filePath}`);
|
|
1183
1447
|
}
|
|
1184
1448
|
try {
|
|
1185
1449
|
const content = readFileSync2(resolvedPath, "utf-8");
|
|
1186
|
-
|
|
1450
|
+
const spec = JSON.parse(content);
|
|
1451
|
+
return ensureSpecCoverage(spec);
|
|
1187
1452
|
} catch (parseError) {
|
|
1188
1453
|
throw new Error(`Failed to parse ${filePath}: ${parseError instanceof Error ? parseError.message : parseError}`);
|
|
1189
1454
|
}
|
|
1190
1455
|
}
|
|
1191
|
-
function
|
|
1456
|
+
function printSummary(diff, baseName, headName, fromCache, log) {
|
|
1192
1457
|
log("");
|
|
1193
|
-
|
|
1458
|
+
const cacheIndicator = fromCache ? chalk3.cyan(" (cached)") : "";
|
|
1459
|
+
log(chalk3.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
|
|
1194
1460
|
log("─".repeat(40));
|
|
1195
|
-
printCoverage(diff, log);
|
|
1196
|
-
printAPIChanges(diff, log);
|
|
1197
|
-
if (diff.docsImpact) {
|
|
1198
|
-
printDocsRequiringUpdates(diff, log);
|
|
1199
|
-
}
|
|
1200
1461
|
log("");
|
|
1201
|
-
}
|
|
1202
|
-
function printCoverage(diff, log) {
|
|
1203
1462
|
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 ? ", ..." : ""}`));
|
|
1463
|
+
const coverageSign = diff.coverageDelta > 0 ? "+" : "";
|
|
1464
|
+
log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
|
|
1465
|
+
const breakingCount = diff.breaking.length;
|
|
1466
|
+
const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
|
|
1467
|
+
if (breakingCount > 0) {
|
|
1468
|
+
const severityNote = highSeverity > 0 ? chalk3.red(` (${highSeverity} high severity)`) : "";
|
|
1469
|
+
log(` Breaking: ${chalk3.red(breakingCount)} changes${severityNote}`);
|
|
1470
|
+
} else {
|
|
1471
|
+
log(` Breaking: ${chalk3.green("0")} changes`);
|
|
1472
|
+
}
|
|
1473
|
+
const newCount = diff.nonBreaking.length;
|
|
1474
|
+
const undocCount = diff.newUndocumented.length;
|
|
1475
|
+
if (newCount > 0) {
|
|
1476
|
+
const undocNote = undocCount > 0 ? chalk3.yellow(` (${undocCount} undocumented)`) : "";
|
|
1477
|
+
log(` New: ${chalk3.green(newCount)} exports${undocNote}`);
|
|
1276
1478
|
}
|
|
1277
1479
|
if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
|
|
1278
|
-
log("");
|
|
1279
1480
|
const parts = [];
|
|
1280
|
-
if (diff.driftIntroduced > 0)
|
|
1281
|
-
parts.push(chalk3.red(`+${diff.driftIntroduced}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
parts.
|
|
1285
|
-
}
|
|
1286
|
-
log(` Drift: ${parts.join(", ")}`);
|
|
1481
|
+
if (diff.driftIntroduced > 0)
|
|
1482
|
+
parts.push(chalk3.red(`+${diff.driftIntroduced}`));
|
|
1483
|
+
if (diff.driftResolved > 0)
|
|
1484
|
+
parts.push(chalk3.green(`-${diff.driftResolved}`));
|
|
1485
|
+
log(` Drift: ${parts.join(", ")}`);
|
|
1287
1486
|
}
|
|
1487
|
+
log("");
|
|
1288
1488
|
}
|
|
1289
|
-
function
|
|
1290
|
-
if (!
|
|
1489
|
+
async function printAISummary(diff, log) {
|
|
1490
|
+
if (!isAIDocsAnalysisAvailable()) {
|
|
1491
|
+
log(chalk3.yellow(`
|
|
1492
|
+
⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
|
|
1291
1493
|
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"));
|
|
1494
|
+
}
|
|
1495
|
+
if (!diff.docsImpact)
|
|
1298
1496
|
return;
|
|
1497
|
+
log(chalk3.gray(`
|
|
1498
|
+
Generating AI summary...`));
|
|
1499
|
+
const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
|
|
1500
|
+
file: f.file,
|
|
1501
|
+
exportName: r.exportName,
|
|
1502
|
+
changeType: r.changeType,
|
|
1503
|
+
context: r.context
|
|
1504
|
+
})));
|
|
1505
|
+
const summary = await generateImpactSummary(impacts);
|
|
1506
|
+
if (summary) {
|
|
1507
|
+
log("");
|
|
1508
|
+
log(chalk3.bold("AI Summary"));
|
|
1509
|
+
log(chalk3.cyan(` ${summary}`));
|
|
1299
1510
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
instantiationOnlyFiles.push(file);
|
|
1309
|
-
}
|
|
1511
|
+
}
|
|
1512
|
+
function validateDiff(diff, headSpec, options) {
|
|
1513
|
+
const { minCoverage, maxDrift, checks } = options;
|
|
1514
|
+
const failures = [];
|
|
1515
|
+
const headExportsWithDrift = new Set((headSpec.exports ?? []).filter((e) => e.docs?.drift?.length).map((e) => e.name)).size;
|
|
1516
|
+
const headDriftScore = headSpec.exports?.length ? Math.round(headExportsWithDrift / headSpec.exports.length * 100) : 0;
|
|
1517
|
+
if (minCoverage !== undefined && diff.newCoverage < minCoverage) {
|
|
1518
|
+
failures.push(`Coverage ${diff.newCoverage}% below minimum ${minCoverage}%`);
|
|
1310
1519
|
}
|
|
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
|
-
}
|
|
1520
|
+
if (maxDrift !== undefined && headDriftScore > maxDrift) {
|
|
1521
|
+
failures.push(`Drift ${headDriftScore}% exceeds maximum ${maxDrift}%`);
|
|
1331
1522
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1523
|
+
if (checks.has("regression") && diff.coverageDelta < 0) {
|
|
1524
|
+
failures.push(`Coverage regressed by ${Math.abs(diff.coverageDelta)}%`);
|
|
1334
1525
|
}
|
|
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}`));
|
|
1526
|
+
if (checks.has("breaking") && diff.breaking.length > 0) {
|
|
1527
|
+
failures.push(`${diff.breaking.length} breaking change(s)`);
|
|
1341
1528
|
}
|
|
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 ? ", ..." : ""}`));
|
|
1529
|
+
if (checks.has("drift") && diff.driftIntroduced > 0) {
|
|
1530
|
+
failures.push(`${diff.driftIntroduced} new drift issue(s)`);
|
|
1348
1531
|
}
|
|
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
|
-
}
|
|
1532
|
+
if (checks.has("undocumented") && diff.newUndocumented.length > 0) {
|
|
1533
|
+
failures.push(`${diff.newUndocumented.length} undocumented export(s)`);
|
|
1356
1534
|
}
|
|
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);
|
|
1535
|
+
if (checks.has("docs-impact") && hasDocsImpact(diff)) {
|
|
1536
|
+
const summary = getDocsImpactSummary(diff);
|
|
1537
|
+
failures.push(`${summary.totalIssues} docs issue(s)`);
|
|
1364
1538
|
}
|
|
1365
|
-
return
|
|
1539
|
+
return failures;
|
|
1366
1540
|
}
|
|
1367
1541
|
function printGitHubAnnotations(diff, log) {
|
|
1368
1542
|
if (diff.coverageDelta !== 0) {
|
|
@@ -1410,159 +1584,6 @@ function printGitHubAnnotations(diff, log) {
|
|
|
1410
1584
|
log(`::warning title=Drift Detected::${diff.driftIntroduced} new drift issue(s) introduced`);
|
|
1411
1585
|
}
|
|
1412
1586
|
}
|
|
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
1587
|
|
|
1567
1588
|
// src/commands/info.ts
|
|
1568
1589
|
import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
|
|
@@ -1603,7 +1624,7 @@ function registerInfoCommand(program) {
|
|
|
1603
1624
|
|
|
1604
1625
|
// src/commands/init.ts
|
|
1605
1626
|
import * as fs4 from "node:fs";
|
|
1606
|
-
import * as
|
|
1627
|
+
import * as path6 from "node:path";
|
|
1607
1628
|
import chalk5 from "chalk";
|
|
1608
1629
|
var defaultDependencies3 = {
|
|
1609
1630
|
fileExists: fs4.existsSync,
|
|
@@ -1618,7 +1639,7 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1618
1639
|
...dependencies
|
|
1619
1640
|
};
|
|
1620
1641
|
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 =
|
|
1642
|
+
const cwd = path6.resolve(options.cwd);
|
|
1622
1643
|
const formatOption = String(options.format ?? "auto").toLowerCase();
|
|
1623
1644
|
if (!isValidFormat(formatOption)) {
|
|
1624
1645
|
error(chalk5.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
|
|
@@ -1627,7 +1648,7 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1627
1648
|
}
|
|
1628
1649
|
const existing = findExistingConfig(cwd, fileExists2);
|
|
1629
1650
|
if (existing) {
|
|
1630
|
-
error(chalk5.red(`A DocCov config already exists at ${
|
|
1651
|
+
error(chalk5.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
|
|
1631
1652
|
process.exitCode = 1;
|
|
1632
1653
|
return;
|
|
1633
1654
|
}
|
|
@@ -1637,7 +1658,7 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1637
1658
|
log(chalk5.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
|
|
1638
1659
|
}
|
|
1639
1660
|
const fileName = `doccov.config.${targetFormat}`;
|
|
1640
|
-
const outputPath =
|
|
1661
|
+
const outputPath = path6.join(cwd, fileName);
|
|
1641
1662
|
if (fileExists2(outputPath)) {
|
|
1642
1663
|
error(chalk5.red(`Cannot create ${fileName}; file already exists.`));
|
|
1643
1664
|
process.exitCode = 1;
|
|
@@ -1645,18 +1666,18 @@ function registerInitCommand(program, dependencies = {}) {
|
|
|
1645
1666
|
}
|
|
1646
1667
|
const template = buildTemplate(targetFormat);
|
|
1647
1668
|
writeFileSync3(outputPath, template, { encoding: "utf8" });
|
|
1648
|
-
log(chalk5.green(`✓ Created ${
|
|
1669
|
+
log(chalk5.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
|
|
1649
1670
|
});
|
|
1650
1671
|
}
|
|
1651
1672
|
var isValidFormat = (value) => {
|
|
1652
1673
|
return value === "auto" || value === "mjs" || value === "js" || value === "cjs";
|
|
1653
1674
|
};
|
|
1654
1675
|
var findExistingConfig = (cwd, fileExists2) => {
|
|
1655
|
-
let current =
|
|
1656
|
-
const { root } =
|
|
1676
|
+
let current = path6.resolve(cwd);
|
|
1677
|
+
const { root } = path6.parse(current);
|
|
1657
1678
|
while (true) {
|
|
1658
1679
|
for (const candidate of DOCCOV_CONFIG_FILENAMES) {
|
|
1659
|
-
const candidatePath =
|
|
1680
|
+
const candidatePath = path6.join(current, candidate);
|
|
1660
1681
|
if (fileExists2(candidatePath)) {
|
|
1661
1682
|
return candidatePath;
|
|
1662
1683
|
}
|
|
@@ -1664,7 +1685,7 @@ var findExistingConfig = (cwd, fileExists2) => {
|
|
|
1664
1685
|
if (current === root) {
|
|
1665
1686
|
break;
|
|
1666
1687
|
}
|
|
1667
|
-
current =
|
|
1688
|
+
current = path6.dirname(current);
|
|
1668
1689
|
}
|
|
1669
1690
|
return null;
|
|
1670
1691
|
};
|
|
@@ -1686,17 +1707,17 @@ var detectPackageType = (cwd, fileExists2, readFileSync3) => {
|
|
|
1686
1707
|
return;
|
|
1687
1708
|
};
|
|
1688
1709
|
var findNearestPackageJson = (cwd, fileExists2) => {
|
|
1689
|
-
let current =
|
|
1690
|
-
const { root } =
|
|
1710
|
+
let current = path6.resolve(cwd);
|
|
1711
|
+
const { root } = path6.parse(current);
|
|
1691
1712
|
while (true) {
|
|
1692
|
-
const candidate =
|
|
1713
|
+
const candidate = path6.join(current, "package.json");
|
|
1693
1714
|
if (fileExists2(candidate)) {
|
|
1694
1715
|
return candidate;
|
|
1695
1716
|
}
|
|
1696
1717
|
if (current === root) {
|
|
1697
1718
|
break;
|
|
1698
1719
|
}
|
|
1699
|
-
current =
|
|
1720
|
+
current = path6.dirname(current);
|
|
1700
1721
|
}
|
|
1701
1722
|
return null;
|
|
1702
1723
|
};
|
|
@@ -1758,7 +1779,7 @@ var buildTemplate = (format) => {
|
|
|
1758
1779
|
import * as fs6 from "node:fs";
|
|
1759
1780
|
import * as fsPromises from "node:fs/promises";
|
|
1760
1781
|
import * as os from "node:os";
|
|
1761
|
-
import * as
|
|
1782
|
+
import * as path8 from "node:path";
|
|
1762
1783
|
import {
|
|
1763
1784
|
buildCloneUrl,
|
|
1764
1785
|
buildDisplayUrl,
|
|
@@ -1783,7 +1804,7 @@ import { simpleGit } from "simple-git";
|
|
|
1783
1804
|
|
|
1784
1805
|
// src/utils/llm-build-plan.ts
|
|
1785
1806
|
import * as fs5 from "node:fs";
|
|
1786
|
-
import * as
|
|
1807
|
+
import * as path7 from "node:path";
|
|
1787
1808
|
import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
|
|
1788
1809
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
1789
1810
|
import { generateObject as generateObject3 } from "ai";
|
|
@@ -1819,7 +1840,7 @@ function getModel3() {
|
|
|
1819
1840
|
async function gatherContextFiles(repoDir) {
|
|
1820
1841
|
const sections = [];
|
|
1821
1842
|
for (const fileName of CONTEXT_FILES) {
|
|
1822
|
-
const filePath =
|
|
1843
|
+
const filePath = path7.join(repoDir, fileName);
|
|
1823
1844
|
if (fs5.existsSync(filePath)) {
|
|
1824
1845
|
try {
|
|
1825
1846
|
let content = fs5.readFileSync(filePath, "utf-8");
|
|
@@ -1894,7 +1915,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
1894
1915
|
log(chalk6.bold(`Scanning ${displayUrl}`));
|
|
1895
1916
|
log(chalk6.gray(`Branch/tag: ${parsed.ref}`));
|
|
1896
1917
|
log("");
|
|
1897
|
-
tempDir =
|
|
1918
|
+
tempDir = path8.join(os.tmpdir(), `doccov-scan-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
1898
1919
|
fs6.mkdirSync(tempDir, { recursive: true });
|
|
1899
1920
|
process.stdout.write(chalk6.cyan(`> Cloning ${parsed.owner}/${parsed.repo}...
|
|
1900
1921
|
`));
|
|
@@ -2030,7 +2051,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2030
2051
|
error("");
|
|
2031
2052
|
throw new Error(`Package not found: ${options.package}`);
|
|
2032
2053
|
}
|
|
2033
|
-
targetDir =
|
|
2054
|
+
targetDir = path8.join(tempDir, pkg.path);
|
|
2034
2055
|
packageName = pkg.name;
|
|
2035
2056
|
log(chalk6.gray(`Analyzing package: ${packageName}`));
|
|
2036
2057
|
}
|
|
@@ -2079,7 +2100,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2079
2100
|
`));
|
|
2080
2101
|
const llmEntry = await runLlmFallback("WASM project detected");
|
|
2081
2102
|
if (llmEntry) {
|
|
2082
|
-
entryPath =
|
|
2103
|
+
entryPath = path8.join(targetDir, llmEntry);
|
|
2083
2104
|
if (buildFailed) {
|
|
2084
2105
|
process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (using pre-committed declarations)
|
|
2085
2106
|
`));
|
|
@@ -2089,20 +2110,20 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2089
2110
|
`));
|
|
2090
2111
|
}
|
|
2091
2112
|
} else {
|
|
2092
|
-
entryPath =
|
|
2113
|
+
entryPath = path8.join(targetDir, entry.path);
|
|
2093
2114
|
process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2094
2115
|
`));
|
|
2095
2116
|
log(chalk6.yellow(" ⚠ WASM project detected but no API key - analysis may be limited"));
|
|
2096
2117
|
}
|
|
2097
2118
|
} else {
|
|
2098
|
-
entryPath =
|
|
2119
|
+
entryPath = path8.join(targetDir, entry.path);
|
|
2099
2120
|
process.stdout.write(chalk6.green(`✓ Entry point: ${entry.path} (from ${entry.source})
|
|
2100
2121
|
`));
|
|
2101
2122
|
}
|
|
2102
2123
|
} catch (entryError) {
|
|
2103
2124
|
const llmEntry = await runLlmFallback("Heuristics failed");
|
|
2104
2125
|
if (llmEntry) {
|
|
2105
|
-
entryPath =
|
|
2126
|
+
entryPath = path8.join(targetDir, llmEntry);
|
|
2106
2127
|
process.stdout.write(chalk6.green(`✓ Entry point: ${llmEntry} (from LLM fallback)
|
|
2107
2128
|
`));
|
|
2108
2129
|
} else {
|
|
@@ -2127,7 +2148,7 @@ function registerScanCommand(program, dependencies = {}) {
|
|
|
2127
2148
|
}
|
|
2128
2149
|
const spec = result.spec;
|
|
2129
2150
|
if (options.saveSpec) {
|
|
2130
|
-
const specPath =
|
|
2151
|
+
const specPath = path8.resolve(process.cwd(), options.saveSpec);
|
|
2131
2152
|
fs6.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
2132
2153
|
log(chalk6.green(`✓ Saved spec to ${options.saveSpec}`));
|
|
2133
2154
|
}
|
|
@@ -2252,7 +2273,7 @@ function printTextResult(result, log) {
|
|
|
2252
2273
|
|
|
2253
2274
|
// src/commands/spec.ts
|
|
2254
2275
|
import * as fs7 from "node:fs";
|
|
2255
|
-
import * as
|
|
2276
|
+
import * as path9 from "node:path";
|
|
2256
2277
|
import { DocCov as DocCov4, NodeFileSystem as NodeFileSystem4, resolveTarget as resolveTarget3 } from "@doccov/sdk";
|
|
2257
2278
|
import { normalize, validateSpec } from "@openpkg-ts/spec";
|
|
2258
2279
|
import chalk8 from "chalk";
|
|
@@ -2300,7 +2321,7 @@ function getArrayLength(value) {
|
|
|
2300
2321
|
}
|
|
2301
2322
|
function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
|
|
2302
2323
|
const location = diagnostic.location;
|
|
2303
|
-
const relativePath = location?.file ?
|
|
2324
|
+
const relativePath = location?.file ? path9.relative(baseDir, location.file) || location.file : undefined;
|
|
2304
2325
|
const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
|
|
2305
2326
|
const locationPrefix = locationText ? `${locationText} ` : "";
|
|
2306
2327
|
return `${prefix} ${locationPrefix}${diagnostic.message}`;
|
|
@@ -2329,7 +2350,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2329
2350
|
try {
|
|
2330
2351
|
config = await loadDocCovConfig(targetDir);
|
|
2331
2352
|
if (config?.filePath) {
|
|
2332
|
-
log(chalk8.gray(`Loaded configuration from ${
|
|
2353
|
+
log(chalk8.gray(`Loaded configuration from ${path9.relative(targetDir, config.filePath)}`));
|
|
2333
2354
|
}
|
|
2334
2355
|
} catch (configError) {
|
|
2335
2356
|
error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
|
|
@@ -2385,7 +2406,7 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2385
2406
|
}
|
|
2386
2407
|
process.exit(1);
|
|
2387
2408
|
}
|
|
2388
|
-
const outputPath =
|
|
2409
|
+
const outputPath = path9.resolve(process.cwd(), options.output);
|
|
2389
2410
|
writeFileSync5(outputPath, JSON.stringify(normalized, null, 2));
|
|
2390
2411
|
log(chalk8.green(`> Wrote ${options.output}`));
|
|
2391
2412
|
log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
|
|
@@ -2407,8 +2428,8 @@ function registerSpecCommand(program, dependencies = {}) {
|
|
|
2407
2428
|
|
|
2408
2429
|
// src/cli.ts
|
|
2409
2430
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2410
|
-
var __dirname2 =
|
|
2411
|
-
var packageJson = JSON.parse(readFileSync4(
|
|
2431
|
+
var __dirname2 = path10.dirname(__filename2);
|
|
2432
|
+
var packageJson = JSON.parse(readFileSync4(path10.join(__dirname2, "../package.json"), "utf-8"));
|
|
2412
2433
|
var program = new Command;
|
|
2413
2434
|
program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
|
|
2414
2435
|
registerCheckCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "DocCov CLI - Documentation coverage and drift detection for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@ai-sdk/anthropic": "^1.0.0",
|
|
50
50
|
"@ai-sdk/openai": "^1.0.0",
|
|
51
|
-
"@doccov/sdk": "^0.
|
|
51
|
+
"@doccov/sdk": "^0.12.0",
|
|
52
52
|
"@inquirer/prompts": "^7.8.0",
|
|
53
53
|
"@openpkg-ts/spec": "^0.8.0",
|
|
54
54
|
"ai": "^4.0.0",
|