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