@doccov/cli 0.18.0 → 0.20.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,23 +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
- var policyConfigSchema = z.object({
39
- path: z.string().min(1),
40
- minCoverage: z.number().min(0).max(100).optional(),
41
- maxDrift: z.number().min(0).max(100).optional(),
42
- requireExamples: z.boolean().optional()
43
- });
44
33
  var docCovConfigSchema = z.object({
45
34
  include: stringList.optional(),
46
35
  exclude: stringList.optional(),
47
36
  plugins: z.array(z.unknown()).optional(),
48
37
  docs: docsConfigSchema.optional(),
49
- check: checkConfigSchema.optional(),
50
- quality: qualityConfigSchema.optional(),
51
- policies: z.array(policyConfigSchema).optional()
38
+ check: checkConfigSchema.optional()
52
39
  });
53
40
  var normalizeList = (value) => {
54
41
  if (!value) {
@@ -80,21 +67,12 @@ var normalizeConfig = (input) => {
80
67
  maxDrift: input.check.maxDrift
81
68
  };
82
69
  }
83
- let quality;
84
- if (input.quality) {
85
- quality = {
86
- rules: input.quality.rules
87
- };
88
- }
89
- const policies = input.policies;
90
70
  return {
91
71
  include,
92
72
  exclude,
93
73
  plugins: input.plugins,
94
74
  docs,
95
- check,
96
- quality,
97
- policies
75
+ check
98
76
  };
99
77
  };
100
78
 
@@ -102,12 +80,8 @@ var normalizeConfig = (input) => {
102
80
  var DOCCOV_CONFIG_FILENAMES = [
103
81
  "doccov.config.ts",
104
82
  "doccov.config.mts",
105
- "doccov.config.cts",
106
83
  "doccov.config.js",
107
- "doccov.config.mjs",
108
- "doccov.config.cjs",
109
- "doccov.yml",
110
- "doccov.yaml"
84
+ "doccov.config.mjs"
111
85
  ];
112
86
  var fileExists = async (filePath) => {
113
87
  try {
@@ -134,11 +108,6 @@ var findConfigFile = async (cwd) => {
134
108
  }
135
109
  };
136
110
  var importConfigModule = async (absolutePath) => {
137
- const ext = path.extname(absolutePath);
138
- if (ext === ".yml" || ext === ".yaml") {
139
- const content = await readFile(absolutePath, "utf-8");
140
- return parseYaml(content);
141
- }
142
111
  const fileUrl = pathToFileURL(absolutePath);
143
112
  fileUrl.searchParams.set("t", Date.now().toString());
144
113
  const module = await import(fileUrl.href);
@@ -177,7 +146,7 @@ ${formatIssues(issues)}`);
177
146
  // src/config/index.ts
178
147
  var defineConfig = (config) => config;
179
148
  // src/cli.ts
180
- import { readFileSync as readFileSync4 } from "node:fs";
149
+ import { readFileSync as readFileSync5 } from "node:fs";
181
150
  import * as path9 from "node:path";
182
151
  import { fileURLToPath } from "node:url";
183
152
  import { Command } from "commander";
@@ -186,14 +155,11 @@ import { Command } from "commander";
186
155
  import * as fs2 from "node:fs";
187
156
  import * as path4 from "node:path";
188
157
  import {
189
- analyzeSpecContributors,
190
- analyzeSpecOwnership,
191
158
  applyEdits,
192
159
  categorizeDrifts,
193
160
  createSourceFile,
194
161
  DocCov,
195
162
  enrichSpec,
196
- evaluatePolicies,
197
163
  findJSDocLocation,
198
164
  generateFixesForExport,
199
165
  generateReport,
@@ -201,6 +167,7 @@ import {
201
167
  NodeFileSystem,
202
168
  parseExamplesFlag,
203
169
  parseJSDocToPatch,
170
+ parseMarkdownFiles,
204
171
  resolveTarget,
205
172
  serializeJSDoc,
206
173
  validateExamples
@@ -208,7 +175,8 @@ import {
208
175
  import {
209
176
  DRIFT_CATEGORIES as DRIFT_CATEGORIES2
210
177
  } from "@openpkg-ts/spec";
211
- import chalk5 from "chalk";
178
+ import chalk4 from "chalk";
179
+ import { glob } from "glob";
212
180
 
213
181
  // src/reports/changelog-renderer.ts
214
182
  function renderChangelog(data, options = {}) {
@@ -649,7 +617,8 @@ function renderPRComment(data, opts = {}) {
649
617
  const { diff, headSpec } = data;
650
618
  const limit = opts.limit ?? 10;
651
619
  const lines = [];
652
- 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;
653
622
  const statusIcon = hasIssues ? diff.coverageDelta < 0 ? "❌" : "⚠️" : "✅";
654
623
  lines.push(`## ${statusIcon} DocCov — Documentation Coverage`);
655
624
  lines.push("");
@@ -661,6 +630,13 @@ function renderPRComment(data, opts = {}) {
661
630
  if (diff.driftIntroduced > 0) {
662
631
  lines.push(`**Doc drift issues:** ${diff.driftIntroduced}`);
663
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
+ }
664
640
  if (diff.newUndocumented.length > 0) {
665
641
  lines.push("");
666
642
  lines.push("### Undocumented exports in this PR");
@@ -673,7 +649,15 @@ function renderPRComment(data, opts = {}) {
673
649
  lines.push("");
674
650
  renderDriftIssues(lines, diff.newUndocumented, headSpec, opts, limit);
675
651
  }
676
- 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);
677
661
  if (fixGuidance) {
678
662
  lines.push("");
679
663
  lines.push("### How to fix");
@@ -687,6 +671,16 @@ function renderPRComment(data, opts = {}) {
687
671
  renderDetailsTable(lines, diff);
688
672
  lines.push("");
689
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
+ }
690
684
  return lines.join(`
691
685
  `);
692
686
  }
@@ -807,15 +801,47 @@ function getMissingSignals(exp) {
807
801
  }
808
802
  return missing;
809
803
  }
810
- 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) {
811
830
  const sections = [];
812
831
  if (diff.newUndocumented.length > 0) {
813
832
  sections.push(`**For undocumented exports:**
814
833
  ` + "Add JSDoc/TSDoc blocks with description, `@param`, and `@returns` tags.");
815
834
  }
816
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).` : "";
817
839
  sections.push(`**For doc drift:**
818
- ` + "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.");
819
845
  }
820
846
  if (diff.breaking.length > 0) {
821
847
  sections.push(`**For breaking changes:**
@@ -978,275 +1004,9 @@ function writeReports(options) {
978
1004
  }));
979
1005
  return results;
980
1006
  }
