@doccov/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config/doccov-config.ts
4
- import { access, readFile } from "node:fs/promises";
4
+ import { access } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
- import { parse as parseYaml } from "yaml";
8
7
 
9
8
  // src/config/schema.ts
10
9
  import { z } from "zod";
@@ -16,7 +15,6 @@ var docsConfigSchema = z.object({
16
15
  include: stringList.optional(),
17
16
  exclude: stringList.optional()
18
17
  });
19
- var severitySchema = z.enum(["error", "warn", "off"]);
20
18
  var exampleModeSchema = z.enum([
21
19
  "presence",
22
20
  "typecheck",
@@ -32,16 +30,12 @@ var checkConfigSchema = z.object({
32
30
  minCoverage: z.number().min(0).max(100).optional(),
33
31
  maxDrift: z.number().min(0).max(100).optional()
34
32
  });
35
- var qualityConfigSchema = z.object({
36
- rules: z.record(severitySchema).optional()
37
- });
38
33
  var docCovConfigSchema = z.object({
39
34
  include: stringList.optional(),
40
35
  exclude: stringList.optional(),
41
36
  plugins: z.array(z.unknown()).optional(),
42
37
  docs: docsConfigSchema.optional(),
43
- check: checkConfigSchema.optional(),
44
- quality: qualityConfigSchema.optional()
38
+ check: checkConfigSchema.optional()
45
39
  });
46
40
  var normalizeList = (value) => {
47
41
  if (!value) {
@@ -73,19 +67,12 @@ var normalizeConfig = (input) => {
73
67
  maxDrift: input.check.maxDrift
74
68
  };
75
69
  }
76
- let quality;
77
- if (input.quality) {
78
- quality = {
79
- rules: input.quality.rules
80
- };
81
- }
82
70
  return {
83
71
  include,
84
72
  exclude,
85
73
  plugins: input.plugins,
86
74
  docs,
87
- check,
88
- quality
75
+ check
89
76
  };
90
77
  };
91
78
 
@@ -93,12 +80,8 @@ var normalizeConfig = (input) => {
93
80
  var DOCCOV_CONFIG_FILENAMES = [
94
81
  "doccov.config.ts",
95
82
  "doccov.config.mts",
96
- "doccov.config.cts",
97
83
  "doccov.config.js",
98
- "doccov.config.mjs",
99
- "doccov.config.cjs",
100
- "doccov.yml",
101
- "doccov.yaml"
84
+ "doccov.config.mjs"
102
85
  ];
103
86
  var fileExists = async (filePath) => {
104
87
  try {
@@ -125,11 +108,6 @@ var findConfigFile = async (cwd) => {
125
108
  }
126
109
  };
127
110
  var importConfigModule = async (absolutePath) => {
128
- const ext = path.extname(absolutePath);
129
- if (ext === ".yml" || ext === ".yaml") {
130
- const content = await readFile(absolutePath, "utf-8");
131
- return parseYaml(content);
132
- }
133
111
  const fileUrl = pathToFileURL(absolutePath);
134
112
  fileUrl.searchParams.set("t", Date.now().toString());
135
113
  const module = await import(fileUrl.href);
@@ -168,8 +146,8 @@ ${formatIssues(issues)}`);
168
146
  // src/config/index.ts
169
147
  var defineConfig = (config) => config;
170
148
  // src/cli.ts
171
- import { readFileSync as readFileSync3 } from "node:fs";
172
- import * as path8 from "node:path";
149
+ import { readFileSync as readFileSync5 } from "node:fs";
150
+ import * as path9 from "node:path";
173
151
  import { fileURLToPath } from "node:url";
174
152
  import { Command } from "commander";
175
153
 
@@ -189,6 +167,7 @@ import {
189
167
  NodeFileSystem,
190
168
  parseExamplesFlag,
191
169
  parseJSDocToPatch,
170
+ parseMarkdownFiles,
192
171
  resolveTarget,
193
172
  serializeJSDoc,
194
173
  validateExamples
@@ -196,8 +175,76 @@ import {
196
175
  import {
197
176
  DRIFT_CATEGORIES as DRIFT_CATEGORIES2
198
177
  } from "@openpkg-ts/spec";
199
- import chalk3 from "chalk";
178
+ import chalk4 from "chalk";
179
+ import { glob } from "glob";
200
180
 
181
+ // src/reports/changelog-renderer.ts
182
+ function renderChangelog(data, options = {}) {
183
+ const { diff, categorizedBreaking } = data;
184
+ const lines = [];
185
+ const version = options.version ?? data.version ?? "Unreleased";
186
+ const date = options.date instanceof Date ? options.date.toISOString().split("T")[0] : options.date ?? new Date().toISOString().split("T")[0];
187
+ lines.push(`## [${version}] - ${date}`);
188
+ lines.push("");
189
+ if (diff.breaking.length > 0) {
190
+ lines.push("### ⚠️ BREAKING CHANGES");
191
+ lines.push("");
192
+ if (categorizedBreaking && categorizedBreaking.length > 0) {
193
+ for (const breaking of categorizedBreaking) {
194
+ const severity = breaking.severity === "high" ? "**" : "";
195
+ lines.push(`- ${severity}${breaking.name}${severity}: ${breaking.reason}`);
196
+ }
197
+ } else {
198
+ for (const id of diff.breaking) {
199
+ lines.push(`- \`${id}\` removed or changed`);
200
+ }
201
+ }
202
+ lines.push("");
203
+ }
204
+ if (diff.nonBreaking.length > 0) {
205
+ lines.push("### Added");
206
+ lines.push("");
207
+ for (const id of diff.nonBreaking) {
208
+ lines.push(`- \`${id}\``);
209
+ }
210
+ lines.push("");
211
+ }
212
+ if (diff.docsOnly.length > 0) {
213
+ lines.push("### Documentation");
214
+ lines.push("");
215
+ for (const id of diff.docsOnly) {
216
+ lines.push(`- Updated documentation for \`${id}\``);
217
+ }
218
+ lines.push("");
219
+ }
220
+ if (diff.coverageDelta !== 0) {
221
+ lines.push("### Coverage");
222
+ lines.push("");
223
+ const arrow = diff.coverageDelta > 0 ? "↑" : "↓";
224
+ const sign = diff.coverageDelta > 0 ? "+" : "";
225
+ lines.push(`- Documentation coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% (${arrow} ${sign}${diff.coverageDelta}%)`);
226
+ lines.push("");
227
+ }
228
+ if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
229
+ if (!lines.some((l) => l.startsWith("### Coverage"))) {
230
+ lines.push("### Coverage");
231
+ lines.push("");
232
+ }
233
+ if (diff.driftResolved > 0) {
234
+ lines.push(`- Fixed ${diff.driftResolved} drift issue${diff.driftResolved === 1 ? "" : "s"}`);
235
+ }
236
+ if (diff.driftIntroduced > 0) {
237
+ lines.push(`- ${diff.driftIntroduced} new drift issue${diff.driftIntroduced === 1 ? "" : "s"} detected`);
238
+ }
239
+ lines.push("");
240
+ }
241
+ if (options.compareUrl) {
242
+ lines.push(`**Full Changelog**: ${options.compareUrl}`);
243
+ lines.push("");
244
+ }
245
+ return lines.join(`
246
+ `);
247
+ }
201
248
  // src/reports/diff-markdown.ts
202
249
  import * as path2 from "node:path";
203
250
  function bar(pct, width = 10) {
@@ -570,7 +617,8 @@ function renderPRComment(data, opts = {}) {
570
617
  const { diff, headSpec } = data;
571
618
  const limit = opts.limit ?? 10;
572
619
  const lines = [];
573
- const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
620
+ const hasStaleRefs = (opts.staleDocsRefs?.length ?? 0) > 0;
621
+ const hasIssues = diff.newUndocumented.length > 0 || diff.driftIntroduced > 0 || diff.breaking.length > 0 || hasStaleRefs || opts.minCoverage !== undefined && diff.newCoverage < opts.minCoverage;
574
622
  const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
575
623
  lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
576
624
  lines.push("");
@@ -582,6 +630,13 @@ function renderPRComment(data, opts = {}) {
582
630
  if (diff.driftIntroduced > 0) {
583
631
  lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
584
632
  }
633
+ if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
634
+ lines.push(`**Stale doc references:** ${opts.staleDocsRefs.length}`);
635
+ }
636
+ if (opts.semverBump) {
637
+ const emoji = opts.semverBump.bump === "major" ? "\uD83D\uDD34" : opts.semverBump.bump === "minor" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
638
+ lines.push(`**Semver:** ${emoji} ${opts.semverBump.bump.toUpperCase()} (${opts.semverBump.reason})`);
639
+ }
585
640
  if (diff.newUndocumented.length > 0) {
586
641
  lines.push("");
587
642
  lines.push("### Undocumented exports in this PR");
@@ -594,7 +649,15 @@ function renderPRComment(data, opts = {}) {
594
649
  lines.push("");
595
650
  renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
596
651
  }
597
- const fixGuidance = renderFixGuidance(diff);
652
+ if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
653
+ lines.push("");
654
+ lines.push("### \uD83D\uDCDD Stale documentation references");
655
+ lines.push("");
656
+ lines.push("These markdown files reference exports that no longer exist:");
657
+ lines.push("");
658
+ renderStaleDocsRefs(lines, opts.staleDocsRefs, opts, limit);
659
+ }
660
+ const fixGuidance = renderFixGuidance(diff, opts);
598
661
  if (fixGuidance) {
599
662
  lines.push("");
600
663
  lines.push("### How to fix");
@@ -608,6 +671,16 @@ function renderPRComment(data, opts = {}) {
608
671
  renderDetailsTable(lines, diff);
609
672
  lines.push("");
610
673
  lines.push("</details>");
674
+ if (opts.includeBadge !== false && opts.repoUrl) {
675
+ const repoMatch = opts.repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
676
+ if (repoMatch) {
677
+ const [, owner, repo] = repoMatch;
678
+ lines.push("");
679
+ lines.push("---");
680
+ lines.push("");
681
+ lines.push(`[![DocCov](https://doccov.dev/badge/${owner}/${repo})](https://doccov.dev/${owner}/${repo})`);
682
+ }
683
+ }
611
684
  return lines.join(`
612
685
  `);
613
686
  }
@@ -728,15 +801,47 @@ function getMissingSignals(exp) {
728
801
  }
729
802
  return missing;
730
803
  }
731
- function renderFixGuidance(diff) {
804
+ function renderStaleDocsRefs(lines, refs, opts, limit) {
805
+ const byFile = new Map;
806
+ for (const ref of refs) {
807
+ const list = byFile.get(ref.file) ?? [];
808
+ list.push({ line: ref.line, exportName: ref.exportName });
809
+ byFile.set(ref.file, list);
810
+ }
811
+ let count = 0;
812
+ for (const [file, fileRefs] of byFile) {
813
+ if (count >= limit)
814
+ break;
815
+ const fileLink = buildFileLink(file, opts);
816
+ lines.push(`\uD83D\uDCC1 ${fileLink}`);
817
+ for (const ref of fileRefs) {
818
+ if (count >= limit)
819
+ break;
820
+ lines.push(`- Line ${ref.line}: \`${ref.exportName}\` does not exist`);
821
+ count++;
822
+ }
823
+ lines.push("");
824
+ }
825
+ if (refs.length > count) {
826
+ lines.push(`_...and ${refs.length - count} more stale references_`);
827
+ }
828
+ }
829
+ function renderFixGuidance(diff, opts) {
732
830
  const sections = [];
733
831
  if (diff.newUndocumented.length > 0) {
734
832
  sections.push(`**For undocumented exports:**
735
833
  ` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
736
834
  }
737
835
  if (diff.driftIntroduced > 0) {
836
+ const fixableNote = opts.fixableDriftCount && opts.fixableDriftCount > 0 ? `
837
+
838
+ **Quick fix:** Run \`npx doccov check --fix\` to auto-fix ${opts.fixableDriftCount} issue(s).` : "";
738
839
  sections.push(`**For doc drift:**
739
- ` + "Update the code examples in your markdown files to match current signatures.");
840
+ ` + "Update JSDoc to match current code signatures." + fixableNote);
841
+ }
842
+ if (opts.staleDocsRefs && opts.staleDocsRefs.length > 0) {
843
+ sections.push(`**For stale docs:**
844
+ ` + "Update or remove code examples that reference deleted exports.");
740
845
  }
741
846
  if (diff.breaking.length > 0) {
742
847
  sections.push(`**For breaking changes:**
@@ -899,76 +1004,59 @@ function writeReports(options) {
899
1004
  }));
900
1005
  return results;
901
1006
  }
902
- // src/utils/llm-assertion-parser.ts
903
- import { createAnthropic } from "@ai-sdk/anthropic";
904
- import { createOpenAI } from "@ai-sdk/openai";
905
- import { generateObject } from "ai";
906
- import { z as z2 } from "zod";
907
- var AssertionParseSchema = z2.object({
908
- assertions: z2.array(z2.object({
909
- lineNumber: z2.number().describe("1-indexed line number where the assertion appears"),
910
- expected: z2.string().describe("The expected output value"),
911
- originalComment: z2.string().describe("The original comment text"),
912
- suggestedSyntax: z2.string().describe("The line rewritten with standard // => value syntax")
913
- })).describe("List of assertion-like comments found in the code"),
914
- hasAssertions: z2.boolean().describe("Whether any assertion-like comments were found")
915
- });
916
- var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
917
-
918
- Look for comments that appear to specify expected output values, such as:
919
- - "// should be 3"
920
- - "// returns 5"
921
- - "// outputs: hello"
922
- - "// expected: [1, 2, 3]"
923
- - "// 42" (bare value after console.log)
924
- - "// result: true"
925
-
926
- Do NOT include:
927
- - Regular code comments that explain what the code does
928
- - Comments that are instructions or documentation
929
- - Comments with // => (already using standard syntax)
930
-
931
- For each assertion found, extract:
932
- 1. The line number (1-indexed)
933
- 2. The expected value (just the value, not the comment prefix)
934
- 3. The original comment text
935
- 4. A suggested rewrite of the ENTIRE line using "// => value" syntax
936
-
937
- Code:
938
- \`\`\`
939
- ${code}
940
- \`\`\``;
941
- function getModel() {
942
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
943
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
944
- const anthropic = createAnthropic();
945
- return anthropic("claude-sonnet-4-20250514");
946
- }
947
- const openai = createOpenAI();
948
- return openai("gpt-4o-mini");
949
- }
950
- function isLLMAssertionParsingAvailable() {
951
- return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
952
- }
953
- async function parseAssertionsWithLLM(code) {
954
- if (!isLLMAssertionParsingAvailable()) {
955
- return null;
1007
+ // src/utils/filter-options.ts
1008
+ import { mergeFilters, parseListFlag } from "@doccov/sdk";
1009
+ import chalk2 from "chalk";
1010
+ var parseVisibilityFlag = (value) => {
1011
+ if (!value)
1012
+ return;
1013
+ const validTags = ["public", "beta", "alpha", "internal"];
1014
+ const parsed = parseListFlag(value);
1015
+ if (!parsed)
1016
+ return;
1017
+ const result = [];
1018
+ for (const tag of parsed) {
1019
+ const lower = tag.toLowerCase();
1020
+ if (validTags.includes(lower)) {
1021
+ result.push(lower);
1022
+ }
956
1023
  }
957
- try {
958
- const model = getModel();
959
- const { object } = await generateObject({
960
- model,
961
- schema: AssertionParseSchema,
962
- prompt: ASSERTION_PARSE_PROMPT(code)
963
- });
964
- return object;
965
- } catch {
966
- return null;
1024
+ return result.length > 0 ? result : undefined;
1025
+ };
1026
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk2.cyan(value)).join(", ")}`;
1027
+ var mergeFilterOptions = (config, cliOptions) => {
1028
+ const messages = [];
1029
+ if (config?.include) {
1030
+ messages.push(formatList("include filters from config", config.include));
967
1031
  }
968
- }
1032
+ if (config?.exclude) {
1033
+ messages.push(formatList("exclude filters from config", config.exclude));
1034
+ }
1035
+ if (cliOptions.include) {
1036
+ messages.push(formatList("apply include filters from CLI", cliOptions.include));
1037
+ }
1038
+ if (cliOptions.exclude) {
1039
+ messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
1040
+ }
1041
+ if (cliOptions.visibility) {
1042
+ messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
1043
+ }
1044
+ const resolved = mergeFilters(config, cliOptions);
1045
+ if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
1046
+ return { messages };
1047
+ }
1048
+ const source = resolved.source === "override" ? "cli" : resolved.source;
1049
+ return {
1050
+ include: resolved.include,
1051
+ exclude: resolved.exclude,
1052
+ visibility: cliOptions.visibility,
1053
+ source,
1054
+ messages
1055
+ };
1056
+ };
969
1057
 
970
1058
  // src/utils/progress.ts
971
- import chalk2 from "chalk";
1059
+ import chalk3 from "chalk";
972
1060
  class StepProgress {
973
1061
  steps;
974
1062
  currentStep = 0;
@@ -996,7 +1084,7 @@ class StepProgress {
996
1084
  this.completeCurrentStep();
997
1085
  if (message) {
998
1086
  const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
999
- console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
1087
+ console.log(`${chalk3.green("✓")} ${message} ${chalk3.dim(`(${elapsed}s)`)}`);
1000
1088
  }
1001
1089
  }
1002
1090
  render() {
@@ -1004,17 +1092,17 @@ class StepProgress {
1004
1092
  if (!step)
1005
1093
  return;
1006
1094
  const label = step.activeLabel ?? step.label;
1007
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1008
- process.stdout.write(`\r${prefix} ${chalk2.cyan(label)}...`);
1095
+ const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1096
+ process.stdout.write(`\r${prefix} ${chalk3.cyan(label)}...`);
1009
1097
  }
1010
1098
  completeCurrentStep() {
1011
1099
  const step = this.steps[this.currentStep];
1012
1100
  if (!step)
1013
1101
  return;
1014
1102
  const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
1015
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1103
+ const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1016
1104
  process.stdout.write(`\r${" ".repeat(80)}\r`);
1017
- console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
1105
+ console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
1018
1106
  }
1019
1107
  }
1020
1108
 
@@ -1058,10 +1146,10 @@ function registerCheckCommand(program, dependencies = {}) {
1058
1146
  ...defaultDependencies,
1059
1147
  ...dependencies
1060
1148
  };
1061
- 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("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --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)").option("--no-cache", "Bypass spec cache and force regeneration").action(async (entry, options) => {
1149
+ program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--docs <glob>", "Glob pattern for markdown docs to check for stale refs", collect, []).option("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--preview", "Preview fixes with diff output (implies --fix)").option("--dry-run", "Alias for --preview").option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--update-snapshot", "Force regenerate .doccov/report.json").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)").option("--no-cache", "Bypass spec cache and force regeneration").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").action(async (entry, options) => {
1062
1150
  try {
1063
- const validations = parseExamplesFlag(options.examples);
1064
- const hasExamples = validations.length > 0;
1151
+ let validations = parseExamplesFlag(options.examples);
1152
+ let hasExamples = validations.length > 0;
1065
1153
  const stepList = [
1066
1154
  { label: "Resolved target", activeLabel: "Resolving target" },
1067
1155
  { label: "Loaded config", activeLabel: "Loading config" },
@@ -1081,10 +1169,29 @@ function registerCheckCommand(program, dependencies = {}) {
1081
1169
  const { targetDir, entryFile } = resolved;
1082
1170
  steps.next();
1083
1171
  const config = await loadDocCovConfig(targetDir);
1084
- const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
1085
- const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
1172
+ if (!hasExamples && config?.check?.examples) {
1173
+ const configExamples = config.check.examples;
1174
+ if (Array.isArray(configExamples)) {
1175
+ validations = configExamples;
1176
+ } else if (typeof configExamples === "string") {
1177
+ validations = parseExamplesFlag(configExamples);
1178
+ }
1179
+ hasExamples = validations.length > 0;
1180
+ }
1181
+ const DEFAULT_MIN_COVERAGE = 80;
1182
+ const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage ?? DEFAULT_MIN_COVERAGE;
1183
+ const minCoverage = clampPercentage(minCoverageRaw);
1086
1184
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
1087
1185
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
1186
+ const cliFilters = {
1187
+ include: undefined,
1188
+ exclude: undefined,
1189
+ visibility: parseVisibilityFlag(options.visibility)
1190
+ };
1191
+ const resolvedFilters = mergeFilterOptions(config, cliFilters);
1192
+ if (resolvedFilters.visibility) {
1193
+ log(chalk4.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1194
+ }
1088
1195
  steps.next();
1089
1196
  const resolveExternalTypes = !options.skipResolve;
1090
1197
  let specResult;
@@ -1094,7 +1201,8 @@ function registerCheckCommand(program, dependencies = {}) {
1094
1201
  useCache: options.cache !== false,
1095
1202
  cwd: options.cwd
1096
1203
  });
1097
- specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
1204
+ const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
1205
+ specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1098
1206
  if (!specResult) {
1099
1207
  throw new Error("Failed to analyze documentation coverage.");
1100
1208
  }
@@ -1104,13 +1212,8 @@ function registerCheckCommand(program, dependencies = {}) {
1104
1212
  steps.next();
1105
1213
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
1106
1214
  const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
1107
- const shouldFix = options.fix || options.write;
1108
- const violations = [];
1109
- for (const exp of spec.exports ?? []) {
1110
- for (const v of exp.docs?.violations ?? []) {
1111
- violations.push({ exportName: exp.name, violation: v });
1112
- }
1113
- }
1215
+ const isPreview = options.preview || options.dryRun;
1216
+ const shouldFix = options.fix || options.write || isPreview;
1114
1217
  let exampleResult;
1115
1218
  const typecheckErrors = [];
1116
1219
  const runtimeDrifts = [];
@@ -1120,11 +1223,7 @@ function registerCheckCommand(program, dependencies = {}) {
1120
1223
  packagePath: targetDir,
1121
1224
  exportNames: (spec.exports ?? []).map((e) => e.name),
1122
1225
  timeout: 5000,
1123
- installTimeout: 60000,
1124
- llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
1125
- const result = await parseAssertionsWithLLM(example);
1126
- return result;
1127
- } : undefined
1226
+ installTimeout: 60000
1128
1227
  });
1129
1228
  if (exampleResult.typecheck) {
1130
1229
  for (const err of exampleResult.typecheck.errors) {
@@ -1147,6 +1246,41 @@ function registerCheckCommand(program, dependencies = {}) {
1147
1246
  }
1148
1247
  steps.next();
1149
1248
  }
1249
+ const staleRefs = [];
1250
+ let docsPatterns = options.docs;
1251
+ if (docsPatterns.length === 0 && config?.docs?.include) {
1252
+ docsPatterns = config.docs.include;
1253
+ }
1254
+ if (docsPatterns.length > 0) {
1255
+ const markdownFiles = await loadMarkdownFiles(docsPatterns, targetDir);
1256
+ if (markdownFiles.length > 0) {
1257
+ const exportNames = (spec.exports ?? []).map((e) => e.name);
1258
+ const exportSet = new Set(exportNames);
1259
+ for (const mdFile of markdownFiles) {
1260
+ for (const block of mdFile.codeBlocks) {
1261
+ const codeLines = block.code.split(`
1262
+ `);
1263
+ for (let i = 0;i < codeLines.length; i++) {
1264
+ const line = codeLines[i];
1265
+ const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*['"]/);
1266
+ if (importMatch) {
1267
+ const imports = importMatch[1].split(",").map((s) => s.trim().split(/\s+/)[0]);
1268
+ for (const imp of imports) {
1269
+ if (imp && !exportSet.has(imp)) {
1270
+ staleRefs.push({
1271
+ file: mdFile.path,
1272
+ line: block.lineStart + i,
1273
+ exportName: imp,
1274
+ context: line.trim()
1275
+ });
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1150
1284
  const coverageScore = spec.docs?.coverageScore ?? 0;
1151
1285
  const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
1152
1286
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
@@ -1156,12 +1290,12 @@ function registerCheckCommand(program, dependencies = {}) {
1156
1290
  if (allDrifts.length > 0) {
1157
1291
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1158
1292
  if (fixable.length === 0) {
1159
- log(chalk3.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1293
+ log(chalk4.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1160
1294
  } else {
1161
1295
  log("");
1162
- log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
1296
+ log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
1163
1297
  if (nonFixable.length > 0) {
1164
- log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1298
+ log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1165
1299
  }
1166
1300
  log("");
1167
1301
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -1169,22 +1303,22 @@ function registerCheckCommand(program, dependencies = {}) {
1169
1303
  const editsByFile = new Map;
1170
1304
  for (const [exp, drifts] of groupedDrifts) {
1171
1305
  if (!exp.source?.file) {
1172
- log(chalk3.gray(` Skipping ${exp.name}: no source location`));
1306
+ log(chalk4.gray(` Skipping ${exp.name}: no source location`));
1173
1307
  continue;
1174
1308
  }
1175
1309
  if (exp.source.file.endsWith(".d.ts")) {
1176
- log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
1310
+ log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
1177
1311
  continue;
1178
1312
  }
1179
1313
  const filePath = path4.resolve(targetDir, exp.source.file);
1180
1314
  if (!fs2.existsSync(filePath)) {
1181
- log(chalk3.gray(` Skipping ${exp.name}: file not found`));
1315
+ log(chalk4.gray(` Skipping ${exp.name}: file not found`));
1182
1316
  continue;
1183
1317
  }
1184
1318
  const sourceFile = createSourceFile(filePath);
1185
1319
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1186
1320
  if (!location) {
1187
- log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
1321
+ log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
1188
1322
  continue;
1189
1323
  }
1190
1324
  let existingPatch = {};
@@ -1216,34 +1350,61 @@ function registerCheckCommand(program, dependencies = {}) {
1216
1350
  editsByFile.set(filePath, fileEdits);
1217
1351
  }
1218
1352
  if (edits.length > 0) {
1219
- if (options.dryRun) {
1220
- log(chalk3.bold("Dry run - changes that would be made:"));
1353
+ if (isPreview) {
1354
+ log(chalk4.bold("Preview - changes that would be made:"));
1221
1355
  log("");
1222
1356
  for (const [filePath, fileEdits] of editsByFile) {
1223
1357
  const relativePath = path4.relative(targetDir, filePath);
1224
- log(chalk3.cyan(` ${relativePath}:`));
1225
1358
  for (const { export: exp, edit, fixes } of fileEdits) {
1226
- const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
1227
- log(` ${chalk3.bold(exp.name)} [${lineInfo}]`);
1228
- for (const fix of fixes) {
1229
- log(chalk3.green(` + ${fix.description}`));
1359
+ log(chalk4.cyan(`${relativePath}:${edit.startLine + 1}`));
1360
+ log(chalk4.bold(` ${exp.name}`));
1361
+ log("");
1362
+ if (edit.hasExisting && edit.existingJSDoc) {
1363
+ const oldLines = edit.existingJSDoc.split(`
1364
+ `);
1365
+ const newLines = edit.newJSDoc.split(`
1366
+ `);
1367
+ for (const line of oldLines) {
1368
+ log(chalk4.red(` - ${line}`));
1369
+ }
1370
+ for (const line of newLines) {
1371
+ log(chalk4.green(` + ${line}`));
1372
+ }
1373
+ } else {
1374
+ const newLines = edit.newJSDoc.split(`
1375
+ `);
1376
+ for (const line of newLines) {
1377
+ log(chalk4.green(` + ${line}`));
1378
+ }
1230
1379
  }
1380
+ log("");
1381
+ log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
1382
+ log("");
1231
1383
  }
1232
- log("");
1233
1384
  }
1234
- log(chalk3.gray("Run without --dry-run to apply these changes."));
1385
+ const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
1386
+ log(chalk4.yellow(`${totalFixes} fix(es) across ${editsByFile.size} file(s) would be applied.`));
1387
+ log(chalk4.gray("Run with --fix to apply these changes."));
1235
1388
  } else {
1236
1389
  const applyResult = await applyEdits(edits);
1237
1390
  if (applyResult.errors.length > 0) {
1238
1391
  for (const err of applyResult.errors) {
1239
- error(chalk3.red(` ${err.file}: ${err.error}`));
1392
+ error(chalk4.red(` ${err.file}: ${err.error}`));
1240
1393
  }
1241
1394
  }
1395
+ const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
1396
+ log("");
1397
+ log(chalk4.green(`✓ Applied ${totalFixes} fix(es) to ${applyResult.filesModified} file(s)`));
1398
+ for (const [filePath, fileEdits] of editsByFile) {
1399
+ const relativePath = path4.relative(targetDir, filePath);
1400
+ const fixCount = fileEdits.reduce((s, e) => s + e.fixes.length, 0);
1401
+ log(chalk4.dim(` ${relativePath} (${fixCount} fixes)`));
1402
+ }
1242
1403
  }
1243
1404
  }
1244
1405
  }
1245
1406
  }
1246
- if (!options.dryRun) {
1407
+ if (!isPreview) {
1247
1408
  driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1248
1409
  }
1249
1410
  }
@@ -1267,8 +1428,7 @@ function registerCheckCommand(program, dependencies = {}) {
1267
1428
  case "github":
1268
1429
  formatContent = renderGithubSummary(stats, {
1269
1430
  coverageScore,
1270
- driftCount: driftExports.length,
1271
- qualityIssues: violations.length
1431
+ driftCount: driftExports.length
1272
1432
  });
1273
1433
  break;
1274
1434
  default:
@@ -1288,11 +1448,10 @@ function registerCheckCommand(program, dependencies = {}) {
1288
1448
  const totalExportsForDrift2 = spec.exports?.length ?? 0;
1289
1449
  const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
1290
1450
  const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
1291
- const coverageFailed2 = minCoverage !== undefined && coverageScore < minCoverage;
1451
+ const coverageFailed2 = coverageScore < minCoverage;
1292
1452
  const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
1293
- const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
1294
1453
  const hasTypecheckErrors2 = typecheckErrors.length > 0;
1295
- if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2) {
1454
+ if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
1296
1455
  process.exit(1);
1297
1456
  }
1298
1457
  return;
@@ -1300,48 +1459,41 @@ function registerCheckCommand(program, dependencies = {}) {
1300
1459
  const totalExportsForDrift = spec.exports?.length ?? 0;
1301
1460
  const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
1302
1461
  const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
1303
- const coverageFailed = minCoverage !== undefined && coverageScore < minCoverage;
1462
+ const coverageFailed = coverageScore < minCoverage;
1304
1463
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1305
- const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
1306
1464
  const hasTypecheckErrors = typecheckErrors.length > 0;
1307
1465
  if (specWarnings.length > 0 || specInfos.length > 0) {
1308
1466
  log("");
1309
1467
  for (const diag of specWarnings) {
1310
- log(chalk3.yellow(`⚠ ${diag.message}`));
1468
+ log(chalk4.yellow(`⚠ ${diag.message}`));
1311
1469
  if (diag.suggestion) {
1312
- log(chalk3.gray(` ${diag.suggestion}`));
1470
+ log(chalk4.gray(` ${diag.suggestion}`));
1313
1471
  }
1314
1472
  }
1315
1473
  for (const diag of specInfos) {
1316
- log(chalk3.cyan(`ℹ ${diag.message}`));
1474
+ log(chalk4.cyan(`ℹ ${diag.message}`));
1317
1475
  if (diag.suggestion) {
1318
- log(chalk3.gray(` ${diag.suggestion}`));
1476
+ log(chalk4.gray(` ${diag.suggestion}`));
1319
1477
  }
1320
1478
  }
1321
1479
  }
1322
1480
  const pkgName = spec.meta?.name ?? "unknown";
1323
1481
  const pkgVersion = spec.meta?.version ?? "";
1324
1482
  const totalExports = spec.exports?.length ?? 0;
1325
- const errorCount = violations.filter((v) => v.violation.severity === "error").length;
1326
- const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
1327
1483
  log("");
1328
- log(chalk3.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1484
+ log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1329
1485
  log("");
1330
1486
  log(` Exports: ${totalExports}`);
1331
- if (minCoverage !== undefined) {
1332
- if (coverageFailed) {
1333
- log(chalk3.red(` Coverage: ✗ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1334
- } else {
1335
- log(chalk3.green(` Coverage: ✓ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1336
- }
1487
+ if (coverageFailed) {
1488
+ log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1337
1489
  } else {
1338
- log(` Coverage: ${coverageScore}%`);
1490
+ log(chalk4.green(` Coverage: ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1339
1491
  }
1340
1492
  if (maxDrift !== undefined) {
1341
1493
  if (driftFailed) {
1342
- log(chalk3.red(` Drift: ✗ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1494
+ log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1343
1495
  } else {
1344
- log(chalk3.green(` Drift: ✓ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1496
+ log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1345
1497
  }
1346
1498
  } else {
1347
1499
  log(` Drift: ${driftScore}%`);
@@ -1349,48 +1501,51 @@ function registerCheckCommand(program, dependencies = {}) {
1349
1501
  if (exampleResult) {
1350
1502
  const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
1351
1503
  if (typecheckCount > 0) {
1352
- log(` Examples: ${typecheckCount} type errors`);
1504
+ log(chalk4.yellow(` Examples: ${typecheckCount} type error(s)`));
1505
+ for (const err of typecheckErrors.slice(0, 5)) {
1506
+ const loc = `example[${err.error.exampleIndex}]:${err.error.line}:${err.error.column}`;
1507
+ log(chalk4.dim(` ${err.exportName} ${loc}`));
1508
+ log(chalk4.red(` ${err.error.message}`));
1509
+ }
1510
+ if (typecheckErrors.length > 5) {
1511
+ log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
1512
+ }
1353
1513
  } else {
1354
- log(chalk3.green(` Examples: ✓ validated`));
1514
+ log(chalk4.green(` Examples: ✓ validated`));
1355
1515
  }
1356
1516
  }
1357
- if (errorCount > 0 || warnCount > 0) {
1358
- const parts = [];
1359
- if (errorCount > 0)
1360
- parts.push(`${errorCount} errors`);
1361
- if (warnCount > 0)
1362
- parts.push(`${warnCount} warnings`);
1363
- log(` Quality: ${parts.join(", ")}`);
1517
+ const hasStaleRefs = staleRefs.length > 0;
1518
+ if (hasStaleRefs) {
1519
+ log(chalk4.yellow(` Docs: ${staleRefs.length} stale ref(s)`));
1520
+ for (const ref of staleRefs.slice(0, 5)) {
1521
+ log(chalk4.dim(` ${ref.file}:${ref.line} - "${ref.exportName}"`));
1522
+ }
1523
+ if (staleRefs.length > 5) {
1524
+ log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
1525
+ }
1364
1526
  }
1365
1527
  log("");
1366
- const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
1528
+ const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
1367
1529
  if (!failed) {
1368
1530
  const thresholdParts = [];
1369
- if (minCoverage !== undefined) {
1370
- thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
1371
- }
1531
+ thresholdParts.push(`coverage ${coverageScore}% ${minCoverage}%`);
1372
1532
  if (maxDrift !== undefined) {
1373
1533
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1374
1534
  }
1375
- if (thresholdParts.length > 0) {
1376
- log(chalk3.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1377
- } else {
1378
- log(chalk3.green("✓ Check passed"));
1379
- log(chalk3.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1380
- }
1535
+ log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1381
1536
  return;
1382
1537
  }
1383
- if (hasQualityErrors) {
1384
- log(chalk3.red(`✗ ${errorCount} quality errors`));
1385
- }
1386
1538
  if (hasTypecheckErrors) {
1387
- log(chalk3.red(`✗ ${typecheckErrors.length} example type errors`));
1539
+ log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
1540
+ }
1541
+ if (hasStaleRefs) {
1542
+ log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
1388
1543
  }
1389
1544
  log("");
1390
- log(chalk3.dim("Use --format json or --format markdown for detailed reports"));
1545
+ log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
1391
1546
  process.exit(1);
1392
1547
  } catch (commandError) {
1393
- error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1548
+ error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1394
1549
  process.exit(1);
1395
1550
  }
1396
1551
  });
@@ -1414,6 +1569,23 @@ function collectDrift(exportsList) {
1414
1569
  }
1415
1570
  return drifts;
1416
1571
  }
1572
+ function collect(value, previous) {
1573
+ return previous.concat([value]);
1574
+ }
1575
+ async function loadMarkdownFiles(patterns, cwd) {
1576
+ const files = [];
1577
+ for (const pattern of patterns) {
1578
+ const matches = await glob(pattern, { nodir: true, cwd });
1579
+ for (const filePath of matches) {
1580
+ try {
1581
+ const fullPath = path4.resolve(cwd, filePath);
1582
+ const content = fs2.readFileSync(fullPath, "utf-8");
1583
+ files.push({ path: filePath, content });
1584
+ } catch {}
1585
+ }
1586
+ }
1587
+ return parseMarkdownFiles(files);
1588
+ }
1417
1589
 
1418
1590
  // src/commands/diff.ts
1419
1591
  import * as fs3 from "node:fs";
@@ -1425,72 +1597,11 @@ import {
1425
1597
  getDocsImpactSummary,
1426
1598
  hasDocsImpact,
1427
1599
  hashString,
1428
- parseMarkdownFiles
1600
+ parseMarkdownFiles as parseMarkdownFiles2
1429
1601
  } from "@doccov/sdk";
1430
- import chalk4 from "chalk";
1431
- import { glob } from "glob";
1432
-
1433
- // src/utils/docs-impact-ai.ts
1434
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1435
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1436
- import { generateObject as generateObject2, generateText } from "ai";
1437
- import { z as z3 } from "zod";
1438
- var CodeBlockUsageSchema = z3.object({
1439
- isImpacted: z3.boolean().describe("Whether the code block is affected by the change"),
1440
- reason: z3.string().describe("Explanation of why/why not the code is impacted"),
1441
- usageType: z3.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
1442
- suggestedFix: z3.string().optional().describe("If impacted, the suggested code change"),
1443
- confidence: z3.enum(["high", "medium", "low"]).describe("Confidence level of the analysis")
1444
- });
1445
- var MultiBlockAnalysisSchema = z3.object({
1446
- groups: z3.array(z3.object({
1447
- blockIndices: z3.array(z3.number()).describe("Indices of blocks that should run together"),
1448
- reason: z3.string().describe("Why these blocks are related")
1449
- })).describe("Groups of related code blocks"),
1450
- skippedBlocks: z3.array(z3.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
1451
- });
1452
- function getModel2() {
1453
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1454
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1455
- const anthropic = createAnthropic2();
1456
- return anthropic("claude-sonnet-4-20250514");
1457
- }
1458
- const openai = createOpenAI2();
1459
- return openai("gpt-4o-mini");
1460
- }
1461
- function isAIDocsAnalysisAvailable() {
1462
- return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
1463
- }
1464
- async function generateImpactSummary(impacts) {
1465
- if (!isAIDocsAnalysisAvailable()) {
1466
- return null;
1467
- }
1468
- if (impacts.length === 0) {
1469
- return "No documentation impacts detected.";
1470
- }
1471
- try {
1472
- const { text } = await generateText({
1473
- model: getModel2(),
1474
- prompt: `Summarize these documentation impacts for a GitHub PR comment.
1475
-
1476
- Impacts:
1477
- ${impacts.map((i) => `- ${i.file}: ${i.exportName} (${i.changeType})`).join(`
1478
- `)}
1479
-
1480
- Write a brief, actionable summary (2-3 sentences) explaining:
1481
- 1. How many files/references are affected
1482
- 2. What type of updates are needed
1483
- 3. Priority recommendation
1484
-
1485
- Keep it concise and developer-friendly.`
1486
- });
1487
- return text.trim();
1488
- } catch {
1489
- return null;
1490
- }
1491
- }
1492
-
1493
- // src/commands/diff.ts
1602
+ import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
1603
+ import chalk5 from "chalk";
1604
+ import { glob as glob2 } from "glob";
1494
1605
  var defaultDependencies2 = {
1495
1606
  readFileSync: fs3.readFileSync,
1496
1607
  log: console.log,
@@ -1511,11 +1622,11 @@ function getStrictChecks(preset) {
1511
1622
  return checks;
1512
1623
  }
1513
1624
  function registerDiffCommand(program, dependencies = {}) {
1514
- const { readFileSync: readFileSync2, log, error } = {
1625
+ const { readFileSync: readFileSync3, log, error } = {
1515
1626
  ...defaultDependencies2,
1516
1627
  ...dependencies
1517
1628
  };
1518
- program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
1629
+ program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment, changelog", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect2, []).option("--no-cache", "Bypass cache and force regeneration").option("--recommend-version", "Output recommended semver version bump").action(async (baseArg, headArg, options) => {
1519
1630
  try {
1520
1631
  const baseFile = options.base ?? baseArg;
1521
1632
  const headFile = options.head ?? headArg;
@@ -1524,8 +1635,8 @@ function registerDiffCommand(program, dependencies = {}) {
1524
1635
  ` + `Usage: doccov diff <base> <head>
1525
1636
  ` + " or: doccov diff --base main.json --head feature.json");
1526
1637
  }
1527
- const baseSpec = loadSpec(baseFile, readFileSync2);
1528
- const headSpec = loadSpec(headFile, readFileSync2);
1638
+ const baseSpec = loadSpec(baseFile, readFileSync3);
1639
+ const headSpec = loadSpec(headFile, readFileSync3);
1529
1640
  const config = await loadDocCovConfig(options.cwd);
1530
1641
  const baseHash = hashString(JSON.stringify(baseSpec));
1531
1642
  const headHash = hashString(JSON.stringify(headSpec));
@@ -1546,6 +1657,29 @@ function registerDiffCommand(program, dependencies = {}) {
1546
1657
  }
1547
1658
  const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
1548
1659
  const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
1660
+ if (options.recommendVersion) {
1661
+ const recommendation = recommendSemverBump(diff);
1662
+ const currentVersion = headSpec.meta?.version ?? "0.0.0";
1663
+ const nextVersion = calculateNextVersion(currentVersion, recommendation.bump);
1664
+ if (options.format === "json") {
1665
+ log(JSON.stringify({
1666
+ current: currentVersion,
1667
+ recommended: nextVersion,
1668
+ bump: recommendation.bump,
1669
+ reason: recommendation.reason,
1670
+ breakingCount: recommendation.breakingCount,
1671
+ additionCount: recommendation.additionCount,
1672
+ docsOnlyChanges: recommendation.docsOnlyChanges
1673
+ }, null, 2));
1674
+ } else {
1675
+ log("");
1676
+ log(chalk5.bold("Semver Recommendation"));
1677
+ log(` Current version: ${currentVersion}`);
1678
+ log(` Recommended: ${chalk5.cyan(nextVersion)} (${chalk5.yellow(recommendation.bump.toUpperCase())})`);
1679
+ log(` Reason: ${recommendation.reason}`);
1680
+ }
1681
+ return;
1682
+ }
1549
1683
  const format = options.format ?? "text";
1550
1684
  const limit = parseInt(options.limit, 10) || 10;
1551
1685
  const checks = getStrictChecks(options.strict);
@@ -1559,9 +1693,6 @@ function registerDiffCommand(program, dependencies = {}) {
1559
1693
  switch (format) {
1560
1694
  case "text":
1561
1695
  printSummary(diff, baseName, headName, fromCache, log);
1562
- if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
1563
- await printAISummary(diff, log);
1564
- }
1565
1696
  if (!options.stdout) {
1566
1697
  const jsonPath = getDiffReportPath(baseHash, headHash, "json");
1567
1698
  if (!fromCache) {
@@ -1573,8 +1704,8 @@ function registerDiffCommand(program, dependencies = {}) {
1573
1704
  silent: true
1574
1705
  });
1575
1706
  }
1576
- const cacheNote = fromCache ? chalk4.cyan(" (cached)") : "";
1577
- log(chalk4.dim(`Report: ${jsonPath}`) + cacheNote);
1707
+ const cacheNote = fromCache ? chalk5.cyan(" (cached)") : "";
1708
+ log(chalk5.dim(`Report: ${jsonPath}`) + cacheNote);
1578
1709
  }
1579
1710
  break;
1580
1711
  case "json": {
@@ -1626,15 +1757,39 @@ function registerDiffCommand(program, dependencies = {}) {
1626
1757
  printGitHubAnnotations(diff, log);
1627
1758
  break;
1628
1759
  case "pr-comment": {
1760
+ const semverRecommendation = recommendSemverBump(diff);
1629
1761
  const content = renderPRComment({ diff, baseName, headName, headSpec }, {
1630
1762
  repoUrl: options.repoUrl,
1631
1763
  sha: options.sha,
1632
1764
  minCoverage,
1633
- limit
1765
+ limit,
1766
+ semverBump: { bump: semverRecommendation.bump, reason: semverRecommendation.reason }
1634
1767
  });
1635
1768
  log(content);
1636
1769
  break;
1637
1770
  }
1771
+ case "changelog": {
1772
+ const content = renderChangelog({
1773
+ diff,
1774
+ categorizedBreaking: diff.categorizedBreaking,
1775
+ version: headSpec.meta?.version
1776
+ }, {
1777
+ version: headSpec.meta?.version,
1778
+ compareUrl: options.repoUrl ? `${options.repoUrl}/compare/${baseSpec.meta?.version ?? "v0"}...${headSpec.meta?.version ?? "HEAD"}` : undefined
1779
+ });
1780
+ if (options.stdout) {
1781
+ log(content);
1782
+ } else {
1783
+ const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "md");
1784
+ writeReport({
1785
+ format: "markdown",
1786
+ content,
1787
+ outputPath: outputPath.replace(/\.(json|html)$/, ".changelog.md"),
1788
+ cwd: options.cwd
1789
+ });
1790
+ }
1791
+ break;
1792
+ }
1638
1793
  }
1639
1794
  const failures = validateDiff(diff, headSpec, {
1640
1795
  minCoverage,
@@ -1642,29 +1797,29 @@ function registerDiffCommand(program, dependencies = {}) {
1642
1797
  checks
1643
1798
  });
1644
1799
  if (failures.length > 0) {
1645
- log(chalk4.red(`
1800
+ log(chalk5.red(`
1646
1801
  ✗ Check failed`));
1647
1802
  for (const f of failures) {
1648
- log(chalk4.red(` - ${f}`));
1803
+ log(chalk5.red(` - ${f}`));
1649
1804
  }
1650
1805
  process.exitCode = 1;
1651
1806
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1652
- log(chalk4.green(`
1807
+ log(chalk5.green(`
1653
1808
  ✓ All checks passed`));
1654
1809
  }
1655
1810
  } catch (commandError) {
1656
- error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1811
+ error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1657
1812
  process.exitCode = 1;
1658
1813
  }
1659
1814
  });
1660
1815
  }
1661
- function collect(value, previous) {
1816
+ function collect2(value, previous) {
1662
1817
  return previous.concat([value]);
1663
1818
  }
1664
- async function loadMarkdownFiles(patterns) {
1819
+ async function loadMarkdownFiles2(patterns) {
1665
1820
  const files = [];
1666
1821
  for (const pattern of patterns) {
1667
- const matches = await glob(pattern, { nodir: true });
1822
+ const matches = await glob2(pattern, { nodir: true });
1668
1823
  for (const filePath of matches) {
1669
1824
  try {
1670
1825
  const content = fs3.readFileSync(filePath, "utf-8");
@@ -1672,7 +1827,7 @@ async function loadMarkdownFiles(patterns) {
1672
1827
  } catch {}
1673
1828
  }
1674
1829
  }
1675
- return parseMarkdownFiles(files);
1830
+ return parseMarkdownFiles2(files);
1676
1831
  }
1677
1832
  async function generateDiff(baseSpec, headSpec, options, config, log) {
1678
1833
  let markdownFiles;
@@ -1680,21 +1835,21 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1680
1835
  if (!docsPatterns || docsPatterns.length === 0) {
1681
1836
  if (config?.docs?.include) {
1682
1837
  docsPatterns = config.docs.include;
1683
- log(chalk4.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1838
+ log(chalk5.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1684
1839
  }
1685
1840
  }
1686
1841
  if (docsPatterns && docsPatterns.length > 0) {
1687
- markdownFiles = await loadMarkdownFiles(docsPatterns);
1842
+ markdownFiles = await loadMarkdownFiles2(docsPatterns);
1688
1843
  }
1689
1844
  return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
1690
1845
  }
1691
- function loadSpec(filePath, readFileSync2) {
1846
+ function loadSpec(filePath, readFileSync3) {
1692
1847
  const resolvedPath = path5.resolve(filePath);
1693
1848
  if (!fs3.existsSync(resolvedPath)) {
1694
1849
  throw new Error(`File not found: ${filePath}`);
1695
1850
  }
1696
1851
  try {
1697
- const content = readFileSync2(resolvedPath, "utf-8");
1852
+ const content = readFileSync3(resolvedPath, "utf-8");
1698
1853
  const spec = JSON.parse(content);
1699
1854
  return ensureSpecCoverage(spec);
1700
1855
  } catch (parseError) {
@@ -1703,60 +1858,40 @@ function loadSpec(filePath, readFileSync2) {
1703
1858
  }
1704
1859
  function printSummary(diff, baseName, headName, fromCache, log) {
1705
1860
  log("");
1706
- const cacheIndicator = fromCache ? chalk4.cyan(" (cached)") : "";
1707
- log(chalk4.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1861
+ const cacheIndicator = fromCache ? chalk5.cyan(" (cached)") : "";
1862
+ log(chalk5.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1708
1863
  log("─".repeat(40));
1709
1864
  log("");
1710
- const coverageColor = diff.coverageDelta > 0 ? chalk4.green : diff.coverageDelta < 0 ? chalk4.red : chalk4.gray;
1865
+ const coverageColor = diff.coverageDelta > 0 ? chalk5.green : diff.coverageDelta < 0 ? chalk5.red : chalk5.gray;
1711
1866
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1712
1867
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1713
1868
  const breakingCount = diff.breaking.length;
1714
1869
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1715
1870
  if (breakingCount > 0) {
1716
- const severityNote = highSeverity > 0 ? chalk4.red(` (${highSeverity} high severity)`) : "";
1717
- log(` Breaking: ${chalk4.red(breakingCount)} changes${severityNote}`);
1871
+ const severityNote = highSeverity > 0 ? chalk5.red(` (${highSeverity} high severity)`) : "";
1872
+ log(` Breaking: ${chalk5.red(breakingCount)} changes${severityNote}`);
1718
1873
  } else {
1719
- log(` Breaking: ${chalk4.green("0")} changes`);
1874
+ log(` Breaking: ${chalk5.green("0")} changes`);
1720
1875
  }
1721
1876
  const newCount = diff.nonBreaking.length;
1722
1877
  const undocCount = diff.newUndocumented.length;
1723
1878
  if (newCount > 0) {
1724
- const undocNote = undocCount > 0 ? chalk4.yellow(` (${undocCount} undocumented)`) : "";
1725
- log(` New: ${chalk4.green(newCount)} exports${undocNote}`);
1879
+ const undocNote = undocCount > 0 ? chalk5.yellow(` (${undocCount} undocumented)`) : "";
1880
+ log(` New: ${chalk5.green(newCount)} exports${undocNote}`);
1726
1881
  }
1727
1882
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1728
1883
  const parts = [];
1729
1884
  if (diff.driftIntroduced > 0)
1730
- parts.push(chalk4.red(`+${diff.driftIntroduced}`));
1885
+ parts.push(chalk5.red(`+${diff.driftIntroduced}`));
1731
1886
  if (diff.driftResolved > 0)
1732
- parts.push(chalk4.green(`-${diff.driftResolved}`));
1887
+ parts.push(chalk5.green(`-${diff.driftResolved}`));
1733
1888
  log(` Drift: ${parts.join(", ")}`);
1734
1889
  }
1890
+ const recommendation = recommendSemverBump(diff);
1891
+ const bumpColor = recommendation.bump === "major" ? chalk5.red : recommendation.bump === "minor" ? chalk5.yellow : chalk5.green;
1892
+ log(` Semver: ${bumpColor(recommendation.bump.toUpperCase())} (${recommendation.reason})`);
1735
1893
  log("");
1736
1894
  }
1737
- async function printAISummary(diff, log) {
1738
- if (!isAIDocsAnalysisAvailable()) {
1739
- log(chalk4.yellow(`
1740
- ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1741
- return;
1742
- }
1743
- if (!diff.docsImpact)
1744
- return;
1745
- log(chalk4.gray(`
1746
- Generating AI summary...`));
1747
- const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1748
- file: f.file,
1749
- exportName: r.exportName,
1750
- changeType: r.changeType,
1751
- context: r.context
1752
- })));
1753
- const summary = await generateImpactSummary(impacts);
1754
- if (summary) {
1755
- log("");
1756
- log(chalk4.bold("AI Summary"));
1757
- log(chalk4.cyan(` ${summary}`));
1758
- }
1759
- }
1760
1895
  function validateDiff(diff, headSpec, options) {
1761
1896
  const { minCoverage, maxDrift, checks } = options;
1762
1897
  const failures = [];
@@ -1835,7 +1970,7 @@ function printGitHubAnnotations(diff, log) {
1835
1970
 
1836
1971
  // src/commands/info.ts
1837
1972
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1838
- import chalk5 from "chalk";
1973
+ import chalk6 from "chalk";
1839
1974
  function registerInfoCommand(program) {
1840
1975
  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) => {
1841
1976
  try {
@@ -1857,14 +1992,14 @@ function registerInfoCommand(program) {
1857
1992
  const spec = enrichSpec2(specResult.spec);
1858
1993
  const stats = computeStats(spec);
1859
1994
  console.log("");
1860
- console.log(chalk5.bold(`${stats.packageName}@${stats.version}`));
1995
+ console.log(chalk6.bold(`${stats.packageName}@${stats.version}`));
1861
1996
  console.log("");
1862
- console.log(` Exports: ${chalk5.bold(stats.totalExports.toString())}`);
1863
- console.log(` Coverage: ${chalk5.bold(`${stats.coverageScore}%`)}`);
1864
- console.log(` Drift: ${chalk5.bold(`${stats.driftScore}%`)}`);
1997
+ console.log(` Exports: ${chalk6.bold(stats.totalExports.toString())}`);
1998
+ console.log(` Coverage: ${chalk6.bold(`${stats.coverageScore}%`)}`);
1999
+ console.log(` Drift: ${chalk6.bold(`${stats.driftScore}%`)}`);
1865
2000
  console.log("");
1866
2001
  } catch (err) {
1867
- console.error(chalk5.red("Error:"), err instanceof Error ? err.message : err);
2002
+ console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
1868
2003
  process.exit(1);
1869
2004
  }
1870
2005
  });
@@ -1873,53 +2008,65 @@ function registerInfoCommand(program) {
1873
2008
  // src/commands/init.ts
1874
2009
  import * as fs4 from "node:fs";
1875
2010
  import * as path6 from "node:path";
1876
- import chalk6 from "chalk";
2011
+ import chalk7 from "chalk";
1877
2012
  var defaultDependencies3 = {
1878
2013
  fileExists: fs4.existsSync,
1879
2014
  writeFileSync: fs4.writeFileSync,
1880
2015
  readFileSync: fs4.readFileSync,
2016
+ mkdirSync: fs4.mkdirSync,
1881
2017
  log: console.log,
1882
2018
  error: console.error
1883
2019
  };
1884
2020
  function registerInitCommand(program, dependencies = {}) {
1885
- const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync3, log, error } = {
2021
+ const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync4, mkdirSync: mkdirSync3, log, error } = {
1886
2022
  ...defaultDependencies3,
1887
2023
  ...dependencies
1888
2024
  };
1889
- program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs, yaml", "auto").action((options) => {
2025
+ 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) => {
1890
2026
  const cwd = path6.resolve(options.cwd);
1891
- const formatOption = String(options.format ?? "auto").toLowerCase();
1892
- if (!isValidFormat(formatOption)) {
1893
- error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
1894
- process.exitCode = 1;
1895
- return;
1896
- }
1897
2027
  const existing = findExistingConfig(cwd, fileExists2);
1898
2028
  if (existing) {
1899
- error(chalk6.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
2029
+ error(chalk7.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
1900
2030
  process.exitCode = 1;
1901
2031
  return;
1902
2032
  }
1903
- const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
1904
- const targetFormat = resolveFormat(formatOption, packageType);
1905
- if (targetFormat === "js" && packageType !== "module") {
1906
- log(chalk6.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1907
- }
1908
- const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
2033
+ const packageType = detectPackageType(cwd, fileExists2, readFileSync4);
2034
+ const targetFormat = packageType === "module" ? "ts" : "mts";
2035
+ const fileName = `doccov.config.${targetFormat}`;
1909
2036
  const outputPath = path6.join(cwd, fileName);
1910
2037
  if (fileExists2(outputPath)) {
1911
- error(chalk6.red(`Cannot create ${fileName}; file already exists.`));
2038
+ error(chalk7.red(`Cannot create ${fileName}; file already exists.`));
1912
2039
  process.exitCode = 1;
1913
2040
  return;
1914
2041
  }
1915
- const template = buildTemplate(targetFormat);
2042
+ const template = buildConfigTemplate();
1916
2043
  writeFileSync3(outputPath, template, { encoding: "utf8" });
1917
- log(chalk6.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
2044
+ log(chalk7.green(`✓ Created ${fileName}`));
2045
+ if (!options.skipAction) {
2046
+ const workflowDir = path6.join(cwd, ".github", "workflows");
2047
+ const workflowPath = path6.join(workflowDir, "doccov.yml");
2048
+ if (!fileExists2(workflowPath)) {
2049
+ mkdirSync3(workflowDir, { recursive: true });
2050
+ writeFileSync3(workflowPath, buildWorkflowTemplate(), { encoding: "utf8" });
2051
+ log(chalk7.green(`✓ Created .github/workflows/doccov.yml`));
2052
+ } else {
2053
+ log(chalk7.yellow(` Skipped .github/workflows/doccov.yml (already exists)`));
2054
+ }
2055
+ }
2056
+ const repoInfo = detectRepoInfo(cwd, fileExists2, readFileSync4);
2057
+ log("");
2058
+ log(chalk7.bold("Add this badge to your README:"));
2059
+ log("");
2060
+ if (repoInfo) {
2061
+ log(chalk7.cyan(`[![DocCov](https://doccov.dev/badge/${repoInfo.owner}/${repoInfo.repo})](https://doccov.dev/${repoInfo.owner}/${repoInfo.repo})`));
2062
+ } else {
2063
+ log(chalk7.cyan(`[![DocCov](https://doccov.dev/badge/OWNER/REPO)](https://doccov.dev/OWNER/REPO)`));
2064
+ log(chalk7.dim(" Replace OWNER/REPO with your GitHub repo"));
2065
+ }
2066
+ log("");
2067
+ log(chalk7.dim("Run `doccov check` to verify your documentation coverage"));
1918
2068
  });
1919
2069
  }
1920
- var isValidFormat = (value) => {
1921
- return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
1922
- };
1923
2070
  var findExistingConfig = (cwd, fileExists2) => {
1924
2071
  let current = path6.resolve(cwd);
1925
2072
  const { root } = path6.parse(current);
@@ -1937,13 +2084,13 @@ var findExistingConfig = (cwd, fileExists2) => {
1937
2084
  }
1938
2085
  return null;
1939
2086
  };
1940
- var detectPackageType = (cwd, fileExists2, readFileSync3) => {
2087
+ var detectPackageType = (cwd, fileExists2, readFileSync4) => {
1941
2088
  const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
1942
2089
  if (!packageJsonPath) {
1943
2090
  return;
1944
2091
  }
1945
2092
  try {
1946
- const raw = readFileSync3(packageJsonPath, "utf8");
2093
+ const raw = readFileSync4(packageJsonPath, "utf8");
1947
2094
  const parsed = JSON.parse(raw);
1948
2095
  if (parsed.type === "module") {
1949
2096
  return "module";
@@ -1969,121 +2116,85 @@ var findNearestPackageJson = (cwd, fileExists2) => {
1969
2116
  }
1970
2117
  return null;
1971
2118
  };
1972
- var resolveFormat = (format, packageType) => {
1973
- if (format === "yaml")
1974
- return "yaml";
1975
- if (format === "auto") {
1976
- return packageType === "module" ? "js" : "mjs";
1977
- }
1978
- return format;
1979
- };
1980
- var buildTemplate = (format) => {
1981
- if (format === "yaml") {
1982
- return `# doccov.yml
1983
- # include:
1984
- # - "MyClass"
1985
- # - "myFunction"
1986
- # exclude:
1987
- # - "internal*"
1988
-
1989
- check:
1990
- # minCoverage: 80
1991
- # maxDrift: 20
1992
- # examples: typecheck
2119
+ var buildConfigTemplate = () => {
2120
+ return `import { defineConfig } from '@doccov/cli/config';
1993
2121
 
1994
- quality:
1995
- rules:
1996
- # has-description: warn
1997
- # has-params: off
1998
- # has-returns: off
1999
- `;
2000
- }
2001
- const configBody = `{
2002
- // Filter which exports to analyze
2122
+ export default defineConfig({
2123
+ // Filter exports to analyze (optional)
2003
2124
  // include: ['MyClass', 'myFunction'],
2004
2125
  // exclude: ['internal*'],
2005
2126
 
2006
- // Check command thresholds
2007
2127
  check: {
2008
- // Minimum documentation coverage percentage (0-100)
2009
- // minCoverage: 80,
2128
+ // Fail if coverage drops below threshold
2129
+ minCoverage: 80,
2010
2130
 
2011
- // Maximum drift percentage allowed (0-100)
2131
+ // Fail if drift exceeds threshold
2012
2132
  // maxDrift: 20,
2013
-
2014
- // Example validation: 'presence' | 'typecheck' | 'run'
2015
- // examples: 'typecheck',
2016
2133
  },
2134
+ });
2135
+ `;
2136
+ };
2137
+ var buildWorkflowTemplate = () => {
2138
+ return `name: DocCov
2017
2139
 
2018
- // Quality rule severities: 'error' | 'warn' | 'off'
2019
- quality: {
2020
- rules: {
2021
- // 'has-description': 'warn',
2022
- // 'has-params': 'off',
2023
- // 'has-returns': 'off',
2024
- // 'has-examples': 'off',
2025
- // 'no-empty-returns': 'warn',
2026
- // 'consistent-param-style': 'off',
2027
- },
2028
- },
2029
- }`;
2030
- if (format === "cjs") {
2031
- return [
2032
- "const { defineConfig } = require('@doccov/cli/config');",
2033
- "",
2034
- `module.exports = defineConfig(${configBody});`,
2035
- ""
2036
- ].join(`
2037
- `);
2140
+ on:
2141
+ push:
2142
+ branches: [main, master]
2143
+ pull_request:
2144
+ branches: [main, master]
2145
+
2146
+ jobs:
2147
+ doccov:
2148
+ runs-on: ubuntu-latest
2149
+ steps:
2150
+ - uses: actions/checkout@v4
2151
+ - uses: doccov/action@v1
2152
+ with:
2153
+ min-coverage: 80
2154
+ comment-on-pr: true
2155
+ `;
2156
+ };
2157
+ var detectRepoInfo = (cwd, fileExists2, readFileSync4) => {
2158
+ const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
2159
+ if (packageJsonPath) {
2160
+ try {
2161
+ const raw = readFileSync4(packageJsonPath, "utf8");
2162
+ const parsed = JSON.parse(raw);
2163
+ let repoUrl;
2164
+ if (typeof parsed.repository === "string") {
2165
+ repoUrl = parsed.repository;
2166
+ } else if (parsed.repository?.url) {
2167
+ repoUrl = parsed.repository.url;
2168
+ }
2169
+ if (repoUrl) {
2170
+ const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
2171
+ if (match) {
2172
+ return { owner: match[1], repo: match[2] };
2173
+ }
2174
+ }
2175
+ } catch {}
2038
2176
  }
2039
- return [
2040
- "import { defineConfig } from '@doccov/cli/config';",
2041
- "",
2042
- `export default defineConfig(${configBody});`,
2043
- ""
2044
- ].join(`
2045
- `);
2177
+ const gitConfigPath = path6.join(cwd, ".git", "config");
2178
+ if (fileExists2(gitConfigPath)) {
2179
+ try {
2180
+ const config = readFileSync4(gitConfigPath, "utf8");
2181
+ const match = config.match(/url\s*=\s*.*github\.com[/:]([^/]+)\/([^/.]+)/);
2182
+ if (match) {
2183
+ return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
2184
+ }
2185
+ } catch {}
2186
+ }
2187
+ return null;
2046
2188
  };
2047
2189
 
2048
2190
  // src/commands/spec.ts
2049
2191
  import * as fs5 from "node:fs";
2050
2192
  import * as path7 from "node:path";
2051
- import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2193
+ import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2052
2194
  import { normalize, validateSpec } from "@openpkg-ts/spec";
2053
2195
  import chalk8 from "chalk";
2054
2196
  // package.json
2055
- var version = "0.16.0";
2056
-
2057
- // src/utils/filter-options.ts
2058
- import { mergeFilters, parseListFlag } from "@doccov/sdk";
2059
- import chalk7 from "chalk";
2060
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
2061
- var mergeFilterOptions = (config, cliOptions) => {
2062
- const messages = [];
2063
- if (config?.include) {
2064
- messages.push(formatList("include filters from config", config.include));
2065
- }
2066
- if (config?.exclude) {
2067
- messages.push(formatList("exclude filters from config", config.exclude));
2068
- }
2069
- if (cliOptions.include) {
2070
- messages.push(formatList("apply include filters from CLI", cliOptions.include));
2071
- }
2072
- if (cliOptions.exclude) {
2073
- messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
2074
- }
2075
- const resolved = mergeFilters(config, cliOptions);
2076
- if (!resolved.include && !resolved.exclude) {
2077
- return { messages };
2078
- }
2079
- const source = resolved.source === "override" ? "cli" : resolved.source;
2080
- return {
2081
- include: resolved.include,
2082
- exclude: resolved.exclude,
2083
- source,
2084
- messages
2085
- };
2086
- };
2197
+ var version = "0.18.0";
2087
2198
 
2088
2199
  // src/commands/spec.ts
2089
2200
  var defaultDependencies4 = {
@@ -2107,7 +2218,7 @@ function registerSpecCommand(program, dependencies = {}) {
2107
2218
  ...defaultDependencies4,
2108
2219
  ...dependencies
2109
2220
  };
2110
- program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
2221
+ program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("-f, --format <format>", "Output format: json (default) or api-surface", "json").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) => {
2111
2222
  try {
2112
2223
  const steps = new StepProgress([
2113
2224
  { label: "Resolved target", activeLabel: "Resolving target" },
@@ -2135,7 +2246,8 @@ function registerSpecCommand(program, dependencies = {}) {
2135
2246
  steps.next();
2136
2247
  const cliFilters = {
2137
2248
  include: parseListFlag(options.include),
2138
- exclude: parseListFlag(options.exclude)
2249
+ exclude: parseListFlag(options.exclude),
2250
+ visibility: parseVisibilityFlag(options.visibility)
2139
2251
  };
2140
2252
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
2141
2253
  const resolveExternalTypes = !options.skipResolve;
@@ -2143,7 +2255,8 @@ function registerSpecCommand(program, dependencies = {}) {
2143
2255
  resolveExternalTypes,
2144
2256
  maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
2145
2257
  useCache: options.cache !== false,
2146
- cwd: options.cwd
2258
+ cwd: options.cwd,
2259
+ schemaExtraction: options.runtime ? "hybrid" : "static"
2147
2260
  });
2148
2261
  const generationInput = {
2149
2262
  entryPoint: path7.relative(targetDir, entryFile),
@@ -2155,10 +2268,11 @@ function registerSpecCommand(program, dependencies = {}) {
2155
2268
  isMonorepo: resolved.isMonorepo,
2156
2269
  targetPackage: packageInfo?.name
2157
2270
  };
2158
- const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
2271
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
2159
2272
  filters: {
2160
2273
  include: resolvedFilters.include,
2161
- exclude: resolvedFilters.exclude
2274
+ exclude: resolvedFilters.exclude,
2275
+ visibility: resolvedFilters.visibility
2162
2276
  },
2163
2277
  generationInput
2164
2278
  } : { generationInput };
@@ -2177,9 +2291,16 @@ function registerSpecCommand(program, dependencies = {}) {
2177
2291
  process.exit(1);
2178
2292
  }
2179
2293
  steps.next();
2180
- const outputPath = path7.resolve(process.cwd(), options.output);
2181
- writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2182
- steps.complete(`Generated ${options.output}`);
2294
+ const format = options.format ?? "json";
2295
+ const outputPath = path7.resolve(options.cwd, options.output);
2296
+ if (format === "api-surface") {
2297
+ const apiSurface = renderApiSurface(normalized);
2298
+ writeFileSync4(outputPath, apiSurface);
2299
+ steps.complete(`Generated ${options.output} (API surface)`);
2300
+ } else {
2301
+ writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2302
+ steps.complete(`Generated ${options.output}`);
2303
+ }
2183
2304
  log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2184
2305
  log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
2185
2306
  if (options.verbose && normalized.generation) {
@@ -2195,6 +2316,13 @@ function registerSpecCommand(program, dependencies = {}) {
2195
2316
  if (gen.analysis.maxTypeDepth) {
2196
2317
  log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2197
2318
  }
2319
+ if (gen.analysis.schemaExtraction) {
2320
+ const se = gen.analysis.schemaExtraction;
2321
+ log(chalk8.gray(` Schema extraction: ${se.method}`));
2322
+ if (se.runtimeCount) {
2323
+ log(chalk8.gray(` Runtime schemas: ${se.runtimeCount} (${se.vendors?.join(", ")})`));
2324
+ }
2325
+ }
2198
2326
  log("");
2199
2327
  log(chalk8.bold("Environment"));
2200
2328
  log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
@@ -2234,10 +2362,195 @@ function registerSpecCommand(program, dependencies = {}) {
2234
2362
  });
2235
2363
  }
2236
2364
 
2365
+ // src/commands/trends.ts
2366
+ import * as fs6 from "node:fs";
2367
+ import * as path8 from "node:path";
2368
+ import {
2369
+ formatDelta as formatDelta2,
2370
+ getExtendedTrend,
2371
+ getTrend,
2372
+ loadSnapshots,
2373
+ pruneByTier,
2374
+ pruneHistory,
2375
+ renderSparkline,
2376
+ RETENTION_DAYS,
2377
+ saveSnapshot
2378
+ } from "@doccov/sdk";
2379
+ import chalk9 from "chalk";
2380
+ function formatDate(timestamp) {
2381
+ const date = new Date(timestamp);
2382
+ return date.toLocaleDateString("en-US", {
2383
+ month: "short",
2384
+ day: "numeric",
2385
+ year: "numeric",
2386
+ hour: "2-digit",
2387
+ minute: "2-digit"
2388
+ });
2389
+ }
2390
+ function getColorForScore(score) {
2391
+ if (score >= 90)
2392
+ return chalk9.green;
2393
+ if (score >= 70)
2394
+ return chalk9.yellow;
2395
+ if (score >= 50)
2396
+ return chalk9.hex("#FFA500");
2397
+ return chalk9.red;
2398
+ }
2399
+ function formatSnapshot(snapshot) {
2400
+ const color = getColorForScore(snapshot.coverageScore);
2401
+ const date = formatDate(snapshot.timestamp);
2402
+ const version2 = snapshot.version ? ` v${snapshot.version}` : "";
2403
+ const commit = snapshot.commit ? chalk9.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
2404
+ return `${chalk9.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
2405
+ }
2406
+ function formatWeekDate(timestamp) {
2407
+ const date = new Date(timestamp);
2408
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
2409
+ }
2410
+ function formatVelocity(velocity) {
2411
+ if (velocity > 0)
2412
+ return chalk9.green(`+${velocity}%/day`);
2413
+ if (velocity < 0)
2414
+ return chalk9.red(`${velocity}%/day`);
2415
+ return chalk9.gray("0%/day");
2416
+ }
2417
+ function registerTrendsCommand(program) {
2418
+ 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) => {
2419
+ const cwd = path8.resolve(options.cwd);
2420
+ const tier = options.tier ?? "pro";
2421
+ if (options.prune) {
2422
+ const keepCount = parseInt(options.prune, 10);
2423
+ if (!isNaN(keepCount)) {
2424
+ const deleted = pruneHistory(cwd, keepCount);
2425
+ console.log(chalk9.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2426
+ } else {
2427
+ const deleted = pruneByTier(cwd, tier);
2428
+ console.log(chalk9.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2429
+ }
2430
+ return;
2431
+ }
2432
+ if (options.record) {
2433
+ const specPath = path8.resolve(cwd, "openpkg.json");
2434
+ if (!fs6.existsSync(specPath)) {
2435
+ console.error(chalk9.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2436
+ process.exit(1);
2437
+ }
2438
+ try {
2439
+ const specContent = fs6.readFileSync(specPath, "utf-8");
2440
+ const spec = JSON.parse(specContent);
2441
+ const trend = getTrend(spec, cwd);
2442
+ saveSnapshot(trend.current, cwd);
2443
+ console.log(chalk9.green("Recorded coverage snapshot:"));
2444
+ console.log(formatSnapshot(trend.current));
2445
+ if (trend.delta !== undefined) {
2446
+ const deltaStr = formatDelta2(trend.delta);
2447
+ const deltaColor = trend.delta > 0 ? chalk9.green : trend.delta < 0 ? chalk9.red : chalk9.gray;
2448
+ console.log(chalk9.gray("Change from previous:"), deltaColor(deltaStr));
2449
+ }
2450
+ return;
2451
+ } catch (error) {
2452
+ console.error(chalk9.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2453
+ process.exit(1);
2454
+ }
2455
+ }
2456
+ const snapshots = loadSnapshots(cwd);
2457
+ const limit = parseInt(options.limit ?? "10", 10);
2458
+ if (snapshots.length === 0) {
2459
+ console.log(chalk9.yellow("No coverage history found."));
2460
+ console.log(chalk9.gray("Run `doccov trends --record` to save the current coverage."));
2461
+ return;
2462
+ }
2463
+ if (options.json) {
2464
+ const output = {
2465
+ current: snapshots[0],
2466
+ history: snapshots.slice(1),
2467
+ delta: snapshots.length > 1 ? snapshots[0].coverageScore - snapshots[1].coverageScore : undefined,
2468
+ sparkline: snapshots.slice(0, 10).map((s) => s.coverageScore).reverse()
2469
+ };
2470
+ console.log(JSON.stringify(output, null, 2));
2471
+ return;
2472
+ }
2473
+ const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
2474
+ const sparkline = renderSparkline(sparklineData);
2475
+ console.log(chalk9.bold("Coverage Trends"));
2476
+ console.log(chalk9.gray(`Package: ${snapshots[0].package}`));
2477
+ console.log(chalk9.gray(`Sparkline: ${sparkline}`));
2478
+ console.log("");
2479
+ if (snapshots.length >= 2) {
2480
+ const oldest = snapshots[snapshots.length - 1];
2481
+ const newest = snapshots[0];
2482
+ const overallDelta = newest.coverageScore - oldest.coverageScore;
2483
+ const deltaStr = formatDelta2(overallDelta);
2484
+ const deltaColor = overallDelta > 0 ? chalk9.green : overallDelta < 0 ? chalk9.red : chalk9.gray;
2485
+ console.log(chalk9.gray("Overall trend:"), deltaColor(deltaStr), chalk9.gray(`(${snapshots.length} snapshots)`));
2486
+ console.log("");
2487
+ }
2488
+ if (options.extended) {
2489
+ const specPath = path8.resolve(cwd, "openpkg.json");
2490
+ if (fs6.existsSync(specPath)) {
2491
+ try {
2492
+ const specContent = fs6.readFileSync(specPath, "utf-8");
2493
+ const spec = JSON.parse(specContent);
2494
+ const extended = getExtendedTrend(spec, cwd, { tier });
2495
+ console.log(chalk9.bold("Extended Analysis"));
2496
+ console.log(chalk9.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
2497
+ console.log("");
2498
+ console.log(" Velocity:");
2499
+ console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
2500
+ console.log(` 30-day: ${formatVelocity(extended.velocity30d)}`);
2501
+ if (extended.velocity90d !== undefined) {
2502
+ console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
2503
+ }
2504
+ console.log("");
2505
+ const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk9.green : chalk9.red;
2506
+ console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
2507
+ console.log(` All-time high: ${chalk9.green(`${extended.allTimeHigh}%`)}`);
2508
+ console.log(` All-time low: ${chalk9.red(`${extended.allTimeLow}%`)}`);
2509
+ if (extended.dataRange) {
2510
+ const startDate = formatWeekDate(extended.dataRange.start);
2511
+ const endDate = formatWeekDate(extended.dataRange.end);
2512
+ console.log(chalk9.gray(` Data range: ${startDate} - ${endDate}`));
2513
+ }
2514
+ console.log("");
2515
+ if (options.weekly && extended.weeklySummaries.length > 0) {
2516
+ console.log(chalk9.bold("Weekly Summary"));
2517
+ const weekLimit = Math.min(extended.weeklySummaries.length, 8);
2518
+ for (let i = 0;i < weekLimit; i++) {
2519
+ const week = extended.weeklySummaries[i];
2520
+ const weekStart = formatWeekDate(week.weekStart);
2521
+ const weekEnd = formatWeekDate(week.weekEnd);
2522
+ const deltaColor = week.delta > 0 ? chalk9.green : week.delta < 0 ? chalk9.red : chalk9.gray;
2523
+ const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
2524
+ console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
2525
+ }
2526
+ if (extended.weeklySummaries.length > weekLimit) {
2527
+ console.log(chalk9.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2528
+ }
2529
+ console.log("");
2530
+ }
2531
+ } catch {
2532
+ console.log(chalk9.yellow("Could not load openpkg.json for extended analysis"));
2533
+ console.log("");
2534
+ }
2535
+ }
2536
+ }
2537
+ console.log(chalk9.bold("History"));
2538
+ const displaySnapshots = snapshots.slice(0, limit);
2539
+ for (let i = 0;i < displaySnapshots.length; i++) {
2540
+ const snapshot = displaySnapshots[i];
2541
+ const prefix = i === 0 ? chalk9.cyan("→") : " ";
2542
+ console.log(`${prefix} ${formatSnapshot(snapshot)}`);
2543
+ }
2544
+ if (snapshots.length > limit) {
2545
+ console.log(chalk9.gray(` ... and ${snapshots.length - limit} more`));
2546
+ }
2547
+ });
2548
+ }
2549
+
2237
2550
  // src/cli.ts
2238
2551
  var __filename2 = fileURLToPath(import.meta.url);
2239
- var __dirname2 = path8.dirname(__filename2);
2240
- var packageJson = JSON.parse(readFileSync3(path8.join(__dirname2, "../package.json"), "utf-8"));
2552
+ var __dirname2 = path9.dirname(__filename2);
2553
+ var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
2241
2554
  var program = new Command;
2242
2555
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2243
2556
  registerCheckCommand(program);
@@ -2245,6 +2558,7 @@ registerInfoCommand(program);
2245
2558
  registerSpecCommand(program);
2246
2559
  registerDiffCommand(program);
2247
2560
  registerInitCommand(program);
2561
+ registerTrendsCommand(program);
2248
2562
  program.command("*", { hidden: true }).action(() => {
2249
2563
  program.outputHelp();
2250
2564
  });