981
- // src/utils/ai-client.ts
982
- import chalk2 from "chalk";
983
-
984
- // src/utils/ai-generate.ts
985
- import { createAnthropic } from "@ai-sdk/anthropic";
986
- import { createOpenAI } from "@ai-sdk/openai";
987
- import { generateObject } from "ai";
988
- import { z as z2 } from "zod";
989
- var JSDocGenerationSchema = z2.object({
990
- description: z2.string().describe("1-2 sentence description of what this does"),
991
- params: z2.array(z2.object({
992
- name: z2.string(),
993
- type: z2.string().optional(),
994
- description: z2.string()
995
- })).optional().describe("Parameter descriptions for functions"),
996
- returns: z2.object({
997
- type: z2.string().optional(),
998
- description: z2.string()
999
- }).optional().describe("Return value description for functions"),
1000
- example: z2.string().optional().describe("Working code example showing typical usage"),
1001
- typeParams: z2.array(z2.object({
1002
- name: z2.string(),
1003
- description: z2.string()
1004
- })).optional().describe("Type parameter descriptions for generics")
1005
- });
1006
- function getModel() {
1007
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1008
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1009
- const anthropic = createAnthropic();
1010
- return anthropic("claude-sonnet-4-20250514");
1011
- }
1012
- const openai = createOpenAI();
1013
- return openai("gpt-4o-mini");
1014
- }
1015
- function isAIGenerationAvailable() {
1016
- return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
1017
- }
1018
- function buildSignature(exp) {
1019
- if (exp.signatures && exp.signatures.length > 0) {
1020
- const sig = exp.signatures[0];
1021
- const params = sig.parameters?.map((p) => `${p.name}: ${p.type ?? "unknown"}`).join(", ") ?? "";
1022
- const ret = sig.returnType ?? "void";
1023
- return `(${params}) => ${ret}`;
1024
- }
1025
- if (exp.type) {
1026
- return typeof exp.type === "string" ? exp.type : JSON.stringify(exp.type);
1027
- }
1028
- return exp.kind;
1029
- }
1030
- function buildMembersContext(exp) {
1031
- if (!exp.members || exp.members.length === 0)
1032
- return "";
1033
- const memberLines = exp.members.slice(0, 10).map((m) => {
1034
- const type = m.type ? `: ${typeof m.type === "string" ? m.type : "object"}` : "";
1035
- return ` - ${m.name}${type}`;
1036
- });
1037
- if (exp.members.length > 10) {
1038
- memberLines.push(` - ... and ${exp.members.length - 10} more members`);
1039
- }
1040
- return `
1041
-
1042
- Members:
1043
- ${memberLines.join(`
1044
- `)}`;
1045
- }
1046
- async function generateJSDocForExport(exp) {
1047
- if (!isAIGenerationAvailable()) {
1048
- return null;
1049
- }
1050
- const signature = buildSignature(exp);
1051
- const membersContext = buildMembersContext(exp);
1052
- const prompt = `Generate JSDoc documentation for this TypeScript export.
1053
-
1054
- Name: ${exp.name}
1055
- Kind: ${exp.kind}
1056
- Signature: ${signature}${membersContext}
1057
-
1058
- Requirements:
1059
- - Description: 1-2 sentences explaining what this does and when to use it
1060
- - For functions: describe each parameter and return value
1061
- - Example: provide a working code snippet showing typical usage
1062
- - Be concise but informative
1063
-
1064
- The documentation should help developers understand the purpose and usage at a glance.`;
1065
- try {
1066
- const { object } = await generateObject({
1067
- model: getModel(),
1068
- schema: JSDocGenerationSchema,
1069
- prompt
1070
- });
1071
- return object;
1072
- } catch {
1073
- return null;
1074
- }
1075
- }
1076
- function toJSDocPatch(result) {
1077
- const patch = {};
1078
- if (result.description) {
1079
- patch.description = result.description;
1080
- }
1081
- if (result.params && result.params.length > 0) {
1082
- patch.params = result.params.map((p) => ({
1083
- name: p.name,
1084
- type: p.type,
1085
- description: p.description
1086
- }));
1087
- }
1088
- if (result.returns) {
1089
- patch.returns = {
1090
- type: result.returns.type,
1091
- description: result.returns.description
1092
- };
1093
- }
1094
- if (result.example) {
1095
- patch.examples = [result.example];
1096
- }
1097
- if (result.typeParams && result.typeParams.length > 0) {
1098
- patch.typeParams = result.typeParams.map((tp) => ({
1099
- name: tp.name,
1100
- description: tp.description
1101
- }));
1102
- }
1103
- return patch;
1104
- }
1105
- async function batchGenerateJSDocs(exports, options = {}) {
1106
- const results = [];
1107
- const { maxConcurrent = 3, onProgress } = options;
1108
- for (let i = 0;i < exports.length; i += maxConcurrent) {
1109
- const batch = exports.slice(i, i + maxConcurrent);
1110
- const promises = batch.map(async (exp) => {
1111
- const generated = await generateJSDocForExport(exp);
1112
- if (generated) {
1113
- onProgress?.(results.length + 1, exports.length, exp.name);
1114
- return {
1115
- exportName: exp.name,
1116
- patch: toJSDocPatch(generated),
1117
- generated: true
1118
- };
1119
- }
1120
- return {
1121
- exportName: exp.name,
1122
- patch: {},
1123
- generated: false
1124
- };
1125
- });
1126
- const batchResults = await Promise.all(promises);
1127
- results.push(...batchResults);
1128
- }
1129
- return results;
1130
- }
1131
-
1132
- // src/utils/ai-client.ts
1133
- var API_BASE = process.env.DOCCOV_API_URL || "https://api.doccov.com";
1134
- function isHostedAPIAvailable() {
1135
- return Boolean(process.env.DOCCOV_API_KEY);
1136
- }
1137
- async function generateViaHostedAPI(exports, packageName) {
1138
- const apiKey = process.env.DOCCOV_API_KEY;
1139
- if (!apiKey) {
1140
- return { error: "No DOCCOV_API_KEY set" };
1141
- }
1142
- try {
1143
- const response = await fetch(`${API_BASE}/v1/ai/generate`, {
1144
- method: "POST",
1145
- headers: {
1146
- Authorization: `Bearer ${apiKey}`,
1147
- "Content-Type": "application/json"
1148
- },
1149
- body: JSON.stringify({
1150
- exports: exports.map((exp) => ({
1151
- name: exp.name,
1152
- kind: exp.kind,
1153
- signature: buildSignature2(exp),
1154
- members: exp.members?.slice(0, 10).map((m) => ({
1155
- name: m.name,
1156
- type: typeof m.type === "string" ? m.type : undefined
1157
- }))
1158
- })),
1159
- packageName
1160
- })
1161
- });
1162
- if (response.status === 429) {
1163
- const data = await response.json();
1164
- return {
1165
- error: data.error || "Monthly AI limit reached",
1166
- quotaExceeded: true
1167
- };
1168
- }
1169
- if (!response.ok) {
1170
- const data = await response.json();
1171
- return { error: data.error || "API request failed" };
1172
- }
1173
- return await response.json();
1174
- } catch (err) {
1175
- return { error: err instanceof Error ? err.message : "Network error" };
1176
- }
1177
- }
1178
- function buildSignature2(exp) {
1179
- if (exp.signatures && exp.signatures.length > 0) {
1180
- const sig = exp.signatures[0];
1181
- const params = sig.parameters?.map((p) => `${p.name}: ${p.type ?? "unknown"}`).join(", ") ?? "";
1182
- const ret = sig.returnType ?? "void";
1183
- return `(${params}) => ${ret}`;
1184
- }
1185
- if (exp.type) {
1186
- return typeof exp.type === "string" ? exp.type : JSON.stringify(exp.type);
1187
- }
1188
- return exp.kind;
1189
- }
1190
- async function generateWithFallback(exports, options = {}) {
1191
- const { log = console.log } = options;
1192
- if (isHostedAPIAvailable()) {
1193
- log(chalk2.dim("Using DocCov hosted AI..."));
1194
- const hostedResult = await generateViaHostedAPI(exports, options.packageName);
1195
- if ("error" in hostedResult) {
1196
- if (hostedResult.quotaExceeded) {
1197
- log(chalk2.yellow(`
1198
- ⚠ ${hostedResult.error}`));
1199
- log(chalk2.dim("Falling back to local API keys..."));
1200
- if (isAIGenerationAvailable()) {
1201
- return await generateViaBYOK(exports, options);
1202
- }
1203
- log(chalk2.red("No local API keys configured."));
1204
- log(chalk2.dim("Set OPENAI_API_KEY or ANTHROPIC_API_KEY for unlimited generation."));
1205
- return { results: [], source: "byok" };
1206
- }
1207
- log(chalk2.yellow(`
1208
- ⚠ Hosted API error: ${hostedResult.error}`));
1209
- log(chalk2.dim("Falling back to local API keys..."));
1210
- } else {
1211
- const results = hostedResult.results.map((r) => ({
1212
- exportName: r.name,
1213
- patch: r.patch ?? {},
1214
- generated: r.patch !== null
1215
- }));
1216
- return {
1217
- results,
1218
- source: "hosted",
1219
- quotaRemaining: hostedResult.quota.remaining,
1220
- quotaResetAt: hostedResult.quota.resetAt
1221
- };
1222
- }
1223
- }
1224
- if (isAIGenerationAvailable()) {
1225
- return await generateViaBYOK(exports, options);
1226
- }
1227
- log(chalk2.yellow(`
1228
- ⚠ No AI configuration available.`));
1229
- log(chalk2.dim("Options:"));
1230
- log(chalk2.dim(" 1. Set DOCCOV_API_KEY for hosted AI (included with Team/Pro plan)"));
1231
- log(chalk2.dim(" 2. Set OPENAI_API_KEY or ANTHROPIC_API_KEY for direct API access"));
1232
- return { results: [], source: "byok" };
1233
- }
1234
- async function generateViaBYOK(exports, options) {
1235
- const { log = console.log } = options;
1236
- log(chalk2.dim("Using local API keys (BYOK)..."));
1237
- const results = await batchGenerateJSDocs(exports, {
1238
- maxConcurrent: options.maxConcurrent ?? 3,
1239
- onProgress: options.onProgress
1240
- });
1241
- return {
1242
- results,
1243
- source: "byok"
1244
- };
1245
- }
1246
-
1247
1007
  // src/utils/filter-options.ts
1248
1008
  import { mergeFilters, parseListFlag } from "@doccov/sdk";
1249
- import chalk3 from "chalk";
1009
+ import chalk2 from "chalk";
1250
1010
  var parseVisibilityFlag = (value) => {
1251
1011
  if (!value)
1252
1012
  return;
@@ -1263,7 +1023,7 @@ var parseVisibilityFlag = (value) => {
1263
1023
  }
1264
1024
  return result.length > 0 ? result : undefined;
1265
1025
  };
1266
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
1026
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk2.cyan(value)).join(", ")}`;
1267
1027
  var mergeFilterOptions = (config, cliOptions) => {
1268
1028
  const messages = [];
1269
1029
  if (config?.include) {
@@ -1295,76 +1055,8 @@ var mergeFilterOptions = (config, cliOptions) => {
1295
1055
  };
1296
1056
  };
1297
1057
 
1298
- // src/utils/llm-assertion-parser.ts
1299
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1300
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1301
- import { generateObject as generateObject2 } from "ai";
1302
- import { z as z3 } from "zod";
1303
- var AssertionParseSchema = z3.object({
1304
- assertions: z3.array(z3.object({
1305
- lineNumber: z3.number().describe("1-indexed line number where the assertion appears"),
1306
- expected: z3.string().describe("The expected output value"),
1307
- originalComment: z3.string().describe("The original comment text"),
1308
- suggestedSyntax: z3.string().describe("The line rewritten with standard // => value syntax")
1309
- })).describe("List of assertion-like comments found in the code"),
1310
- hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
1311
- });
1312
- var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
1313
-
1314
- Look for comments that appear to specify expected output values, such as:
1315
- - "// should be 3"
1316
- - "// returns 5"
1317
- - "// outputs: hello"
1318
- - "// expected: [1, 2, 3]"
1319
- - "// 42" (bare value after console.log)
1320
- - "// result: true"
1321
-
1322
- Do NOT include:
1323
- - Regular code comments that explain what the code does
1324
- - Comments that are instructions or documentation
1325
- - Comments with // => (already using standard syntax)
1326
-
1327
- For each assertion found, extract:
1328
- 1. The line number (1-indexed)
1329
- 2. The expected value (just the value, not the comment prefix)
1330
- 3. The original comment text
1331
- 4. A suggested rewrite of the ENTIRE line using "// => value" syntax
1332
-
1333
- Code:
1334
- \`\`\`
1335
- ${code}
1336
- \`\`\``;
1337
- function getModel2() {
1338
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1339
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1340
- const anthropic = createAnthropic2();
1341
- return anthropic("claude-sonnet-4-20250514");
1342
- }
1343
- const openai = createOpenAI2();
1344
- return openai("gpt-4o-mini");
1345
- }
1346
- function isLLMAssertionParsingAvailable() {
1347
- return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
1348
- }
1349
- async function parseAssertionsWithLLM(code) {
1350
- if (!isLLMAssertionParsingAvailable()) {
1351
- return null;
1352
- }
1353
- try {
1354
- const model = getModel2();
1355
- const { object } = await generateObject2({
1356
- model,
1357
- schema: AssertionParseSchema,
1358
- prompt: ASSERTION_PARSE_PROMPT(code)
1359
- });
1360
- return object;
1361
- } catch {
1362
- return null;
1363
- }
1364
- }
1365
-
1366
1058
  // src/utils/progress.ts
1367
- import chalk4 from "chalk";
1059
+ import chalk3 from "chalk";
1368
1060
  class StepProgress {
1369
1061
  steps;
1370
1062
  currentStep = 0;
@@ -1392,7 +1084,7 @@ class StepProgress {
1392
1084
  this.completeCurrentStep();
1393
1085
  if (message) {
1394
1086
  const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
1395
- console.log(`${chalk4.green("✓")} ${message} ${chalk4.dim(`(${elapsed}s)`)}`);
1087
+ console.log(`${chalk3.green("✓")} ${message} ${chalk3.dim(`(${elapsed}s)`)}`);
1396
1088
  }
1397
1089
  }
1398
1090
  render() {
@@ -1400,17 +1092,17 @@ class StepProgress {
1400
1092
  if (!step)
1401
1093
  return;
1402
1094
  const label = step.activeLabel ?? step.label;
1403
- const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1404
- process.stdout.write(`\r${prefix} ${chalk4.cyan(label)}...`);
1095
+ const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1096
+ process.stdout.write(`\r${prefix} ${chalk3.cyan(label)}...`);
1405
1097
  }
1406
1098
  completeCurrentStep() {
1407
1099
  const step = this.steps[this.currentStep];
1408
1100
  if (!step)
1409
1101
  return;
1410
1102
  const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
1411
- const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1103
+ const prefix = chalk3.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1412
1104
  process.stdout.write(`\r${" ".repeat(80)}\r`);
1413
- console.log(`${prefix} ${step.label} ${chalk4.green("✓")} ${chalk4.dim(`(${elapsed}s)`)}`);
1105
+ console.log(`${prefix} ${step.label} ${chalk3.green("✓")} ${chalk3.dim(`(${elapsed}s)`)}`);
1414
1106
  }
1415
1107
  }
1416
1108
 
@@ -1454,10 +1146,10 @@ function registerCheckCommand(program, dependencies = {}) {
1454
1146
  ...defaultDependencies,
1455
1147
  ...dependencies
1456
1148
  };
1457
- 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("--generate", "AI-generate missing JSDoc (requires --fix and API key)").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").option("--visibility <tags>", "Filter by release stage: public,beta,alpha,internal (comma-separated)").option("--owners", "Show coverage breakdown by CODEOWNERS").option("--contributors", "Show documentation contributors (git blame)").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) => {
1458
1150
  try {
1459
- const validations = parseExamplesFlag(options.examples);
1460
- const hasExamples = validations.length > 0;
1151
+ let validations = parseExamplesFlag(options.examples);
1152
+ let hasExamples = validations.length > 0;
1461
1153
  const stepList = [
1462
1154
  { label: "Resolved target", activeLabel: "Resolving target" },
1463
1155
  { label: "Loaded config", activeLabel: "Loading config" },
@@ -1477,8 +1169,18 @@ function registerCheckCommand(program, dependencies = {}) {
1477
1169
  const { targetDir, entryFile } = resolved;
1478
1170
  steps.next();
1479
1171
  const config = await loadDocCovConfig(targetDir);
1480
- const minCoverageRaw = options.minCoverage ?? config?.check?.minCoverage;
1481
- 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);
1482
1184
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
1483
1185
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
1484
1186
  const cliFilters = {
@@ -1488,7 +1190,7 @@ function registerCheckCommand(program, dependencies = {}) {
1488
1190
  };
1489
1191
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
1490
1192
  if (resolvedFilters.visibility) {
1491
- log(chalk5.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1193
+ log(chalk4.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1492
1194
  }
1493
1195
  steps.next();
1494
1196
  const resolveExternalTypes = !options.skipResolve;
@@ -1510,31 +1212,8 @@ function registerCheckCommand(program, dependencies = {}) {
1510
1212
  steps.next();
1511
1213
  const specWarnings = specResult.diagnostics.filter((d) => d.severity === "warning");
1512
1214
  const specInfos = specResult.diagnostics.filter((d) => d.severity === "info");
1513
- const shouldFix = options.fix || options.write;
1514
- const violations = [];
1515
- for (const exp of spec.exports ?? []) {
1516
- for (const v of exp.docs?.violations ?? []) {
1517
- violations.push({ exportName: exp.name, violation: v });
1518
- }
1519
- }
1520
- let policyResult;
1521
- if (config?.policies && config.policies.length > 0) {
1522
- policyResult = evaluatePolicies(config.policies, spec, {
1523
- baseDir: targetDir
1524
- });
1525
- }
1526
- let ownershipResult;
1527
- if (options.owners) {
1528
- ownershipResult = analyzeSpecOwnership(spec, {
1529
- baseDir: targetDir
1530
- }) ?? undefined;
1531
- }
1532
- let contributorResult;
1533
- if (options.contributors) {
1534
- contributorResult = analyzeSpecContributors(spec, {
1535
- baseDir: targetDir
1536
- }) ?? undefined;
1537
- }
1215
+ const isPreview = options.preview || options.dryRun;
1216
+ const shouldFix = options.fix || options.write || isPreview;
1538
1217
  let exampleResult;
1539
1218
  const typecheckErrors = [];
1540
1219
  const runtimeDrifts = [];
@@ -1544,11 +1223,7 @@ function registerCheckCommand(program, dependencies = {}) {
1544
1223
  packagePath: targetDir,
1545
1224
  exportNames: (spec.exports ?? []).map((e) => e.name),
1546
1225
  timeout: 5000,
1547
- installTimeout: 60000,
1548
- llmAssertionParser: isLLMAssertionParsingAvailable() ? async (example) => {
1549
- const result = await parseAssertionsWithLLM(example);
1550
- return result;
1551
- } : undefined
1226
+ installTimeout: 60000
1552
1227
  });
1553
1228
  if (exampleResult.typecheck) {
1554
1229
  for (const err of exampleResult.typecheck.errors) {
@@ -1571,6 +1246,41 @@ function registerCheckCommand(program, dependencies = {}) {
1571
1246
  }
1572
1247
  steps.next();
1573
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
+ }
1574
1284
  const coverageScore = spec.docs?.coverageScore ?? 0;
1575
1285
  const allDriftExports = [...collectDrift(spec.exports ?? []), ...runtimeDrifts];
1576
1286
  let driftExports = hasExamples ? allDriftExports : allDriftExports.filter((d) => d.category !== "example");
@@ -1580,12 +1290,12 @@ function registerCheckCommand(program, dependencies = {}) {
1580
1290
  if (allDrifts.length > 0) {
1581
1291
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1582
1292
  if (fixable.length === 0) {
1583
- log(chalk5.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.`));
1584
1294
  } else {
1585
1295
  log("");
1586
- log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
1296
+ log(chalk4.bold(`Found ${fixable.length} fixable issue(s)`));
1587
1297
  if (nonFixable.length > 0) {
1588
- log(chalk5.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1298
+ log(chalk4.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1589
1299
  }
1590
1300
  log("");
1591
1301
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -1593,22 +1303,22 @@ function registerCheckCommand(program, dependencies = {}) {
1593
1303
  const editsByFile = new Map;
1594
1304
  for (const [exp, drifts] of groupedDrifts) {
1595
1305
  if (!exp.source?.file) {
1596
- log(chalk5.gray(` Skipping ${exp.name}: no source location`));
1306
+ log(chalk4.gray(` Skipping ${exp.name}: no source location`));
1597
1307
  continue;
1598
1308
  }
1599
1309
  if (exp.source.file.endsWith(".d.ts")) {
1600
- log(chalk5.gray(` Skipping ${exp.name}: declaration file`));
1310
+ log(chalk4.gray(` Skipping ${exp.name}: declaration file`));
1601
1311
  continue;
1602
1312
  }
1603
1313
  const filePath = path4.resolve(targetDir, exp.source.file);
1604
1314
  if (!fs2.existsSync(filePath)) {
1605
- log(chalk5.gray(` Skipping ${exp.name}: file not found`));
1315
+ log(chalk4.gray(` Skipping ${exp.name}: file not found`));
1606
1316
  continue;
1607
1317
  }
1608
1318
  const sourceFile = createSourceFile(filePath);
1609
1319
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1610
1320
  if (!location) {
1611
- log(chalk5.gray(` Skipping ${exp.name}: could not find declaration`));
1321
+ log(chalk4.gray(` Skipping ${exp.name}: could not find declaration`));
1612
1322
  continue;
1613
1323
  }
1614
1324
  let existingPatch = {};
@@ -1640,129 +1350,63 @@ function registerCheckCommand(program, dependencies = {}) {
1640
1350
  editsByFile.set(filePath, fileEdits);
1641
1351
  }
1642
1352
  if (edits.length > 0) {
1643
- if (options.dryRun) {
1644
- log(chalk5.bold("Dry run - changes that would be made:"));
1353
+ if (isPreview) {
1354
+ log(chalk4.bold("Preview - changes that would be made:"));
1645
1355
  log("");
1646
1356
  for (const [filePath, fileEdits] of editsByFile) {
1647
1357
  const relativePath = path4.relative(targetDir, filePath);
1648
- log(chalk5.cyan(` ${relativePath}:`));
1649
1358
  for (const { export: exp, edit, fixes } of fileEdits) {
1650
- const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
1651
- log(` ${chalk5.bold(exp.name)} [${lineInfo}]`);
1652
- for (const fix of fixes) {
1653
- log(chalk5.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
+ }
1654
1379
  }
1380
+ log("");
1381
+ log(chalk4.dim(` Fixes: ${fixes.map((f) => f.description).join(", ")}`));
1382
+ log("");
1655
1383
  }
1656
- log("");
1657
1384
  }
1658
- log(chalk5.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."));
1659
1388
  } else {
1660
1389
  const applyResult = await applyEdits(edits);
1661
1390
  if (applyResult.errors.length > 0) {
1662
1391
  for (const err of applyResult.errors) {
1663
- error(chalk5.red(` ${err.file}: ${err.error}`));
1392
+ error(chalk4.red(` ${err.file}: ${err.error}`));
1664
1393
  }
1665
1394
  }
1666
- }
1667
- }
1668
- }
1669
- }
1670
- if (!options.dryRun) {
1671
- driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1672
- }
1673
- }
1674
- const generatedExportKeys = new Set;
1675
- if (shouldFix && options.generate) {
1676
- const hasAIAccess = isHostedAPIAvailable() || isAIGenerationAvailable();
1677
- if (!hasAIAccess) {
1678
- log("");
1679
- log(chalk5.yellow("⚠ --generate requires AI access. Options:"));
1680
- log(chalk5.dim(" 1. Set DOCCOV_API_KEY for hosted AI (included with Team/Pro plan)"));
1681
- log(chalk5.dim(" 2. Set OPENAI_API_KEY or ANTHROPIC_API_KEY for direct API access"));
1682
- } else {
1683
- const undocumented = (spec.exports ?? []).filter((exp) => {
1684
- if (exp.description)
1685
- return false;
1686
- if (exp.source?.file?.endsWith(".d.ts"))
1687
- return false;
1688
- if (!exp.source?.file)
1689
- return false;
1690
- return true;
1691
- });
1692
- if (undocumented.length > 0) {
1693
- log("");
1694
- log(chalk5.bold(`Generating JSDoc for ${undocumented.length} undocumented export(s)`));
1695
- log("");
1696
- const aiResult = await generateWithFallback(undocumented, {
1697
- maxConcurrent: 3,
1698
- onProgress: (completed, total, name) => {
1699
- log(chalk5.dim(` [${completed}/${total}] ${name}`));
1700
- },
1701
- log,
1702
- packageName: spec.meta?.name
1703
- });
1704
- const generated = aiResult.results;
1705
- const edits = [];
1706
- for (const result of generated) {
1707
- if (!result.generated)
1708
- continue;
1709
- const exp = undocumented.find((e) => e.name === result.exportName);
1710
- if (!exp || !exp.source?.file)
1711
- continue;
1712
- const filePath = path4.resolve(targetDir, exp.source.file);
1713
- if (!fs2.existsSync(filePath))
1714
- continue;
1715
- const sourceFile = createSourceFile(filePath);
1716
- const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1717
- if (!location)
1718
- continue;
1719
- let existingPatch = {};
1720
- if (location.hasExisting && location.existingJSDoc) {
1721
- existingPatch = parseJSDocToPatch(location.existingJSDoc);
1722
- }
1723
- const mergedPatch = { ...existingPatch, ...result.patch };
1724
- const newJSDoc = serializeJSDoc(mergedPatch, location.indent);
1725
- edits.push({
1726
- filePath,
1727
- symbolName: exp.name,
1728
- startLine: location.startLine,
1729
- endLine: location.endLine,
1730
- hasExisting: location.hasExisting,
1731
- existingJSDoc: location.existingJSDoc,
1732
- newJSDoc,
1733
- indent: location.indent
1734
- });
1735
- generatedExportKeys.add(exp.name);
1736
- }
1737
- if (edits.length > 0) {
1738
- if (options.dryRun) {
1739
- log("");
1740
- log(chalk5.bold("Dry run - JSDoc that would be generated:"));
1741
- for (const edit of edits) {
1742
- const relativePath = path4.relative(targetDir, edit.filePath);
1743
- log(chalk5.cyan(` ${relativePath}:`));
1744
- log(` ${chalk5.bold(edit.symbolName)} [line ${edit.startLine + 1}]`);
1745
- log(chalk5.green(" + description, params, returns, example"));
1746
- }
1395
+ const totalFixes = Array.from(editsByFile.values()).reduce((sum, edits2) => sum + edits2.reduce((s, e) => s + e.fixes.length, 0), 0);
1747
1396
  log("");
1748
- log(chalk5.gray("Run without --dry-run to apply these changes."));
1749
- } else {
1750
- const applyResult = await applyEdits(edits);
1751
- log("");
1752
- log(chalk5.green(`✓ Generated JSDoc for ${edits.length} export(s)`));
1753
- if (aiResult.source === "hosted" && aiResult.quotaRemaining !== undefined) {
1754
- const remaining = aiResult.quotaRemaining === "unlimited" ? "unlimited" : aiResult.quotaRemaining.toLocaleString();
1755
- log(chalk5.dim(` AI calls remaining: ${remaining}`));
1756
- }
1757
- if (applyResult.errors.length > 0) {
1758
- for (const err of applyResult.errors) {
1759
- error(chalk5.red(` ${err.file}: ${err.error}`));
1760
- }
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)`));
1761
1402
  }
1762
1403
  }
1763
1404
  }
1764
1405
  }
1765
1406
  }
1407
+ if (!isPreview) {
1408
+ driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1409
+ }
1766
1410
  }
1767
1411
  steps.complete("Check complete");
1768
1412
  if (format !== "text") {
@@ -1784,8 +1428,7 @@ function registerCheckCommand(program, dependencies = {}) {
1784
1428
  case "github":
1785
1429
  formatContent = renderGithubSummary(stats, {
1786
1430
  coverageScore,
1787
- driftCount: driftExports.length,
1788
- qualityIssues: violations.length
1431
+ driftCount: driftExports.length
1789
1432
  });
1790
1433
  break;
1791
1434
  default:
@@ -1805,12 +1448,10 @@ function registerCheckCommand(program, dependencies = {}) {
1805
1448
  const totalExportsForDrift2 = spec.exports?.length ?? 0;
1806
1449
  const exportsWithDrift2 = new Set(driftExports.map((d) => d.name)).size;
1807
1450
  const driftScore2 = totalExportsForDrift2 === 0 ? 0 : Math.round(exportsWithDrift2 / totalExportsForDrift2 * 100);
1808
- const coverageFailed2 = minCoverage !== undefined && coverageScore < minCoverage;
1451
+ const coverageFailed2 = coverageScore < minCoverage;
1809
1452
  const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
1810
- const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
1811
1453
  const hasTypecheckErrors2 = typecheckErrors.length > 0;
1812
- const policiesFailed2 = policyResult && !policyResult.allPassed;
1813
- if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
1454
+ if (coverageFailed2 || driftFailed2 || hasTypecheckErrors2) {
1814
1455
  process.exit(1);
1815
1456
  }
1816
1457
  return;
@@ -1818,49 +1459,41 @@ function registerCheckCommand(program, dependencies = {}) {
1818
1459
  const totalExportsForDrift = spec.exports?.length ?? 0;
1819
1460
  const exportsWithDrift = new Set(driftExports.map((d) => d.name)).size;
1820
1461
  const driftScore = totalExportsForDrift === 0 ? 0 : Math.round(exportsWithDrift / totalExportsForDrift * 100);
1821
- const coverageFailed = minCoverage !== undefined && coverageScore < minCoverage;
1462
+ const coverageFailed = coverageScore < minCoverage;
1822
1463
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1823
- const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
1824
1464
  const hasTypecheckErrors = typecheckErrors.length > 0;
1825
- const policiesFailed = policyResult && !policyResult.allPassed;
1826
1465
  if (specWarnings.length > 0 || specInfos.length > 0) {
1827
1466
  log("");
1828
1467
  for (const diag of specWarnings) {
1829
- log(chalk5.yellow(`⚠ ${diag.message}`));
1468
+ log(chalk4.yellow(`⚠ ${diag.message}`));
1830
1469
  if (diag.suggestion) {
1831
- log(chalk5.gray(` ${diag.suggestion}`));
1470
+ log(chalk4.gray(` ${diag.suggestion}`));
1832
1471
  }
1833
1472
  }
1834
1473
  for (const diag of specInfos) {
1835
- log(chalk5.cyan(`ℹ ${diag.message}`));
1474
+ log(chalk4.cyan(`ℹ ${diag.message}`));
1836
1475
  if (diag.suggestion) {
1837
- log(chalk5.gray(` ${diag.suggestion}`));
1476
+ log(chalk4.gray(` ${diag.suggestion}`));
1838
1477
  }
1839
1478
  }
1840
1479
  }
1841
1480
  const pkgName = spec.meta?.name ?? "unknown";
1842
1481
  const pkgVersion = spec.meta?.version ?? "";
1843
1482
  const totalExports = spec.exports?.length ?? 0;
1844
- const errorCount = violations.filter((v) => v.violation.severity === "error").length;
1845
- const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
1846
1483
  log("");
1847
- log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1484
+ log(chalk4.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1848
1485
  log("");
1849
1486
  log(` Exports: ${totalExports}`);
1850
- if (minCoverage !== undefined) {
1851
- if (coverageFailed) {
1852
- log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1853
- } else {
1854
- log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1855
- }
1487
+ if (coverageFailed) {
1488
+ log(chalk4.red(` Coverage: ✗ ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1856
1489
  } else {
1857
- log(` Coverage: ${coverageScore}%`);
1490
+ log(chalk4.green(` Coverage: ${coverageScore}%`) + chalk4.dim(` (min ${minCoverage}%)`));
1858
1491
  }
1859
1492
  if (maxDrift !== undefined) {
1860
1493
  if (driftFailed) {
1861
- log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1494
+ log(chalk4.red(` Drift: ✗ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1862
1495
  } else {
1863
- log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1496
+ log(chalk4.green(` Drift: ✓ ${driftScore}%`) + chalk4.dim(` (max ${maxDrift}%)`));
1864
1497
  }
1865
1498
  } else {
1866
1499
  log(` Drift: ${driftScore}%`);
@@ -1868,103 +1501,51 @@ function registerCheckCommand(program, dependencies = {}) {
1868
1501
  if (exampleResult) {
1869
1502
  const typecheckCount = exampleResult.typecheck?.errors.length ?? 0;
1870
1503
  if (typecheckCount > 0) {
1871
- log(` Examples: ${typecheckCount} type errors`);
1872
- } else {
1873
- log(chalk5.green(` Examples: ✓ validated`));
1874
- }
1875
- }
1876
- if (errorCount > 0 || warnCount > 0) {
1877
- const parts = [];
1878
- if (errorCount > 0)
1879
- parts.push(`${errorCount} errors`);
1880
- if (warnCount > 0)
1881
- parts.push(`${warnCount} warnings`);
1882
- log(` Quality: ${parts.join(", ")}`);
1883
- }
1884
- if (policyResult && policyResult.results.length > 0) {
1885
- log("");
1886
- log(chalk5.bold(" Policies:"));
1887
- for (const result of policyResult.results) {
1888
- const status = result.passed ? chalk5.green("✓") : chalk5.red("✗");
1889
- const policyPath = result.policy.path;
1890
- const matchCount = result.matchedExports.length;
1891
- if (result.passed) {
1892
- log(` ${status} ${policyPath} (${matchCount} exports)`);
1893
- } else {
1894
- log(` ${status} ${policyPath} (${matchCount} exports)`);
1895
- for (const failure of result.failures) {
1896
- log(chalk5.red(` ${failure.message}`));
1897
- }
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}`));
1898
1509
  }
1510
+ if (typecheckErrors.length > 5) {
1511
+ log(chalk4.dim(` ... and ${typecheckErrors.length - 5} more`));
1512
+ }
1513
+ } else {
1514
+ log(chalk4.green(` Examples: ✓ validated`));
1899
1515
  }
1900
1516
  }
1901
- if (ownershipResult) {
1902
- log("");
1903
- log(chalk5.bold(" Owners:"));
1904
- const sortedOwners = [...ownershipResult.byOwner.values()].sort((a, b) => a.coverageScore - b.coverageScore);
1905
- for (const stats of sortedOwners) {
1906
- const coverageColor = stats.coverageScore >= 80 ? chalk5.green : stats.coverageScore >= 50 ? chalk5.yellow : chalk5.red;
1907
- const coverageStr = coverageColor(`${stats.coverageScore}%`);
1908
- const undocCount = stats.undocumentedExports.length;
1909
- log(` ${stats.owner.padEnd(24)} ${coverageStr.padStart(12)} coverage (${stats.totalExports} exports${undocCount > 0 ? `, ${undocCount} undoc` : ""})`);
1910
- }
1911
- if (ownershipResult.unowned.length > 0) {
1912
- log(chalk5.dim(` (${ownershipResult.unowned.length} exports have no owner in CODEOWNERS)`));
1913
- }
1914
- }
1915
- if (contributorResult) {
1916
- log("");
1917
- log(chalk5.bold(" Contributors:"));
1918
- const sortedContributors = [...contributorResult.byContributor.values()].sort((a, b) => b.documentedExports - a.documentedExports);
1919
- const displayLimit = 10;
1920
- const topContributors = sortedContributors.slice(0, displayLimit);
1921
- for (const stats of topContributors) {
1922
- const name = stats.name.length > 20 ? stats.name.slice(0, 17) + "..." : stats.name;
1923
- const lastDate = stats.lastContribution ? stats.lastContribution.toISOString().split("T")[0] : "unknown";
1924
- log(` ${name.padEnd(22)} ${String(stats.documentedExports).padStart(4)} exports (last: ${lastDate})`);
1925
- }
1926
- if (sortedContributors.length > displayLimit) {
1927
- log(chalk5.dim(` ... and ${sortedContributors.length - displayLimit} more contributors`));
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}"`));
1928
1522
  }
1929
- if (contributorResult.unattributed.length > 0) {
1930
- log(chalk5.dim(` (${contributorResult.unattributed.length} exports could not be attributed via git blame)`));
1523
+ if (staleRefs.length > 5) {
1524
+ log(chalk4.dim(` ... and ${staleRefs.length - 5} more`));
1931
1525
  }
1932
1526
  }
1933
1527
  log("");
1934
- const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors || policiesFailed;
1528
+ const failed = coverageFailed || driftFailed || hasTypecheckErrors || hasStaleRefs;
1935
1529
  if (!failed) {
1936
1530
  const thresholdParts = [];
1937
- if (minCoverage !== undefined) {
1938
- thresholdParts.push(`coverage ${coverageScore}% ≥ ${minCoverage}%`);
1939
- }
1531
+ thresholdParts.push(`coverage ${coverageScore}% ${minCoverage}%`);
1940
1532
  if (maxDrift !== undefined) {
1941
1533
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1942
1534
  }
1943
- if (policyResult) {
1944
- thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
1945
- }
1946
- if (thresholdParts.length > 0) {
1947
- log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1948
- } else {
1949
- log(chalk5.green("✓ Check passed"));
1950
- log(chalk5.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1951
- }
1535
+ log(chalk4.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1952
1536
  return;
1953
1537
  }
1954
- if (hasQualityErrors) {
1955
- log(chalk5.red(`✗ ${errorCount} quality errors`));
1956
- }
1957
1538
  if (hasTypecheckErrors) {
1958
- log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
1539
+ log(chalk4.red(`✗ ${typecheckErrors.length} example type errors`));
1959
1540
  }
1960
- if (policiesFailed && policyResult) {
1961
- log(chalk5.red(`✗ ${policyResult.failedCount} policy failures`));
1541
+ if (hasStaleRefs) {
1542
+ log(chalk4.red(`✗ ${staleRefs.length} stale references in docs`));
1962
1543
  }
1963
1544
  log("");
1964
- log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
1545
+ log(chalk4.dim("Use --format json or --format markdown for detailed reports"));
1965
1546
  process.exit(1);
1966
1547
  } catch (commandError) {
1967
- error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1548
+ error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1968
1549
  process.exit(1);
1969
1550
  }
1970
1551
  });
@@ -1988,6 +1569,23 @@ function collectDrift(exportsList) {
1988
1569
  }
1989
1570
  return drifts;
1990
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
+ }
1991
1589
 
1992
1590
  // src/commands/diff.ts
1993
1591
  import * as fs3 from "node:fs";
@@ -1999,73 +1597,11 @@ import {
1999
1597
  getDocsImpactSummary,
2000
1598
  hasDocsImpact,
2001
1599
  hashString,
2002
- parseMarkdownFiles
1600
+ parseMarkdownFiles as parseMarkdownFiles2
2003
1601
  } from "@doccov/sdk";
2004
1602
  import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
2005
- import chalk6 from "chalk";
2006
- import { glob } from "glob";
2007
-
2008
- // src/utils/docs-impact-ai.ts
2009
- import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
2010
- import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
2011
- import { generateObject as generateObject3, generateText } from "ai";
2012
- import { z as z4 } from "zod";
2013
- var CodeBlockUsageSchema = z4.object({
2014
- isImpacted: z4.boolean().describe("Whether the code block is affected by the change"),
2015
- reason: z4.string().describe("Explanation of why/why not the code is impacted"),
2016
- usageType: z4.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
2017
- suggestedFix: z4.string().optional().describe("If impacted, the suggested code change"),
2018
- confidence: z4.enum(["high", "medium", "low"]).describe("Confidence level of the analysis")
2019
- });
2020
- var MultiBlockAnalysisSchema = z4.object({
2021
- groups: z4.array(z4.object({
2022
- blockIndices: z4.array(z4.number()).describe("Indices of blocks that should run together"),
2023
- reason: z4.string().describe("Why these blocks are related")
2024
- })).describe("Groups of related code blocks"),
2025
- skippedBlocks: z4.array(z4.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
2026
- });
2027
- function getModel3() {
2028
- const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
2029
- if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
2030
- const anthropic = createAnthropic3();
2031
- return anthropic("claude-sonnet-4-20250514");
2032
- }
2033
- const openai = createOpenAI3();
2034
- return openai("gpt-4o-mini");
2035
- }
2036
- function isAIDocsAnalysisAvailable() {
2037
- return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
2038
- }
2039
- async function generateImpactSummary(impacts) {
2040
- if (!isAIDocsAnalysisAvailable()) {
2041
- return null;
2042
- }
2043
- if (impacts.length === 0) {
2044
- return "No documentation impacts detected.";
2045
- }
2046
- try {
2047
- const { text } = await generateText({
2048
- model: getModel3(),
2049
- prompt: `Summarize these documentation impacts for a GitHub PR comment.
2050
-
2051
- Impacts:
2052
- ${impacts.map((i) => `- ${i.file}: ${i.exportName} (${i.changeType})`).join(`
2053
- `)}
2054
-
2055
- Write a brief, actionable summary (2-3 sentences) explaining:
2056
- 1. How many files/references are affected
2057
- 2. What type of updates are needed
2058
- 3. Priority recommendation
2059
-
2060
- Keep it concise and developer-friendly.`
2061
- });
2062
- return text.trim();
2063
- } catch {
2064
- return null;
2065
- }
2066
- }
2067
-
2068
- // src/commands/diff.ts
1603
+ import chalk5 from "chalk";
1604
+ import { glob as glob2 } from "glob";
2069
1605
  var defaultDependencies2 = {
2070
1606
  readFileSync: fs3.readFileSync,
2071
1607
  log: console.log,
@@ -2086,11 +1622,11 @@ function getStrictChecks(preset) {
2086
1622
  return checks;
2087
1623
  }
2088
1624
  function registerDiffCommand(program, dependencies = {}) {
2089
- const { readFileSync: readFileSync2, log, error } = {
1625
+ const { readFileSync: readFileSync3, log, error } = {
2090
1626
  ...defaultDependencies2,
2091
1627
  ...dependencies
2092
1628
  };
2093
- 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", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").option("--recommend-version", "Output recommended semver version bump").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) => {
2094
1630
  try {
2095
1631
  const baseFile = options.base ?? baseArg;
2096
1632
  const headFile = options.head ?? headArg;
@@ -2099,8 +1635,8 @@ function registerDiffCommand(program, dependencies = {}) {
2099
1635
  ` + `Usage: doccov diff <base> <head>
2100
1636
  ` + " or: doccov diff --base main.json --head feature.json");
2101
1637
  }
2102
- const baseSpec = loadSpec(baseFile, readFileSync2);
2103
- const headSpec = loadSpec(headFile, readFileSync2);
1638
+ const baseSpec = loadSpec(baseFile, readFileSync3);
1639
+ const headSpec = loadSpec(headFile, readFileSync3);
2104
1640
  const config = await loadDocCovConfig(options.cwd);
2105
1641
  const baseHash = hashString(JSON.stringify(baseSpec));
2106
1642
  const headHash = hashString(JSON.stringify(headSpec));
@@ -2137,9 +1673,9 @@ function registerDiffCommand(program, dependencies = {}) {
2137
1673
  }, null, 2));
2138
1674
  } else {
2139
1675
  log("");
2140
- log(chalk6.bold("Semver Recommendation"));
1676
+ log(chalk5.bold("Semver Recommendation"));
2141
1677
  log(` Current version: ${currentVersion}`);
2142
- log(` Recommended: ${chalk6.cyan(nextVersion)} (${chalk6.yellow(recommendation.bump.toUpperCase())})`);
1678
+ log(` Recommended: ${chalk5.cyan(nextVersion)} (${chalk5.yellow(recommendation.bump.toUpperCase())})`);
2143
1679
  log(` Reason: ${recommendation.reason}`);
2144
1680
  }
2145
1681
  return;
@@ -2157,9 +1693,6 @@ function registerDiffCommand(program, dependencies = {}) {
2157
1693
  switch (format) {
2158
1694
  case "text":
2159
1695
  printSummary(diff, baseName, headName, fromCache, log);
2160
- if (options.ai && diff.docsImpact && hasDocsImpact(diff)) {
2161
- await printAISummary(diff, log);
2162
- }
2163
1696
  if (!options.stdout) {
2164
1697
  const jsonPath = getDiffReportPath(baseHash, headHash, "json");
2165
1698
  if (!fromCache) {
@@ -2171,8 +1704,8 @@ function registerDiffCommand(program, dependencies = {}) {
2171
1704
  silent: true
2172
1705
  });
2173
1706
  }
2174
- const cacheNote = fromCache ? chalk6.cyan(" (cached)") : "";
2175
- log(chalk6.dim(`Report: ${jsonPath}`) + cacheNote);
1707
+ const cacheNote = fromCache ? chalk5.cyan(" (cached)") : "";
1708
+ log(chalk5.dim(`Report: ${jsonPath}`) + cacheNote);
2176
1709
  }
2177
1710
  break;
2178
1711
  case "json": {
@@ -2224,11 +1757,13 @@ function registerDiffCommand(program, dependencies = {}) {
2224
1757
  printGitHubAnnotations(diff, log);
2225
1758
  break;
2226
1759
  case "pr-comment": {
1760
+ const semverRecommendation = recommendSemverBump(diff);
2227
1761
  const content = renderPRComment({ diff, baseName, headName, headSpec }, {
2228
1762
  repoUrl: options.repoUrl,
2229
1763
  sha: options.sha,
2230
1764
  minCoverage,
2231
- limit
1765
+ limit,
1766
+ semverBump: { bump: semverRecommendation.bump, reason: semverRecommendation.reason }
2232
1767
  });
2233
1768
  log(content);
2234
1769
  break;
@@ -2262,29 +1797,29 @@ function registerDiffCommand(program, dependencies = {}) {
2262
1797
  checks
2263
1798
  });
2264
1799
  if (failures.length > 0) {
2265
- log(chalk6.red(`
1800
+ log(chalk5.red(`
2266
1801
  ✗ Check failed`));
2267
1802
  for (const f of failures) {
2268
- log(chalk6.red(` - ${f}`));
1803
+ log(chalk5.red(` - ${f}`));
2269
1804
  }
2270
1805
  process.exitCode = 1;
2271
1806
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
2272
- log(chalk6.green(`
1807
+ log(chalk5.green(`
2273
1808
  ✓ All checks passed`));
2274
1809
  }
2275
1810
  } catch (commandError) {
2276
- error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1811
+ error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2277
1812
  process.exitCode = 1;
2278
1813
  }
2279
1814
  });
2280
1815
  }
2281
- function collect(value, previous) {
1816
+ function collect2(value, previous) {
2282
1817
  return previous.concat([value]);
2283
1818
  }
2284
- async function loadMarkdownFiles(patterns) {
1819
+ async function loadMarkdownFiles2(patterns) {
2285
1820
  const files = [];
2286
1821
  for (const pattern of patterns) {
2287
- const matches = await glob(pattern, { nodir: true });
1822
+ const matches = await glob2(pattern, { nodir: true });
2288
1823
  for (const filePath of matches) {
2289
1824
  try {
2290
1825
  const content = fs3.readFileSync(filePath, "utf-8");
@@ -2292,7 +1827,7 @@ async function loadMarkdownFiles(patterns) {
2292
1827
  } catch {}
2293
1828
  }
2294
1829
  }
2295
- return parseMarkdownFiles(files);
1830
+ return parseMarkdownFiles2(files);
2296
1831
  }
2297
1832
  async function generateDiff(baseSpec, headSpec, options, config, log) {
2298
1833
  let markdownFiles;
@@ -2300,21 +1835,21 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
2300
1835
  if (!docsPatterns || docsPatterns.length === 0) {
2301
1836
  if (config?.docs?.include) {
2302
1837
  docsPatterns = config.docs.include;
2303
- log(chalk6.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1838
+ log(chalk5.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
2304
1839
  }
2305
1840
  }
2306
1841
  if (docsPatterns && docsPatterns.length > 0) {
2307
- markdownFiles = await loadMarkdownFiles(docsPatterns);
1842
+ markdownFiles = await loadMarkdownFiles2(docsPatterns);
2308
1843
  }
2309
1844
  return diffSpecWithDocs(baseSpec, headSpec, { markdownFiles });
2310
1845
  }
2311
- function loadSpec(filePath, readFileSync2) {
1846
+ function loadSpec(filePath, readFileSync3) {
2312
1847
  const resolvedPath = path5.resolve(filePath);
2313
1848
  if (!fs3.existsSync(resolvedPath)) {
2314
1849
  throw new Error(`File not found: ${filePath}`);
2315
1850
  }
2316
1851
  try {
2317
- const content = readFileSync2(resolvedPath, "utf-8");
1852
+ const content = readFileSync3(resolvedPath, "utf-8");
2318
1853
  const spec = JSON.parse(content);
2319
1854
  return ensureSpecCoverage(spec);
2320
1855
  } catch (parseError) {
@@ -2323,60 +1858,40 @@ function loadSpec(filePath, readFileSync2) {
2323
1858
  }
2324
1859
  function printSummary(diff, baseName, headName, fromCache, log) {
2325
1860
  log("");
2326
- const cacheIndicator = fromCache ? chalk6.cyan(" (cached)") : "";
2327
- log(chalk6.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1861
+ const cacheIndicator = fromCache ? chalk5.cyan(" (cached)") : "";
1862
+ log(chalk5.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
2328
1863
  log("─".repeat(40));
2329
1864
  log("");
2330
- const coverageColor = diff.coverageDelta > 0 ? chalk6.green : diff.coverageDelta < 0 ? chalk6.red : chalk6.gray;
1865
+ const coverageColor = diff.coverageDelta > 0 ? chalk5.green : diff.coverageDelta < 0 ? chalk5.red : chalk5.gray;
2331
1866
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
2332
1867
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
2333
1868
  const breakingCount = diff.breaking.length;
2334
1869
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
2335
1870
  if (breakingCount > 0) {
2336
- const severityNote = highSeverity > 0 ? chalk6.red(` (${highSeverity} high severity)`) : "";
2337
- log(` Breaking: ${chalk6.red(breakingCount)} changes${severityNote}`);
1871
+ const severityNote = highSeverity > 0 ? chalk5.red(` (${highSeverity} high severity)`) : "";
1872
+ log(` Breaking: ${chalk5.red(breakingCount)} changes${severityNote}`);
2338
1873
  } else {
2339
- log(` Breaking: ${chalk6.green("0")} changes`);
1874
+ log(` Breaking: ${chalk5.green("0")} changes`);
2340
1875
  }
2341
1876
  const newCount = diff.nonBreaking.length;
2342
1877
  const undocCount = diff.newUndocumented.length;
2343
1878
  if (newCount > 0) {
2344
- const undocNote = undocCount > 0 ? chalk6.yellow(` (${undocCount} undocumented)`) : "";
2345
- log(` New: ${chalk6.green(newCount)} exports${undocNote}`);
1879
+ const undocNote = undocCount > 0 ? chalk5.yellow(` (${undocCount} undocumented)`) : "";
1880
+ log(` New: ${chalk5.green(newCount)} exports${undocNote}`);
2346
1881
  }
2347
1882
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
2348
1883
  const parts = [];
2349
1884
  if (diff.driftIntroduced > 0)
2350
- parts.push(chalk6.red(`+${diff.driftIntroduced}`));
1885
+ parts.push(chalk5.red(`+${diff.driftIntroduced}`));
2351
1886
  if (diff.driftResolved > 0)
2352
- parts.push(chalk6.green(`-${diff.driftResolved}`));
1887
+ parts.push(chalk5.green(`-${diff.driftResolved}`));
2353
1888
  log(` Drift: ${parts.join(", ")}`);
2354
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})`);
2355
1893
  log("");
2356
1894
  }
2357
- async function printAISummary(diff, log) {
2358
- if (!isAIDocsAnalysisAvailable()) {
2359
- log(chalk6.yellow(`
2360
- ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
2361
- return;
2362
- }
2363
- if (!diff.docsImpact)
2364
- return;
2365
- log(chalk6.gray(`
2366
- Generating AI summary...`));
2367
- const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
2368
- file: f.file,
2369
- exportName: r.exportName,
2370
- changeType: r.changeType,
2371
- context: r.context
2372
- })));
2373
- const summary = await generateImpactSummary(impacts);
2374
- if (summary) {
2375
- log("");
2376
- log(chalk6.bold("AI Summary"));
2377
- log(chalk6.cyan(` ${summary}`));
2378
- }
2379
- }
2380
1895
  function validateDiff(diff, headSpec, options) {
2381
1896
  const { minCoverage, maxDrift, checks } = options;
2382
1897
  const failures = [];
@@ -2455,7 +1970,7 @@ function printGitHubAnnotations(diff, log) {
2455
1970
 
2456
1971
  // src/commands/info.ts
2457
1972
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
2458
- import chalk7 from "chalk";
1973
+ import chalk6 from "chalk";
2459
1974
  function registerInfoCommand(program) {
2460
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) => {
2461
1976
  try {
@@ -2477,14 +1992,14 @@ function registerInfoCommand(program) {
2477
1992
  const spec = enrichSpec2(specResult.spec);
2478
1993
  const stats = computeStats(spec);
2479
1994
  console.log("");
2480
- console.log(chalk7.bold(`${stats.packageName}@${stats.version}`));
1995
+ console.log(chalk6.bold(`${stats.packageName}@${stats.version}`));
2481
1996
  console.log("");
2482
- console.log(` Exports: ${chalk7.bold(stats.totalExports.toString())}`);
2483
- console.log(` Coverage: ${chalk7.bold(`${stats.coverageScore}%`)}`);
2484
- console.log(` Drift: ${chalk7.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}%`)}`);
2485
2000
  console.log("");
2486
2001
  } catch (err) {
2487
- console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
2002
+ console.error(chalk6.red("Error:"), err instanceof Error ? err.message : err);
2488
2003
  process.exit(1);
2489
2004
  }
2490
2005
  });
@@ -2493,53 +2008,65 @@ function registerInfoCommand(program) {
2493
2008
  // src/commands/init.ts
2494
2009
  import * as fs4 from "node:fs";
2495
2010
  import * as path6 from "node:path";
2496
- import chalk8 from "chalk";
2011
+ import chalk7 from "chalk";
2497
2012
  var defaultDependencies3 = {
2498
2013
  fileExists: fs4.existsSync,
2499
2014
  writeFileSync: fs4.writeFileSync,
2500
2015
  readFileSync: fs4.readFileSync,
2016
+ mkdirSync: fs4.mkdirSync,
2501
2017
  log: console.log,
2502
2018
  error: console.error
2503
2019
  };
2504
2020
  function registerInitCommand(program, dependencies = {}) {
2505
- const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync3, log, error } = {
2021
+ const { fileExists: fileExists2, writeFileSync: writeFileSync3, readFileSync: readFileSync4, mkdirSync: mkdirSync3, log, error } = {
2506
2022
  ...defaultDependencies3,
2507
2023
  ...dependencies
2508
2024
  };
2509
- 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) => {
2510
2026
  const cwd = path6.resolve(options.cwd);
2511
- const formatOption = String(options.format ?? "auto").toLowerCase();
2512
- if (!isValidFormat(formatOption)) {
2513
- error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
2514
- process.exitCode = 1;
2515
- return;
2516
- }
2517
2027
  const existing = findExistingConfig(cwd, fileExists2);
2518
2028
  if (existing) {
2519
- error(chalk8.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.*"}.`));
2520
2030
  process.exitCode = 1;
2521
2031
  return;
2522
2032
  }
2523
- const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
2524
- const targetFormat = resolveFormat(formatOption, packageType);
2525
- if (targetFormat === "js" && packageType !== "module") {
2526
- log(chalk8.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
2527
- }
2528
- 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}`;
2529
2036
  const outputPath = path6.join(cwd, fileName);
2530
2037
  if (fileExists2(outputPath)) {
2531
- error(chalk8.red(`Cannot create ${fileName}; file already exists.`));
2038
+ error(chalk7.red(`Cannot create ${fileName}; file already exists.`));
2532
2039
  process.exitCode = 1;
2533
2040
  return;
2534
2041
  }
2535
- const template = buildTemplate(targetFormat);
2042
+ const template = buildConfigTemplate();
2536
2043
  writeFileSync3(outputPath, template, { encoding: "utf8" });
2537
- log(chalk8.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"));
2538
2068
  });
2539
2069
  }
2540
- var isValidFormat = (value) => {
2541
- return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
2542
- };
2543
2070
  var findExistingConfig = (cwd, fileExists2) => {
2544
2071
  let current = path6.resolve(cwd);
2545
2072
  const { root } = path6.parse(current);
@@ -2557,13 +2084,13 @@ var findExistingConfig = (cwd, fileExists2) => {
2557
2084
  }
2558
2085
  return null;
2559
2086
  };
2560
- var detectPackageType = (cwd, fileExists2, readFileSync3) => {
2087
+ var detectPackageType = (cwd, fileExists2, readFileSync4) => {
2561
2088
  const packageJsonPath = findNearestPackageJson(cwd, fileExists2);
2562
2089
  if (!packageJsonPath) {
2563
2090
  return;
2564
2091
  }
2565
2092
  try {
2566
- const raw = readFileSync3(packageJsonPath, "utf8");
2093
+ const raw = readFileSync4(packageJsonPath, "utf8");
2567
2094
  const parsed = JSON.parse(raw);
2568
2095
  if (parsed.type === "module") {
2569
2096
  return "module";
@@ -2589,80 +2116,75 @@ var findNearestPackageJson = (cwd, fileExists2) => {
2589
2116
  }
2590
2117
  return null;
2591
2118
  };
2592
- var resolveFormat = (format, packageType) => {
2593
- if (format === "yaml")
2594
- return "yaml";
2595
- if (format === "auto") {
2596
- return packageType === "module" ? "js" : "mjs";
2597
- }
2598
- return format;
2599
- };
2600
- var buildTemplate = (format) => {
2601
- if (format === "yaml") {
2602
- return `# doccov.yml
2603
- # include:
2604
- # - "MyClass"
2605
- # - "myFunction"
2606
- # exclude:
2607
- # - "internal*"
2608
-
2609
- check:
2610
- # minCoverage: 80
2611
- # maxDrift: 20
2612
- # examples: typecheck
2119
+ var buildConfigTemplate = () => {
2120
+ return `import { defineConfig } from '@doccov/cli/config';
2613
2121
 
2614
- quality:
2615
- rules:
2616
- # has-description: warn
2617
- # has-params: off
2618
- # has-returns: off
2619
- `;
2620
- }
2621
- const configBody = `{
2622
- // Filter which exports to analyze
2122
+ export default defineConfig({
2123
+ // Filter exports to analyze (optional)
2623
2124
  // include: ['MyClass', 'myFunction'],
2624
2125
  // exclude: ['internal*'],
2625
2126
 
2626
- // Check command thresholds
2627
2127
  check: {
2628
- // Minimum documentation coverage percentage (0-100)
2629
- // minCoverage: 80,
2128
+ // Fail if coverage drops below threshold
2129
+ minCoverage: 80,
2630
2130
 
2631
- // Maximum drift percentage allowed (0-100)
2131
+ // Fail if drift exceeds threshold
2632
2132
  // maxDrift: 20,
2633
-
2634
- // Example validation: 'presence' | 'typecheck' | 'run'
2635
- // examples: 'typecheck',
2636
2133
  },
2134
+ });
2135
+ `;
2136
+ };
2137
+ var buildWorkflowTemplate = () => {
2138
+ return `name: DocCov
2637
2139
 
2638
- // Quality rule severities: 'error' | 'warn' | 'off'
2639
- quality: {
2640
- rules: {
2641
- // 'has-description': 'warn',
2642
- // 'has-params': 'off',
2643
- // 'has-returns': 'off',
2644
- // 'has-examples': 'off',
2645
- // 'no-empty-returns': 'warn',
2646
- // 'consistent-param-style': 'off',
2647
- },
2648
- },
2649
- }`;
2650
- if (format === "cjs") {
2651
- return [
2652
- "const { defineConfig } = require('@doccov/cli/config');",
2653
- "",
2654
- `module.exports = defineConfig(${configBody});`,
2655
- ""
2656
- ].join(`
2657
- `);
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 {}
2658
2176
  }
2659
- return [
2660
- "import { defineConfig } from '@doccov/cli/config';",
2661
- "",
2662
- `export default defineConfig(${configBody});`,
2663
- ""
2664
- ].join(`
2665
- `);
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;
2666
2188
  };
2667
2189
 
2668
2190
  // src/commands/spec.ts
@@ -2670,9 +2192,9 @@ import * as fs5 from "node:fs";
2670
2192
  import * as path7 from "node:path";
2671
2193
  import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2672
2194
  import { normalize, validateSpec } from "@openpkg-ts/spec";
2673
- import chalk9 from "chalk";
2195
+ import chalk8 from "chalk";
2674
2196
  // package.json
2675
- var version = "0.17.0";
2197
+ var version = "0.19.0";
2676
2198
 
2677
2199
  // src/commands/spec.ts
2678
2200
  var defaultDependencies4 = {
@@ -2687,7 +2209,7 @@ function getArrayLength(value) {
2687
2209
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2688
2210
  const location = diagnostic.location;
2689
2211
  const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
2690
- const locationText = location && relativePath ? chalk9.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2212
+ const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2691
2213
  const locationPrefix = locationText ? `${locationText} ` : "";
2692
2214
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
2693
2215
  }
@@ -2696,7 +2218,7 @@ function registerSpecCommand(program, dependencies = {}) {
2696
2218
  ...defaultDependencies4,
2697
2219
  ...dependencies
2698
2220
  };
2699
- 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("--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) => {
2700
2222
  try {
2701
2223
  const steps = new StepProgress([
2702
2224
  { label: "Resolved target", activeLabel: "Resolving target" },
@@ -2718,7 +2240,7 @@ function registerSpecCommand(program, dependencies = {}) {
2718
2240
  try {
2719
2241
  config = await loadDocCovConfig(targetDir);
2720
2242
  } catch (configError) {
2721
- error(chalk9.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2243
+ error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2722
2244
  process.exit(1);
2723
2245
  }
2724
2246
  steps.next();
@@ -2733,7 +2255,8 @@ function registerSpecCommand(program, dependencies = {}) {
2733
2255
  resolveExternalTypes,
2734
2256
  maxDepth: options.maxTypeDepth ? parseInt(options.maxTypeDepth, 10) : undefined,
2735
2257
  useCache: options.cache !== false,
2736
- cwd: options.cwd
2258
+ cwd: options.cwd,
2259
+ schemaExtraction: options.runtime ? "hybrid" : "static"
2737
2260
  });
2738
2261
  const generationInput = {
2739
2262
  entryPoint: path7.relative(targetDir, entryFile),
@@ -2761,9 +2284,9 @@ function registerSpecCommand(program, dependencies = {}) {
2761
2284
  const normalized = normalize(result.spec);
2762
2285
  const validation = validateSpec(normalized);
2763
2286
  if (!validation.ok) {
2764
- error(chalk9.red("Spec failed schema validation"));
2287
+ error(chalk8.red("Spec failed schema validation"));
2765
2288
  for (const err of validation.errors) {
2766
- error(chalk9.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2289
+ error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2767
2290
  }
2768
2291
  process.exit(1);
2769
2292
  }
@@ -2778,55 +2301,62 @@ function registerSpecCommand(program, dependencies = {}) {
2778
2301
  writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2779
2302
  steps.complete(`Generated ${options.output}`);
2780
2303
  }
2781
- log(chalk9.gray(` ${getArrayLength(normalized.exports)} exports`));
2782
- log(chalk9.gray(` ${getArrayLength(normalized.types)} types`));
2304
+ log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2305
+ log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
2783
2306
  if (options.verbose && normalized.generation) {
2784
2307
  const gen = normalized.generation;
2785
2308
  log("");
2786
- log(chalk9.bold("Generation Info"));
2787
- log(chalk9.gray(` Timestamp: ${gen.timestamp}`));
2788
- log(chalk9.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2789
- log(chalk9.gray(` Entry point: ${gen.analysis.entryPoint}`));
2790
- log(chalk9.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2791
- log(chalk9.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2792
- log(chalk9.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
2309
+ log(chalk8.bold("Generation Info"));
2310
+ log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
2311
+ log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2312
+ log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
2313
+ log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2314
+ log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2315
+ log(chalk8.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
2793
2316
  if (gen.analysis.maxTypeDepth) {
2794
- log(chalk9.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2317
+ log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
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
+ }
2795
2325
  }
2796
2326
  log("");
2797
- log(chalk9.bold("Environment"));
2798
- log(chalk9.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2327
+ log(chalk8.bold("Environment"));
2328
+ log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2799
2329
  if (gen.environment.packageManager) {
2800
- log(chalk9.gray(` Package manager: ${gen.environment.packageManager}`));
2330
+ log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
2801
2331
  }
2802
2332
  if (gen.environment.isMonorepo) {
2803
- log(chalk9.gray(` Monorepo: yes`));
2333
+ log(chalk8.gray(` Monorepo: yes`));
2804
2334
  }
2805
2335
  if (gen.environment.targetPackage) {
2806
- log(chalk9.gray(` Target package: ${gen.environment.targetPackage}`));
2336
+ log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
2807
2337
  }
2808
2338
  if (gen.issues.length > 0) {
2809
2339
  log("");
2810
- log(chalk9.bold("Issues"));
2340
+ log(chalk8.bold("Issues"));
2811
2341
  for (const issue of gen.issues) {
2812
- const prefix = issue.severity === "error" ? chalk9.red(">") : issue.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
2342
+ const prefix = issue.severity === "error" ? chalk8.red(">") : issue.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2813
2343
  log(`${prefix} [${issue.code}] ${issue.message}`);
2814
2344
  if (issue.suggestion) {
2815
- log(chalk9.gray(` ${issue.suggestion}`));
2345
+ log(chalk8.gray(` ${issue.suggestion}`));
2816
2346
  }
2817
2347
  }
2818
2348
  }
2819
2349
  }
2820
2350
  if (options.showDiagnostics && result.diagnostics.length > 0) {
2821
2351
  log("");
2822
- log(chalk9.bold("Diagnostics"));
2352
+ log(chalk8.bold("Diagnostics"));
2823
2353
  for (const diagnostic of result.diagnostics) {
2824
- const prefix = diagnostic.severity === "error" ? chalk9.red(">") : diagnostic.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
2354
+ const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2825
2355
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2826
2356
  }
2827
2357
  }
2828
2358
  } catch (commandError) {
2829
- error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2359
+ error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2830
2360
  process.exit(1);
2831
2361
  }
2832
2362
  });
@@ -2846,7 +2376,7 @@ import {
2846
2376
  RETENTION_DAYS,
2847
2377
  saveSnapshot
2848
2378
  } from "@doccov/sdk";
2849
- import chalk10 from "chalk";
2379
+ import chalk9 from "chalk";
2850
2380
  function formatDate(timestamp) {
2851
2381
  const date = new Date(timestamp);
2852
2382
  return date.toLocaleDateString("en-US", {
@@ -2859,19 +2389,19 @@ function formatDate(timestamp) {
2859
2389
  }
2860
2390
  function getColorForScore(score) {
2861
2391
  if (score >= 90)
2862
- return chalk10.green;
2392
+ return chalk9.green;
2863
2393
  if (score >= 70)
2864
- return chalk10.yellow;
2394
+ return chalk9.yellow;
2865
2395
  if (score >= 50)
2866
- return chalk10.hex("#FFA500");
2867
- return chalk10.red;
2396
+ return chalk9.hex("#FFA500");
2397
+ return chalk9.red;
2868
2398
  }
2869
2399
  function formatSnapshot(snapshot) {
2870
2400
  const color = getColorForScore(snapshot.coverageScore);
2871
2401
  const date = formatDate(snapshot.timestamp);
2872
2402
  const version2 = snapshot.version ? ` v${snapshot.version}` : "";
2873
- const commit = snapshot.commit ? chalk10.gray(` (${snapshot.commit.slice(0, 7)})`) : "";
2874
- return `${chalk10.gray(date)} ${color(`${snapshot.coverageScore}%`)} ${snapshot.documentedExports}/${snapshot.totalExports} exports${version2}${commit}`;
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}`;
2875
2405
  }
2876
2406
  function formatWeekDate(timestamp) {
2877
2407
  const date = new Date(timestamp);
@@ -2879,10 +2409,10 @@ function formatWeekDate(timestamp) {
2879
2409
  }
2880
2410
  function formatVelocity(velocity) {
2881
2411
  if (velocity > 0)
2882
- return chalk10.green(`+${velocity}%/day`);
2412
+ return chalk9.green(`+${velocity}%/day`);
2883
2413
  if (velocity < 0)
2884
- return chalk10.red(`${velocity}%/day`);
2885
- return chalk10.gray("0%/day");
2414
+ return chalk9.red(`${velocity}%/day`);
2415
+ return chalk9.gray("0%/day");
2886
2416
  }
2887
2417
  function registerTrendsCommand(program) {
2888
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) => {
@@ -2892,17 +2422,17 @@ function registerTrendsCommand(program) {
2892
2422
  const keepCount = parseInt(options.prune, 10);
2893
2423
  if (!isNaN(keepCount)) {
2894
2424
  const deleted = pruneHistory(cwd, keepCount);
2895
- console.log(chalk10.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2425
+ console.log(chalk9.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2896
2426
  } else {
2897
2427
  const deleted = pruneByTier(cwd, tier);
2898
- console.log(chalk10.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2428
+ console.log(chalk9.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2899
2429
  }
2900
2430
  return;
2901
2431
  }
2902
2432
  if (options.record) {
2903
2433
  const specPath = path8.resolve(cwd, "openpkg.json");
2904
2434
  if (!fs6.existsSync(specPath)) {
2905
- console.error(chalk10.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2435
+ console.error(chalk9.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2906
2436
  process.exit(1);
2907
2437
  }
2908
2438
  try {
@@ -2910,24 +2440,24 @@ function registerTrendsCommand(program) {
2910
2440
  const spec = JSON.parse(specContent);
2911
2441
  const trend = getTrend(spec, cwd);
2912
2442
  saveSnapshot(trend.current, cwd);
2913
- console.log(chalk10.green("Recorded coverage snapshot:"));
2443
+ console.log(chalk9.green("Recorded coverage snapshot:"));
2914
2444
  console.log(formatSnapshot(trend.current));
2915
2445
  if (trend.delta !== undefined) {
2916
2446
  const deltaStr = formatDelta2(trend.delta);
2917
- const deltaColor = trend.delta > 0 ? chalk10.green : trend.delta < 0 ? chalk10.red : chalk10.gray;
2918
- console.log(chalk10.gray("Change from previous:"), deltaColor(deltaStr));
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));
2919
2449
  }
2920
2450
  return;
2921
2451
  } catch (error) {
2922
- console.error(chalk10.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2452
+ console.error(chalk9.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2923
2453
  process.exit(1);
2924
2454
  }
2925
2455
  }
2926
2456
  const snapshots = loadSnapshots(cwd);
2927
2457
  const limit = parseInt(options.limit ?? "10", 10);
2928
2458
  if (snapshots.length === 0) {
2929
- console.log(chalk10.yellow("No coverage history found."));
2930
- console.log(chalk10.gray("Run `doccov trends --record` to save the current coverage."));
2459
+ console.log(chalk9.yellow("No coverage history found."));
2460
+ console.log(chalk9.gray("Run `doccov trends --record` to save the current coverage."));
2931
2461
  return;
2932
2462
  }
2933
2463
  if (options.json) {
@@ -2942,17 +2472,17 @@ function registerTrendsCommand(program) {
2942
2472
  }
2943
2473
  const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
2944
2474
  const sparkline = renderSparkline(sparklineData);
2945
- console.log(chalk10.bold("Coverage Trends"));
2946
- console.log(chalk10.gray(`Package: ${snapshots[0].package}`));
2947
- console.log(chalk10.gray(`Sparkline: ${sparkline}`));
2475
+ console.log(chalk9.bold("Coverage Trends"));
2476
+ console.log(chalk9.gray(`Package: ${snapshots[0].package}`));
2477
+ console.log(chalk9.gray(`Sparkline: ${sparkline}`));
2948
2478
  console.log("");
2949
2479
  if (snapshots.length >= 2) {
2950
2480
  const oldest = snapshots[snapshots.length - 1];
2951
2481
  const newest = snapshots[0];
2952
2482
  const overallDelta = newest.coverageScore - oldest.coverageScore;
2953
2483
  const deltaStr = formatDelta2(overallDelta);
2954
- const deltaColor = overallDelta > 0 ? chalk10.green : overallDelta < 0 ? chalk10.red : chalk10.gray;
2955
- console.log(chalk10.gray("Overall trend:"), deltaColor(deltaStr), chalk10.gray(`(${snapshots.length} snapshots)`));
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)`));
2956
2486
  console.log("");
2957
2487
  }
2958
2488
  if (options.extended) {
@@ -2962,8 +2492,8 @@ function registerTrendsCommand(program) {
2962
2492
  const specContent = fs6.readFileSync(specPath, "utf-8");
2963
2493
  const spec = JSON.parse(specContent);
2964
2494
  const extended = getExtendedTrend(spec, cwd, { tier });
2965
- console.log(chalk10.bold("Extended Analysis"));
2966
- console.log(chalk10.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
2495
+ console.log(chalk9.bold("Extended Analysis"));
2496
+ console.log(chalk9.gray(`Tier: ${tier} (${RETENTION_DAYS[tier]}-day retention)`));
2967
2497
  console.log("");
2968
2498
  console.log(" Velocity:");
2969
2499
  console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
@@ -2972,47 +2502,47 @@ function registerTrendsCommand(program) {
2972
2502
  console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
2973
2503
  }
2974
2504
  console.log("");
2975
- const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk10.green : chalk10.red;
2505
+ const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk9.green : chalk9.red;
2976
2506
  console.log(` Projected (30d): ${projColor(`${extended.projected30d}%`)}`);
2977
- console.log(` All-time high: ${chalk10.green(`${extended.allTimeHigh}%`)}`);
2978
- console.log(` All-time low: ${chalk10.red(`${extended.allTimeLow}%`)}`);
2507
+ console.log(` All-time high: ${chalk9.green(`${extended.allTimeHigh}%`)}`);
2508
+ console.log(` All-time low: ${chalk9.red(`${extended.allTimeLow}%`)}`);
2979
2509
  if (extended.dataRange) {
2980
2510
  const startDate = formatWeekDate(extended.dataRange.start);
2981
2511
  const endDate = formatWeekDate(extended.dataRange.end);
2982
- console.log(chalk10.gray(` Data range: ${startDate} - ${endDate}`));
2512
+ console.log(chalk9.gray(` Data range: ${startDate} - ${endDate}`));
2983
2513
  }
2984
2514
  console.log("");
2985
2515
  if (options.weekly && extended.weeklySummaries.length > 0) {
2986
- console.log(chalk10.bold("Weekly Summary"));
2516
+ console.log(chalk9.bold("Weekly Summary"));
2987
2517
  const weekLimit = Math.min(extended.weeklySummaries.length, 8);
2988
2518
  for (let i = 0;i < weekLimit; i++) {
2989
2519
  const week = extended.weeklySummaries[i];
2990
2520
  const weekStart = formatWeekDate(week.weekStart);
2991
2521
  const weekEnd = formatWeekDate(week.weekEnd);
2992
- const deltaColor = week.delta > 0 ? chalk10.green : week.delta < 0 ? chalk10.red : chalk10.gray;
2522
+ const deltaColor = week.delta > 0 ? chalk9.green : week.delta < 0 ? chalk9.red : chalk9.gray;
2993
2523
  const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
2994
2524
  console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
2995
2525
  }
2996
2526
  if (extended.weeklySummaries.length > weekLimit) {
2997
- console.log(chalk10.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2527
+ console.log(chalk9.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2998
2528
  }
2999
2529
  console.log("");
3000
2530
  }
3001
2531
  } catch {
3002
- console.log(chalk10.yellow("Could not load openpkg.json for extended analysis"));
2532
+ console.log(chalk9.yellow("Could not load openpkg.json for extended analysis"));
3003
2533
  console.log("");
3004
2534
  }
3005
2535
  }
3006
2536
  }
3007
- console.log(chalk10.bold("History"));
2537
+ console.log(chalk9.bold("History"));
3008
2538
  const displaySnapshots = snapshots.slice(0, limit);
3009
2539
  for (let i = 0;i < displaySnapshots.length; i++) {
3010
2540
  const snapshot = displaySnapshots[i];
3011
- const prefix = i === 0 ? chalk10.cyan("→") : " ";
2541
+ const prefix = i === 0 ? chalk9.cyan("→") : " ";
3012
2542
  console.log(`${prefix} ${formatSnapshot(snapshot)}`);
3013
2543
  }
3014
2544
  if (snapshots.length > limit) {
3015
- console.log(chalk10.gray(` ... and ${snapshots.length - limit} more`));
2545
+ console.log(chalk9.gray(` ... and ${snapshots.length - limit} more`));
3016
2546
  }
3017
2547
  });
3018
2548
  }
@@ -3020,7 +2550,7 @@ function registerTrendsCommand(program) {
3020
2550
  // src/cli.ts
3021
2551
  var __filename2 = fileURLToPath(import.meta.url);
3022
2552
  var __dirname2 = path9.dirname(__filename2);
3023
- var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
2553
+ var packageJson = JSON.parse(readFileSync5(path9.join(__dirname2, "../package.json"), "utf-8"));
3024
2554
  var program = new Command;
3025
2555
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
3026
2556
  registerCheckCommand(program);