@doccov/cli 0.17.0 → 0.18.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
@@ -35,13 +35,20 @@ var checkConfigSchema = z.object({
35
35
  var qualityConfigSchema = z.object({
36
36
  rules: z.record(severitySchema).optional()
37
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
+ });
38
44
  var docCovConfigSchema = z.object({
39
45
  include: stringList.optional(),
40
46
  exclude: stringList.optional(),
41
47
  plugins: z.array(z.unknown()).optional(),
42
48
  docs: docsConfigSchema.optional(),
43
49
  check: checkConfigSchema.optional(),
44
- quality: qualityConfigSchema.optional()
50
+ quality: qualityConfigSchema.optional(),
51
+ policies: z.array(policyConfigSchema).optional()
45
52
  });
46
53
  var normalizeList = (value) => {
47
54
  if (!value) {
@@ -79,13 +86,15 @@ var normalizeConfig = (input) => {
79
86
  rules: input.quality.rules
80
87
  };
81
88
  }
89
+ const policies = input.policies;
82
90
  return {
83
91
  include,
84
92
  exclude,
85
93
  plugins: input.plugins,
86
94
  docs,
87
95
  check,
88
- quality
96
+ quality,
97
+ policies
89
98
  };
90
99
  };
91
100
 
@@ -168,8 +177,8 @@ ${formatIssues(issues)}`);
168
177
  // src/config/index.ts
169
178
  var defineConfig = (config) => config;
170
179
  // src/cli.ts
171
- import { readFileSync as readFileSync3 } from "node:fs";
172
- import * as path8 from "node:path";
180
+ import { readFileSync as readFileSync4 } from "node:fs";
181
+ import * as path9 from "node:path";
173
182
  import { fileURLToPath } from "node:url";
174
183
  import { Command } from "commander";
175
184
 
@@ -177,11 +186,14 @@ import { Command } from "commander";
177
186
  import * as fs2 from "node:fs";
178
187
  import * as path4 from "node:path";
179
188
  import {
189
+ analyzeSpecContributors,
190
+ analyzeSpecOwnership,
180
191
  applyEdits,
181
192
  categorizeDrifts,
182
193
  createSourceFile,
183
194
  DocCov,
184
195
  enrichSpec,
196
+ evaluatePolicies,
185
197
  findJSDocLocation,
186
198
  generateFixesForExport,
187
199
  generateReport,
@@ -196,8 +208,75 @@ import {
196
208
  import {
197
209
  DRIFT_CATEGORIES as DRIFT_CATEGORIES2
198
210
  } from "@openpkg-ts/spec";
199
- import chalk3 from "chalk";
211
+ import chalk5 from "chalk";
200
212
 
213
+ // src/reports/changelog-renderer.ts
214
+ function renderChangelog(data, options = {}) {
215
+ const { diff, categorizedBreaking } = data;
216
+ const lines = [];
217
+ const version = options.version ?? data.version ?? "Unreleased";
218
+ const date = options.date instanceof Date ? options.date.toISOString().split("T")[0] : options.date ?? new Date().toISOString().split("T")[0];
219
+ lines.push(`## [${version}] - ${date}`);
220
+ lines.push("");
221
+ if (diff.breaking.length > 0) {
222
+ lines.push("### ⚠️ BREAKING CHANGES");
223
+ lines.push("");
224
+ if (categorizedBreaking && categorizedBreaking.length > 0) {
225
+ for (const breaking of categorizedBreaking) {
226
+ const severity = breaking.severity === "high" ? "**" : "";
227
+ lines.push(`- ${severity}${breaking.name}${severity}: ${breaking.reason}`);
228
+ }
229
+ } else {
230
+ for (const id of diff.breaking) {
231
+ lines.push(`- \`${id}\` removed or changed`);
232
+ }
233
+ }
234
+ lines.push("");
235
+ }
236
+ if (diff.nonBreaking.length > 0) {
237
+ lines.push("### Added");
238
+ lines.push("");
239
+ for (const id of diff.nonBreaking) {
240
+ lines.push(`- \`${id}\``);
241
+ }
242
+ lines.push("");
243
+ }
244
+ if (diff.docsOnly.length > 0) {
245
+ lines.push("### Documentation");
246
+ lines.push("");
247
+ for (const id of diff.docsOnly) {
248
+ lines.push(`- Updated documentation for \`${id}\``);
249
+ }
250
+ lines.push("");
251
+ }
252
+ if (diff.coverageDelta !== 0) {
253
+ lines.push("### Coverage");
254
+ lines.push("");
255
+ const arrow = diff.coverageDelta > 0 ? "↑" : "↓";
256
+ const sign = diff.coverageDelta > 0 ? "+" : "";
257
+ lines.push(`- Documentation coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% (${arrow} ${sign}${diff.coverageDelta}%)`);
258
+ lines.push("");
259
+ }
260
+ if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
261
+ if (!lines.some((l) => l.startsWith("### Coverage"))) {
262
+ lines.push("### Coverage");
263
+ lines.push("");
264
+ }
265
+ if (diff.driftResolved > 0) {
266
+ lines.push(`- Fixed ${diff.driftResolved} drift issue${diff.driftResolved === 1 ? "" : "s"}`);
267
+ }
268
+ if (diff.driftIntroduced > 0) {
269
+ lines.push(`- ${diff.driftIntroduced} new drift issue${diff.driftIntroduced === 1 ? "" : "s"} detected`);
270
+ }
271
+ lines.push("");
272
+ }
273
+ if (options.compareUrl) {
274
+ lines.push(`**Full Changelog**: ${options.compareUrl}`);
275
+ lines.push("");
276
+ }
277
+ return lines.join(`
278
+ `);
279
+ }
201
280
  // src/reports/diff-markdown.ts
202
281
  import * as path2 from "node:path";
203
282
  function bar(pct, width = 10) {
@@ -899,19 +978,336 @@ function writeReports(options) {
899
978
  }));
900
979
  return results;
901
980
  }
902
- // src/utils/llm-assertion-parser.ts
981
+ // src/utils/ai-client.ts
982
+ import chalk2 from "chalk";
983
+
984
+ // src/utils/ai-generate.ts
903
985
  import { createAnthropic } from "@ai-sdk/anthropic";
904
986
  import { createOpenAI } from "@ai-sdk/openai";
905
987
  import { generateObject } from "ai";
906
988
  import { z as z2 } from "zod";
907
- var AssertionParseSchema = z2.object({
908
- assertions: z2.array(z2.object({
909
- lineNumber: z2.number().describe("1-indexed line number where the assertion appears"),
910
- expected: z2.string().describe("The expected output value"),
911
- originalComment: z2.string().describe("The original comment text"),
912
- suggestedSyntax: z2.string().describe("The line rewritten with standard // => value syntax")
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
+ // src/utils/filter-options.ts
1248
+ import { mergeFilters, parseListFlag } from "@doccov/sdk";
1249
+ import chalk3 from "chalk";
1250
+ var parseVisibilityFlag = (value) => {
1251
+ if (!value)
1252
+ return;
1253
+ const validTags = ["public", "beta", "alpha", "internal"];
1254
+ const parsed = parseListFlag(value);
1255
+ if (!parsed)
1256
+ return;
1257
+ const result = [];
1258
+ for (const tag of parsed) {
1259
+ const lower = tag.toLowerCase();
1260
+ if (validTags.includes(lower)) {
1261
+ result.push(lower);
1262
+ }
1263
+ }
1264
+ return result.length > 0 ? result : undefined;
1265
+ };
1266
+ var formatList = (label, values) => `${label}: ${values.map((value) => chalk3.cyan(value)).join(", ")}`;
1267
+ var mergeFilterOptions = (config, cliOptions) => {
1268
+ const messages = [];
1269
+ if (config?.include) {
1270
+ messages.push(formatList("include filters from config", config.include));
1271
+ }
1272
+ if (config?.exclude) {
1273
+ messages.push(formatList("exclude filters from config", config.exclude));
1274
+ }
1275
+ if (cliOptions.include) {
1276
+ messages.push(formatList("apply include filters from CLI", cliOptions.include));
1277
+ }
1278
+ if (cliOptions.exclude) {
1279
+ messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
1280
+ }
1281
+ if (cliOptions.visibility) {
1282
+ messages.push(formatList("apply visibility filter from CLI", cliOptions.visibility));
1283
+ }
1284
+ const resolved = mergeFilters(config, cliOptions);
1285
+ if (!resolved.include && !resolved.exclude && !cliOptions.visibility) {
1286
+ return { messages };
1287
+ }
1288
+ const source = resolved.source === "override" ? "cli" : resolved.source;
1289
+ return {
1290
+ include: resolved.include,
1291
+ exclude: resolved.exclude,
1292
+ visibility: cliOptions.visibility,
1293
+ source,
1294
+ messages
1295
+ };
1296
+ };
1297
+
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")
913
1309
  })).describe("List of assertion-like comments found in the code"),
914
- hasAssertions: z2.boolean().describe("Whether any assertion-like comments were found")
1310
+ hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
915
1311
  });
916
1312
  var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
917
1313
 
@@ -938,13 +1334,13 @@ Code:
938
1334
  \`\`\`
939
1335
  ${code}
940
1336
  \`\`\``;
941
- function getModel() {
1337
+ function getModel2() {
942
1338
  const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
943
1339
  if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
944
- const anthropic = createAnthropic();
1340
+ const anthropic = createAnthropic2();
945
1341
  return anthropic("claude-sonnet-4-20250514");
946
1342
  }
947
- const openai = createOpenAI();
1343
+ const openai = createOpenAI2();
948
1344
  return openai("gpt-4o-mini");
949
1345
  }
950
1346
  function isLLMAssertionParsingAvailable() {
@@ -955,8 +1351,8 @@ async function parseAssertionsWithLLM(code) {
955
1351
  return null;
956
1352
  }
957
1353
  try {
958
- const model = getModel();
959
- const { object } = await generateObject({
1354
+ const model = getModel2();
1355
+ const { object } = await generateObject2({
960
1356
  model,
961
1357
  schema: AssertionParseSchema,
962
1358
  prompt: ASSERTION_PARSE_PROMPT(code)
@@ -968,7 +1364,7 @@ async function parseAssertionsWithLLM(code) {
968
1364
  }
969
1365
 
970
1366
  // src/utils/progress.ts
971
- import chalk2 from "chalk";
1367
+ import chalk4 from "chalk";
972
1368
  class StepProgress {
973
1369
  steps;
974
1370
  currentStep = 0;
@@ -996,7 +1392,7 @@ class StepProgress {
996
1392
  this.completeCurrentStep();
997
1393
  if (message) {
998
1394
  const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
999
- console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
1395
+ console.log(`${chalk4.green("✓")} ${message} ${chalk4.dim(`(${elapsed}s)`)}`);
1000
1396
  }
1001
1397
  }
1002
1398
  render() {
@@ -1004,17 +1400,17 @@ class StepProgress {
1004
1400
  if (!step)
1005
1401
  return;
1006
1402
  const label = step.activeLabel ?? step.label;
1007
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1008
- process.stdout.write(`\r${prefix} ${chalk2.cyan(label)}...`);
1403
+ const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1404
+ process.stdout.write(`\r${prefix} ${chalk4.cyan(label)}...`);
1009
1405
  }
1010
1406
  completeCurrentStep() {
1011
1407
  const step = this.steps[this.currentStep];
1012
1408
  if (!step)
1013
1409
  return;
1014
1410
  const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
1015
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1411
+ const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1016
1412
  process.stdout.write(`\r${" ".repeat(80)}\r`);
1017
- console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
1413
+ console.log(`${prefix} ${step.label} ${chalk4.green("✓")} ${chalk4.dim(`(${elapsed}s)`)}`);
1018
1414
  }
1019
1415
  }
1020
1416
 
@@ -1058,7 +1454,7 @@ function registerCheckCommand(program, dependencies = {}) {
1058
1454
  ...defaultDependencies,
1059
1455
  ...dependencies
1060
1456
  };
1061
- program.command("check [entry]").description("Check documentation coverage and output reports").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--min-coverage <percentage>", "Minimum docs coverage percentage (0-100)", (value) => Number(value)).option("--max-drift <percentage>", "Maximum drift percentage allowed (0-100)", (value) => Number(value)).option("--examples [mode]", "Example validation: presence, typecheck, run (comma-separated). Bare flag runs all.").option("--skip-resolve", "Skip external type resolution from node_modules").option("--fix", "Auto-fix drift issues").option("--write", "Alias for --fix").option("--dry-run", "Preview fixes without writing (requires --fix)").option("--format <format>", "Output format: text, json, markdown, html, github", "text").option("-o, --output <file>", "Custom output path (overrides default .doccov/ path)").option("--stdout", "Output to stdout instead of writing to .doccov/").option("--update-snapshot", "Force regenerate .doccov/report.json").option("--limit <n>", "Max exports to show in report tables", "20").option("--max-type-depth <number>", "Maximum depth for type conversion (default: 20)").option("--no-cache", "Bypass spec cache and force regeneration").action(async (entry, options) => {
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) => {
1062
1458
  try {
1063
1459
  const validations = parseExamplesFlag(options.examples);
1064
1460
  const hasExamples = validations.length > 0;
@@ -1085,6 +1481,15 @@ function registerCheckCommand(program, dependencies = {}) {
1085
1481
  const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
1086
1482
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
1087
1483
  const maxDrift = maxDriftRaw !== undefined ? clampPercentage(maxDriftRaw) : undefined;
1484
+ const cliFilters = {
1485
+ include: undefined,
1486
+ exclude: undefined,
1487
+ visibility: parseVisibilityFlag(options.visibility)
1488
+ };
1489
+ const resolvedFilters = mergeFilterOptions(config, cliFilters);
1490
+ if (resolvedFilters.visibility) {
1491
+ log(chalk5.dim(`Filtering by visibility: ${resolvedFilters.visibility.join(", ")}`));
1492
+ }
1088
1493
  steps.next();
1089
1494
  const resolveExternalTypes = !options.skipResolve;
1090
1495
  let specResult;
@@ -1094,7 +1499,8 @@ function registerCheckCommand(program, dependencies = {}) {
1094
1499
  useCache: options.cache !== false,
1095
1500
  cwd: options.cwd
1096
1501
  });
1097
- specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
1502
+ const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
1503
+ specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1098
1504
  if (!specResult) {
1099
1505
  throw new Error("Failed to analyze documentation coverage.");
1100
1506
  }
@@ -1111,6 +1517,24 @@ function registerCheckCommand(program, dependencies = {}) {
1111
1517
  violations.push({ exportName: exp.name, violation: v });
1112
1518
  }
1113
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
+ }
1114
1538
  let exampleResult;
1115
1539
  const typecheckErrors = [];
1116
1540
  const runtimeDrifts = [];
@@ -1156,12 +1580,12 @@ function registerCheckCommand(program, dependencies = {}) {
1156
1580
  if (allDrifts.length > 0) {
1157
1581
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1158
1582
  if (fixable.length === 0) {
1159
- log(chalk3.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1583
+ log(chalk5.yellow(`Found ${nonFixable.length} drift issue(s), but none are auto-fixable.`));
1160
1584
  } else {
1161
1585
  log("");
1162
- log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
1586
+ log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
1163
1587
  if (nonFixable.length > 0) {
1164
- log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1588
+ log(chalk5.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1165
1589
  }
1166
1590
  log("");
1167
1591
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -1169,22 +1593,22 @@ function registerCheckCommand(program, dependencies = {}) {
1169
1593
  const editsByFile = new Map;
1170
1594
  for (const [exp, drifts] of groupedDrifts) {
1171
1595
  if (!exp.source?.file) {
1172
- log(chalk3.gray(` Skipping ${exp.name}: no source location`));
1596
+ log(chalk5.gray(` Skipping ${exp.name}: no source location`));
1173
1597
  continue;
1174
1598
  }
1175
1599
  if (exp.source.file.endsWith(".d.ts")) {
1176
- log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
1600
+ log(chalk5.gray(` Skipping ${exp.name}: declaration file`));
1177
1601
  continue;
1178
1602
  }
1179
1603
  const filePath = path4.resolve(targetDir, exp.source.file);
1180
1604
  if (!fs2.existsSync(filePath)) {
1181
- log(chalk3.gray(` Skipping ${exp.name}: file not found`));
1605
+ log(chalk5.gray(` Skipping ${exp.name}: file not found`));
1182
1606
  continue;
1183
1607
  }
1184
1608
  const sourceFile = createSourceFile(filePath);
1185
1609
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1186
1610
  if (!location) {
1187
- log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
1611
+ log(chalk5.gray(` Skipping ${exp.name}: could not find declaration`));
1188
1612
  continue;
1189
1613
  }
1190
1614
  let existingPatch = {};
@@ -1217,26 +1641,26 @@ function registerCheckCommand(program, dependencies = {}) {
1217
1641
  }
1218
1642
  if (edits.length > 0) {
1219
1643
  if (options.dryRun) {
1220
- log(chalk3.bold("Dry run - changes that would be made:"));
1644
+ log(chalk5.bold("Dry run - changes that would be made:"));
1221
1645
  log("");
1222
1646
  for (const [filePath, fileEdits] of editsByFile) {
1223
1647
  const relativePath = path4.relative(targetDir, filePath);
1224
- log(chalk3.cyan(` ${relativePath}:`));
1648
+ log(chalk5.cyan(` ${relativePath}:`));
1225
1649
  for (const { export: exp, edit, fixes } of fileEdits) {
1226
1650
  const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
1227
- log(` ${chalk3.bold(exp.name)} [${lineInfo}]`);
1651
+ log(` ${chalk5.bold(exp.name)} [${lineInfo}]`);
1228
1652
  for (const fix of fixes) {
1229
- log(chalk3.green(` + ${fix.description}`));
1653
+ log(chalk5.green(` + ${fix.description}`));
1230
1654
  }
1231
1655
  }
1232
1656
  log("");
1233
1657
  }
1234
- log(chalk3.gray("Run without --dry-run to apply these changes."));
1658
+ log(chalk5.gray("Run without --dry-run to apply these changes."));
1235
1659
  } else {
1236
1660
  const applyResult = await applyEdits(edits);
1237
1661
  if (applyResult.errors.length > 0) {
1238
1662
  for (const err of applyResult.errors) {
1239
- error(chalk3.red(` ${err.file}: ${err.error}`));
1663
+ error(chalk5.red(` ${err.file}: ${err.error}`));
1240
1664
  }
1241
1665
  }
1242
1666
  }
@@ -1247,6 +1671,99 @@ function registerCheckCommand(program, dependencies = {}) {
1247
1671
  driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1248
1672
  }
1249
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
+ }
1747
+ 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
+ }
1761
+ }
1762
+ }
1763
+ }
1764
+ }
1765
+ }
1766
+ }
1250
1767
  steps.complete("Check complete");
1251
1768
  if (format !== "text") {
1252
1769
  const limit = parseInt(options.limit, 10) || 20;
@@ -1292,7 +1809,8 @@ function registerCheckCommand(program, dependencies = {}) {
1292
1809
  const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
1293
1810
  const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
1294
1811
  const hasTypecheckErrors2 = typecheckErrors.length > 0;
1295
- if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2) {
1812
+ const policiesFailed2 = policyResult && !policyResult.allPassed;
1813
+ if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
1296
1814
  process.exit(1);
1297
1815
  }
1298
1816
  return;
@@ -1304,18 +1822,19 @@ function registerCheckCommand(program, dependencies = {}) {
1304
1822
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1305
1823
  const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
1306
1824
  const hasTypecheckErrors = typecheckErrors.length > 0;
1825
+ const policiesFailed = policyResult && !policyResult.allPassed;
1307
1826
  if (specWarnings.length > 0 || specInfos.length > 0) {
1308
1827
  log("");
1309
1828
  for (const diag of specWarnings) {
1310
- log(chalk3.yellow(`⚠ ${diag.message}`));
1829
+ log(chalk5.yellow(`⚠ ${diag.message}`));
1311
1830
  if (diag.suggestion) {
1312
- log(chalk3.gray(` ${diag.suggestion}`));
1831
+ log(chalk5.gray(` ${diag.suggestion}`));
1313
1832
  }
1314
1833
  }
1315
1834
  for (const diag of specInfos) {
1316
- log(chalk3.cyan(`ℹ ${diag.message}`));
1835
+ log(chalk5.cyan(`ℹ ${diag.message}`));
1317
1836
  if (diag.suggestion) {
1318
- log(chalk3.gray(` ${diag.suggestion}`));
1837
+ log(chalk5.gray(` ${diag.suggestion}`));
1319
1838
  }
1320
1839
  }
1321
1840
  }
@@ -1325,23 +1844,23 @@ function registerCheckCommand(program, dependencies = {}) {
1325
1844
  const errorCount = violations.filter((v) => v.violation.severity === "error").length;
1326
1845
  const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
1327
1846
  log("");
1328
- log(chalk3.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1847
+ log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1329
1848
  log("");
1330
1849
  log(` Exports: ${totalExports}`);
1331
1850
  if (minCoverage !== undefined) {
1332
1851
  if (coverageFailed) {
1333
- log(chalk3.red(` Coverage: ✗ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1852
+ log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1334
1853
  } else {
1335
- log(chalk3.green(` Coverage: ✓ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1854
+ log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1336
1855
  }
1337
1856
  } else {
1338
1857
  log(` Coverage: ${coverageScore}%`);
1339
1858
  }
1340
1859
  if (maxDrift !== undefined) {
1341
1860
  if (driftFailed) {
1342
- log(chalk3.red(` Drift: ✗ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1861
+ log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1343
1862
  } else {
1344
- log(chalk3.green(` Drift: ✓ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1863
+ log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1345
1864
  }
1346
1865
  } else {
1347
1866
  log(` Drift: ${driftScore}%`);
@@ -1351,7 +1870,7 @@ function registerCheckCommand(program, dependencies = {}) {
1351
1870
  if (typecheckCount > 0) {
1352
1871
  log(` Examples: ${typecheckCount} type errors`);
1353
1872
  } else {
1354
- log(chalk3.green(` Examples: ✓ validated`));
1873
+ log(chalk5.green(` Examples: ✓ validated`));
1355
1874
  }
1356
1875
  }
1357
1876
  if (errorCount > 0 || warnCount > 0) {
@@ -1362,8 +1881,57 @@ function registerCheckCommand(program, dependencies = {}) {
1362
1881
  parts.push(`${warnCount} warnings`);
1363
1882
  log(` Quality: ${parts.join(", ")}`);
1364
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
+ }
1898
+ }
1899
+ }
1900
+ }
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`));
1928
+ }
1929
+ if (contributorResult.unattributed.length > 0) {
1930
+ log(chalk5.dim(` (${contributorResult.unattributed.length} exports could not be attributed via git blame)`));
1931
+ }
1932
+ }
1365
1933
  log("");
1366
- const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
1934
+ const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors || policiesFailed;
1367
1935
  if (!failed) {
1368
1936
  const thresholdParts = [];
1369
1937
  if (minCoverage !== undefined) {
@@ -1372,25 +1940,31 @@ function registerCheckCommand(program, dependencies = {}) {
1372
1940
  if (maxDrift !== undefined) {
1373
1941
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1374
1942
  }
1943
+ if (policyResult) {
1944
+ thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
1945
+ }
1375
1946
  if (thresholdParts.length > 0) {
1376
- log(chalk3.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1947
+ log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1377
1948
  } else {
1378
- log(chalk3.green("✓ Check passed"));
1379
- log(chalk3.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1949
+ log(chalk5.green("✓ Check passed"));
1950
+ log(chalk5.dim(" No thresholds configured. Use --min-coverage or --max-drift to enforce."));
1380
1951
  }
1381
1952
  return;
1382
1953
  }
1383
1954
  if (hasQualityErrors) {
1384
- log(chalk3.red(`✗ ${errorCount} quality errors`));
1955
+ log(chalk5.red(`✗ ${errorCount} quality errors`));
1385
1956
  }
1386
1957
  if (hasTypecheckErrors) {
1387
- log(chalk3.red(`✗ ${typecheckErrors.length} example type errors`));
1958
+ log(chalk5.red(`✗ ${typecheckErrors.length} example type errors`));
1959
+ }
1960
+ if (policiesFailed && policyResult) {
1961
+ log(chalk5.red(`✗ ${policyResult.failedCount} policy failures`));
1388
1962
  }
1389
1963
  log("");
1390
- log(chalk3.dim("Use --format json or --format markdown for detailed reports"));
1964
+ log(chalk5.dim("Use --format json or --format markdown for detailed reports"));
1391
1965
  process.exit(1);
1392
1966
  } catch (commandError) {
1393
- error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1967
+ error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1394
1968
  process.exit(1);
1395
1969
  }
1396
1970
  });
@@ -1427,35 +2001,36 @@ import {
1427
2001
  hashString,
1428
2002
  parseMarkdownFiles
1429
2003
  } from "@doccov/sdk";
1430
- import chalk4 from "chalk";
2004
+ import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
2005
+ import chalk6 from "chalk";
1431
2006
  import { glob } from "glob";
1432
2007
 
1433
2008
  // src/utils/docs-impact-ai.ts
1434
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1435
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1436
- import { generateObject as generateObject2, generateText } from "ai";
1437
- import { z as z3 } from "zod";
1438
- var CodeBlockUsageSchema = z3.object({
1439
- isImpacted: z3.boolean().describe("Whether the code block is affected by the change"),
1440
- reason: z3.string().describe("Explanation of why/why not the code is impacted"),
1441
- usageType: z3.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
1442
- suggestedFix: z3.string().optional().describe("If impacted, the suggested code change"),
1443
- confidence: z3.enum(["high", "medium", "low"]).describe("Confidence level of the analysis")
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")
1444
2019
  });
1445
- var MultiBlockAnalysisSchema = z3.object({
1446
- groups: z3.array(z3.object({
1447
- blockIndices: z3.array(z3.number()).describe("Indices of blocks that should run together"),
1448
- reason: z3.string().describe("Why these blocks are related")
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")
1449
2024
  })).describe("Groups of related code blocks"),
1450
- skippedBlocks: z3.array(z3.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
2025
+ skippedBlocks: z4.array(z4.number()).describe("Indices of blocks that should be skipped (incomplete/illustrative)")
1451
2026
  });
1452
- function getModel2() {
2027
+ function getModel3() {
1453
2028
  const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1454
2029
  if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1455
- const anthropic = createAnthropic2();
2030
+ const anthropic = createAnthropic3();
1456
2031
  return anthropic("claude-sonnet-4-20250514");
1457
2032
  }
1458
- const openai = createOpenAI2();
2033
+ const openai = createOpenAI3();
1459
2034
  return openai("gpt-4o-mini");
1460
2035
  }
1461
2036
  function isAIDocsAnalysisAvailable() {
@@ -1470,7 +2045,7 @@ async function generateImpactSummary(impacts) {
1470
2045
  }
1471
2046
  try {
1472
2047
  const { text } = await generateText({
1473
- model: getModel2(),
2048
+ model: getModel3(),
1474
2049
  prompt: `Summarize these documentation impacts for a GitHub PR comment.
1475
2050
 
1476
2051
  Impacts:
@@ -1515,7 +2090,7 @@ function registerDiffCommand(program, dependencies = {}) {
1515
2090
  ...defaultDependencies2,
1516
2091
  ...dependencies
1517
2092
  };
1518
- program.command("diff [base] [head]").description("Compare two OpenPkg specs and detect breaking changes").option("--base <file>", 'Base spec file (the "before" state)').option("--head <file>", 'Head spec file (the "after" state)').option("--format <format>", "Output format: text, json, markdown, html, github, pr-comment", "text").option("--stdout", "Output to stdout instead of writing to .doccov/").option("-o, --output <file>", "Custom output path").option("--cwd <dir>", "Working directory", process.cwd()).option("--limit <n>", "Max items to show in terminal/reports", "10").option("--repo-url <url>", "GitHub repo URL for file links (pr-comment format)").option("--sha <sha>", "Commit SHA for file links (pr-comment format)").option("--min-coverage <n>", "Minimum coverage % for HEAD spec (0-100)").option("--max-drift <n>", "Maximum drift % for HEAD spec (0-100)").option("--strict <preset>", "Fail on conditions: ci, release, quality").option("--docs <glob>", "Glob pattern for markdown docs to check for impact", collect, []).option("--ai", "Use AI for deeper analysis and fix suggestions").option("--no-cache", "Bypass cache and force regeneration").action(async (baseArg, headArg, options) => {
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) => {
1519
2094
  try {
1520
2095
  const baseFile = options.base ?? baseArg;
1521
2096
  const headFile = options.head ?? headArg;
@@ -1546,6 +2121,29 @@ function registerDiffCommand(program, dependencies = {}) {
1546
2121
  }
1547
2122
  const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
1548
2123
  const maxDrift = resolveThreshold(options.maxDrift, config?.check?.maxDrift);
2124
+ if (options.recommendVersion) {
2125
+ const recommendation = recommendSemverBump(diff);
2126
+ const currentVersion = headSpec.meta?.version ?? "0.0.0";
2127
+ const nextVersion = calculateNextVersion(currentVersion, recommendation.bump);
2128
+ if (options.format === "json") {
2129
+ log(JSON.stringify({
2130
+ current: currentVersion,
2131
+ recommended: nextVersion,
2132
+ bump: recommendation.bump,
2133
+ reason: recommendation.reason,
2134
+ breakingCount: recommendation.breakingCount,
2135
+ additionCount: recommendation.additionCount,
2136
+ docsOnlyChanges: recommendation.docsOnlyChanges
2137
+ }, null, 2));
2138
+ } else {
2139
+ log("");
2140
+ log(chalk6.bold("Semver Recommendation"));
2141
+ log(` Current version: ${currentVersion}`);
2142
+ log(` Recommended: ${chalk6.cyan(nextVersion)} (${chalk6.yellow(recommendation.bump.toUpperCase())})`);
2143
+ log(` Reason: ${recommendation.reason}`);
2144
+ }
2145
+ return;
2146
+ }
1549
2147
  const format = options.format ?? "text";
1550
2148
  const limit = parseInt(options.limit, 10) || 10;
1551
2149
  const checks = getStrictChecks(options.strict);
@@ -1573,8 +2171,8 @@ function registerDiffCommand(program, dependencies = {}) {
1573
2171
  silent: true
1574
2172
  });
1575
2173
  }
1576
- const cacheNote = fromCache ? chalk4.cyan(" (cached)") : "";
1577
- log(chalk4.dim(`Report: ${jsonPath}`) + cacheNote);
2174
+ const cacheNote = fromCache ? chalk6.cyan(" (cached)") : "";
2175
+ log(chalk6.dim(`Report: ${jsonPath}`) + cacheNote);
1578
2176
  }
1579
2177
  break;
1580
2178
  case "json": {
@@ -1635,6 +2233,28 @@ function registerDiffCommand(program, dependencies = {}) {
1635
2233
  log(content);
1636
2234
  break;
1637
2235
  }
2236
+ case "changelog": {
2237
+ const content = renderChangelog({
2238
+ diff,
2239
+ categorizedBreaking: diff.categorizedBreaking,
2240
+ version: headSpec.meta?.version
2241
+ }, {
2242
+ version: headSpec.meta?.version,
2243
+ compareUrl: options.repoUrl ? `${options.repoUrl}/compare/${baseSpec.meta?.version ?? "v0"}...${headSpec.meta?.version ?? "HEAD"}` : undefined
2244
+ });
2245
+ if (options.stdout) {
2246
+ log(content);
2247
+ } else {
2248
+ const outputPath = options.output ?? getDiffReportPath(baseHash, headHash, "md");
2249
+ writeReport({
2250
+ format: "markdown",
2251
+ content,
2252
+ outputPath: outputPath.replace(/\.(json|html)$/, ".changelog.md"),
2253
+ cwd: options.cwd
2254
+ });
2255
+ }
2256
+ break;
2257
+ }
1638
2258
  }
1639
2259
  const failures = validateDiff(diff, headSpec, {
1640
2260
  minCoverage,
@@ -1642,18 +2262,18 @@ function registerDiffCommand(program, dependencies = {}) {
1642
2262
  checks
1643
2263
  });
1644
2264
  if (failures.length > 0) {
1645
- log(chalk4.red(`
2265
+ log(chalk6.red(`
1646
2266
  ✗ Check failed`));
1647
2267
  for (const f of failures) {
1648
- log(chalk4.red(` - ${f}`));
2268
+ log(chalk6.red(` - ${f}`));
1649
2269
  }
1650
2270
  process.exitCode = 1;
1651
2271
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1652
- log(chalk4.green(`
2272
+ log(chalk6.green(`
1653
2273
  ✓ All checks passed`));
1654
2274
  }
1655
2275
  } catch (commandError) {
1656
- error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2276
+ error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1657
2277
  process.exitCode = 1;
1658
2278
  }
1659
2279
  });
@@ -1680,7 +2300,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1680
2300
  if (!docsPatterns || docsPatterns.length === 0) {
1681
2301
  if (config?.docs?.include) {
1682
2302
  docsPatterns = config.docs.include;
1683
- log(chalk4.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
2303
+ log(chalk6.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1684
2304
  }
1685
2305
  }
1686
2306
  if (docsPatterns && docsPatterns.length > 0) {
@@ -1703,46 +2323,46 @@ function loadSpec(filePath, readFileSync2) {
1703
2323
  }
1704
2324
  function printSummary(diff, baseName, headName, fromCache, log) {
1705
2325
  log("");
1706
- const cacheIndicator = fromCache ? chalk4.cyan(" (cached)") : "";
1707
- log(chalk4.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
2326
+ const cacheIndicator = fromCache ? chalk6.cyan(" (cached)") : "";
2327
+ log(chalk6.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1708
2328
  log("─".repeat(40));
1709
2329
  log("");
1710
- const coverageColor = diff.coverageDelta > 0 ? chalk4.green : diff.coverageDelta < 0 ? chalk4.red : chalk4.gray;
2330
+ const coverageColor = diff.coverageDelta > 0 ? chalk6.green : diff.coverageDelta < 0 ? chalk6.red : chalk6.gray;
1711
2331
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1712
2332
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1713
2333
  const breakingCount = diff.breaking.length;
1714
2334
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1715
2335
  if (breakingCount > 0) {
1716
- const severityNote = highSeverity > 0 ? chalk4.red(` (${highSeverity} high severity)`) : "";
1717
- log(` Breaking: ${chalk4.red(breakingCount)} changes${severityNote}`);
2336
+ const severityNote = highSeverity > 0 ? chalk6.red(` (${highSeverity} high severity)`) : "";
2337
+ log(` Breaking: ${chalk6.red(breakingCount)} changes${severityNote}`);
1718
2338
  } else {
1719
- log(` Breaking: ${chalk4.green("0")} changes`);
2339
+ log(` Breaking: ${chalk6.green("0")} changes`);
1720
2340
  }
1721
2341
  const newCount = diff.nonBreaking.length;
1722
2342
  const undocCount = diff.newUndocumented.length;
1723
2343
  if (newCount > 0) {
1724
- const undocNote = undocCount > 0 ? chalk4.yellow(` (${undocCount} undocumented)`) : "";
1725
- log(` New: ${chalk4.green(newCount)} exports${undocNote}`);
2344
+ const undocNote = undocCount > 0 ? chalk6.yellow(` (${undocCount} undocumented)`) : "";
2345
+ log(` New: ${chalk6.green(newCount)} exports${undocNote}`);
1726
2346
  }
1727
2347
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1728
2348
  const parts = [];
1729
2349
  if (diff.driftIntroduced > 0)
1730
- parts.push(chalk4.red(`+${diff.driftIntroduced}`));
2350
+ parts.push(chalk6.red(`+${diff.driftIntroduced}`));
1731
2351
  if (diff.driftResolved > 0)
1732
- parts.push(chalk4.green(`-${diff.driftResolved}`));
2352
+ parts.push(chalk6.green(`-${diff.driftResolved}`));
1733
2353
  log(` Drift: ${parts.join(", ")}`);
1734
2354
  }
1735
2355
  log("");
1736
2356
  }
1737
2357
  async function printAISummary(diff, log) {
1738
2358
  if (!isAIDocsAnalysisAvailable()) {
1739
- log(chalk4.yellow(`
2359
+ log(chalk6.yellow(`
1740
2360
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1741
2361
  return;
1742
2362
  }
1743
2363
  if (!diff.docsImpact)
1744
2364
  return;
1745
- log(chalk4.gray(`
2365
+ log(chalk6.gray(`
1746
2366
  Generating AI summary...`));
1747
2367
  const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1748
2368
  file: f.file,
@@ -1753,8 +2373,8 @@ Generating AI summary...`));
1753
2373
  const summary = await generateImpactSummary(impacts);
1754
2374
  if (summary) {
1755
2375
  log("");
1756
- log(chalk4.bold("AI Summary"));
1757
- log(chalk4.cyan(` ${summary}`));
2376
+ log(chalk6.bold("AI Summary"));
2377
+ log(chalk6.cyan(` ${summary}`));
1758
2378
  }
1759
2379
  }
1760
2380
  function validateDiff(diff, headSpec, options) {
@@ -1835,7 +2455,7 @@ function printGitHubAnnotations(diff, log) {
1835
2455
 
1836
2456
  // src/commands/info.ts
1837
2457
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1838
- import chalk5 from "chalk";
2458
+ import chalk7 from "chalk";
1839
2459
  function registerInfoCommand(program) {
1840
2460
  program.command("info [entry]").description("Show brief documentation coverage summary").option("--cwd <dir>", "Working directory", process.cwd()).option("--package <name>", "Target package name (for monorepos)").option("--skip-resolve", "Skip external type resolution from node_modules").action(async (entry, options) => {
1841
2461
  try {
@@ -1857,14 +2477,14 @@ function registerInfoCommand(program) {
1857
2477
  const spec = enrichSpec2(specResult.spec);
1858
2478
  const stats = computeStats(spec);
1859
2479
  console.log("");
1860
- console.log(chalk5.bold(`${stats.packageName}@${stats.version}`));
2480
+ console.log(chalk7.bold(`${stats.packageName}@${stats.version}`));
1861
2481
  console.log("");
1862
- console.log(` Exports: ${chalk5.bold(stats.totalExports.toString())}`);
1863
- console.log(` Coverage: ${chalk5.bold(`${stats.coverageScore}%`)}`);
1864
- console.log(` Drift: ${chalk5.bold(`${stats.driftScore}%`)}`);
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}%`)}`);
1865
2485
  console.log("");
1866
2486
  } catch (err) {
1867
- console.error(chalk5.red("Error:"), err instanceof Error ? err.message : err);
2487
+ console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
1868
2488
  process.exit(1);
1869
2489
  }
1870
2490
  });
@@ -1873,7 +2493,7 @@ function registerInfoCommand(program) {
1873
2493
  // src/commands/init.ts
1874
2494
  import * as fs4 from "node:fs";
1875
2495
  import * as path6 from "node:path";
1876
- import chalk6 from "chalk";
2496
+ import chalk8 from "chalk";
1877
2497
  var defaultDependencies3 = {
1878
2498
  fileExists: fs4.existsSync,
1879
2499
  writeFileSync: fs4.writeFileSync,
@@ -1890,31 +2510,31 @@ function registerInitCommand(program, dependencies = {}) {
1890
2510
  const cwd = path6.resolve(options.cwd);
1891
2511
  const formatOption = String(options.format ?? "auto").toLowerCase();
1892
2512
  if (!isValidFormat(formatOption)) {
1893
- error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
2513
+ error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
1894
2514
  process.exitCode = 1;
1895
2515
  return;
1896
2516
  }
1897
2517
  const existing = findExistingConfig(cwd, fileExists2);
1898
2518
  if (existing) {
1899
- error(chalk6.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
2519
+ error(chalk8.red(`A DocCov config already exists at ${path6.relative(cwd, existing) || "./doccov.config.*"}.`));
1900
2520
  process.exitCode = 1;
1901
2521
  return;
1902
2522
  }
1903
2523
  const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
1904
2524
  const targetFormat = resolveFormat(formatOption, packageType);
1905
2525
  if (targetFormat === "js" && packageType !== "module") {
1906
- log(chalk6.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
2526
+ log(chalk8.yellow('Package is not marked as "type": "module"; creating doccov.config.js may require enabling ESM.'));
1907
2527
  }
1908
2528
  const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
1909
2529
  const outputPath = path6.join(cwd, fileName);
1910
2530
  if (fileExists2(outputPath)) {
1911
- error(chalk6.red(`Cannot create ${fileName}; file already exists.`));
2531
+ error(chalk8.red(`Cannot create ${fileName}; file already exists.`));
1912
2532
  process.exitCode = 1;
1913
2533
  return;
1914
2534
  }
1915
2535
  const template = buildTemplate(targetFormat);
1916
2536
  writeFileSync3(outputPath, template, { encoding: "utf8" });
1917
- log(chalk6.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
2537
+ log(chalk8.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
1918
2538
  });
1919
2539
  }
1920
2540
  var isValidFormat = (value) => {
@@ -2048,42 +2668,11 @@ quality:
2048
2668
  // src/commands/spec.ts
2049
2669
  import * as fs5 from "node:fs";
2050
2670
  import * as path7 from "node:path";
2051
- import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2671
+ import { DocCov as DocCov3, NodeFileSystem as NodeFileSystem3, renderApiSurface, resolveTarget as resolveTarget3 } from "@doccov/sdk";
2052
2672
  import { normalize, validateSpec } from "@openpkg-ts/spec";
2053
- import chalk8 from "chalk";
2673
+ import chalk9 from "chalk";
2054
2674
  // package.json
2055
- var version = "0.16.0";
2056
-
2057
- // src/utils/filter-options.ts
2058
- import { mergeFilters, parseListFlag } from "@doccov/sdk";
2059
- import chalk7 from "chalk";
2060
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
2061
- var mergeFilterOptions = (config, cliOptions) => {
2062
- const messages = [];
2063
- if (config?.include) {
2064
- messages.push(formatList("include filters from config", config.include));
2065
- }
2066
- if (config?.exclude) {
2067
- messages.push(formatList("exclude filters from config", config.exclude));
2068
- }
2069
- if (cliOptions.include) {
2070
- messages.push(formatList("apply include filters from CLI", cliOptions.include));
2071
- }
2072
- if (cliOptions.exclude) {
2073
- messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
2074
- }
2075
- const resolved = mergeFilters(config, cliOptions);
2076
- if (!resolved.include && !resolved.exclude) {
2077
- return { messages };
2078
- }
2079
- const source = resolved.source === "override" ? "cli" : resolved.source;
2080
- return {
2081
- include: resolved.include,
2082
- exclude: resolved.exclude,
2083
- source,
2084
- messages
2085
- };
2086
- };
2675
+ var version = "0.17.0";
2087
2676
 
2088
2677
  // src/commands/spec.ts
2089
2678
  var defaultDependencies4 = {
@@ -2098,7 +2687,7 @@ function getArrayLength(value) {
2098
2687
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2099
2688
  const location = diagnostic.location;
2100
2689
  const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
2101
- const locationText = location && relativePath ? chalk8.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2690
+ const locationText = location && relativePath ? chalk9.gray(`${relativePath}:${location.line ?? 1}:${location.column ?? 1}`) : null;
2102
2691
  const locationPrefix = locationText ? `${locationText} ` : "";
2103
2692
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
2104
2693
  }
@@ -2107,7 +2696,7 @@ function registerSpecCommand(program, dependencies = {}) {
2107
2696
  ...defaultDependencies4,
2108
2697
  ...dependencies
2109
2698
  };
2110
- program.command("spec [entry]").description("Generate OpenPkg specification (JSON)").option("--cwd <dir>", "Working directory", process.cwd()).option("-p, --package <name>", "Target package name (for monorepos)").option("-o, --output <file>", "Output file path", "openpkg.json").option("--include <patterns>", "Include exports matching pattern (comma-separated)").option("--exclude <patterns>", "Exclude exports matching pattern (comma-separated)").option("--skip-resolve", "Skip external type resolution from node_modules").option("--max-type-depth <n>", "Maximum depth for type conversion", "20").option("--no-cache", "Bypass spec cache and force regeneration").option("--show-diagnostics", "Show TypeScript compiler diagnostics").option("--verbose", "Show detailed generation metadata").action(async (entry, options) => {
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) => {
2111
2700
  try {
2112
2701
  const steps = new StepProgress([
2113
2702
  { label: "Resolved target", activeLabel: "Resolving target" },
@@ -2129,13 +2718,14 @@ function registerSpecCommand(program, dependencies = {}) {
2129
2718
  try {
2130
2719
  config = await loadDocCovConfig(targetDir);
2131
2720
  } catch (configError) {
2132
- error(chalk8.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2721
+ error(chalk9.red("Failed to load DocCov config:"), configError instanceof Error ? configError.message : configError);
2133
2722
  process.exit(1);
2134
2723
  }
2135
2724
  steps.next();
2136
2725
  const cliFilters = {
2137
2726
  include: parseListFlag(options.include),
2138
- exclude: parseListFlag(options.exclude)
2727
+ exclude: parseListFlag(options.exclude),
2728
+ visibility: parseVisibilityFlag(options.visibility)
2139
2729
  };
2140
2730
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
2141
2731
  const resolveExternalTypes = !options.skipResolve;
@@ -2155,10 +2745,11 @@ function registerSpecCommand(program, dependencies = {}) {
2155
2745
  isMonorepo: resolved.isMonorepo,
2156
2746
  targetPackage: packageInfo?.name
2157
2747
  };
2158
- const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
2748
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
2159
2749
  filters: {
2160
2750
  include: resolvedFilters.include,
2161
- exclude: resolvedFilters.exclude
2751
+ exclude: resolvedFilters.exclude,
2752
+ visibility: resolvedFilters.visibility
2162
2753
  },
2163
2754
  generationInput
2164
2755
  } : { generationInput };
@@ -2170,74 +2761,266 @@ function registerSpecCommand(program, dependencies = {}) {
2170
2761
  const normalized = normalize(result.spec);
2171
2762
  const validation = validateSpec(normalized);
2172
2763
  if (!validation.ok) {
2173
- error(chalk8.red("Spec failed schema validation"));
2764
+ error(chalk9.red("Spec failed schema validation"));
2174
2765
  for (const err of validation.errors) {
2175
- error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2766
+ error(chalk9.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2176
2767
  }
2177
2768
  process.exit(1);
2178
2769
  }
2179
2770
  steps.next();
2180
- const outputPath = path7.resolve(process.cwd(), options.output);
2181
- writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2182
- steps.complete(`Generated ${options.output}`);
2183
- log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2184
- log(chalk8.gray(` ${getArrayLength(normalized.types)} types`));
2771
+ const format = options.format ?? "json";
2772
+ const outputPath = path7.resolve(options.cwd, options.output);
2773
+ if (format === "api-surface") {
2774
+ const apiSurface = renderApiSurface(normalized);
2775
+ writeFileSync4(outputPath, apiSurface);
2776
+ steps.complete(`Generated ${options.output} (API surface)`);
2777
+ } else {
2778
+ writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2779
+ steps.complete(`Generated ${options.output}`);
2780
+ }
2781
+ log(chalk9.gray(` ${getArrayLength(normalized.exports)} exports`));
2782
+ log(chalk9.gray(` ${getArrayLength(normalized.types)} types`));
2185
2783
  if (options.verbose && normalized.generation) {
2186
2784
  const gen = normalized.generation;
2187
2785
  log("");
2188
- log(chalk8.bold("Generation Info"));
2189
- log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
2190
- log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2191
- log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
2192
- log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2193
- log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2194
- log(chalk8.gray(` External types: ${gen.analysis.resolvedExternalTypes ? "resolved" : "skipped"}`));
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"}`));
2195
2793
  if (gen.analysis.maxTypeDepth) {
2196
- log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2794
+ log(chalk9.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2197
2795
  }
2198
2796
  log("");
2199
- log(chalk8.bold("Environment"));
2200
- log(chalk8.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2797
+ log(chalk9.bold("Environment"));
2798
+ log(chalk9.gray(` node_modules: ${gen.environment.hasNodeModules ? "found" : "not found"}`));
2201
2799
  if (gen.environment.packageManager) {
2202
- log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
2800
+ log(chalk9.gray(` Package manager: ${gen.environment.packageManager}`));
2203
2801
  }
2204
2802
  if (gen.environment.isMonorepo) {
2205
- log(chalk8.gray(` Monorepo: yes`));
2803
+ log(chalk9.gray(` Monorepo: yes`));
2206
2804
  }
2207
2805
  if (gen.environment.targetPackage) {
2208
- log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
2806
+ log(chalk9.gray(` Target package: ${gen.environment.targetPackage}`));
2209
2807
  }
2210
2808
  if (gen.issues.length > 0) {
2211
2809
  log("");
2212
- log(chalk8.bold("Issues"));
2810
+ log(chalk9.bold("Issues"));
2213
2811
  for (const issue of gen.issues) {
2214
- const prefix = issue.severity === "error" ? chalk8.red(">") : issue.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2812
+ const prefix = issue.severity === "error" ? chalk9.red(">") : issue.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
2215
2813
  log(`${prefix} [${issue.code}] ${issue.message}`);
2216
2814
  if (issue.suggestion) {
2217
- log(chalk8.gray(` ${issue.suggestion}`));
2815
+ log(chalk9.gray(` ${issue.suggestion}`));
2218
2816
  }
2219
2817
  }
2220
2818
  }
2221
2819
  }
2222
2820
  if (options.showDiagnostics && result.diagnostics.length > 0) {
2223
2821
  log("");
2224
- log(chalk8.bold("Diagnostics"));
2822
+ log(chalk9.bold("Diagnostics"));
2225
2823
  for (const diagnostic of result.diagnostics) {
2226
- const prefix = diagnostic.severity === "error" ? chalk8.red(">") : diagnostic.severity === "warning" ? chalk8.yellow(">") : chalk8.cyan(">");
2824
+ const prefix = diagnostic.severity === "error" ? chalk9.red(">") : diagnostic.severity === "warning" ? chalk9.yellow(">") : chalk9.cyan(">");
2227
2825
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2228
2826
  }
2229
2827
  }
2230
2828
  } catch (commandError) {
2231
- error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2829
+ error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2232
2830
  process.exit(1);
2233
2831
  }
2234
2832
  });
2235
2833
  }
2236
2834
 
2835
+ // src/commands/trends.ts
2836
+ import * as fs6 from "node:fs";
2837
+ import * as path8 from "node:path";
2838
+ import {
2839
+ formatDelta as formatDelta2,
2840
+ getExtendedTrend,
2841
+ getTrend,
2842
+ loadSnapshots,
2843
+ pruneByTier,
2844
+ pruneHistory,
2845
+ renderSparkline,
2846
+ RETENTION_DAYS,
2847
+ saveSnapshot
2848
+ } from "@doccov/sdk";
2849
+ import chalk10 from "chalk";
2850
+ function formatDate(timestamp) {
2851
+ const date = new Date(timestamp);
2852
+ return date.toLocaleDateString("en-US", {
2853
+ month: "short",
2854
+ day: "numeric",
2855
+ year: "numeric",
2856
+ hour: "2-digit",
2857
+ minute: "2-digit"
2858
+ });
2859
+ }
2860
+ function getColorForScore(score) {
2861
+ if (score >= 90)
2862
+ return chalk10.green;
2863
+ if (score >= 70)
2864
+ return chalk10.yellow;
2865
+ if (score >= 50)
2866
+ return chalk10.hex("#FFA500");
2867
+ return chalk10.red;
2868
+ }
2869
+ function formatSnapshot(snapshot) {
2870
+ const color = getColorForScore(snapshot.coverageScore);
2871
+ const date = formatDate(snapshot.timestamp);
2872
+ 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}`;
2875
+ }
2876
+ function formatWeekDate(timestamp) {
2877
+ const date = new Date(timestamp);
2878
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
2879
+ }
2880
+ function formatVelocity(velocity) {
2881
+ if (velocity > 0)
2882
+ return chalk10.green(`+${velocity}%/day`);
2883
+ if (velocity < 0)
2884
+ return chalk10.red(`${velocity}%/day`);
2885
+ return chalk10.gray("0%/day");
2886
+ }
2887
+ function registerTrendsCommand(program) {
2888
+ 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) => {
2889
+ const cwd = path8.resolve(options.cwd);
2890
+ const tier = options.tier ?? "pro";
2891
+ if (options.prune) {
2892
+ const keepCount = parseInt(options.prune, 10);
2893
+ if (!isNaN(keepCount)) {
2894
+ const deleted = pruneHistory(cwd, keepCount);
2895
+ console.log(chalk10.green(`Pruned ${deleted} old snapshots, kept ${keepCount} most recent`));
2896
+ } else {
2897
+ const deleted = pruneByTier(cwd, tier);
2898
+ console.log(chalk10.green(`Pruned ${deleted} snapshots older than ${RETENTION_DAYS[tier]} days`));
2899
+ }
2900
+ return;
2901
+ }
2902
+ if (options.record) {
2903
+ const specPath = path8.resolve(cwd, "openpkg.json");
2904
+ if (!fs6.existsSync(specPath)) {
2905
+ console.error(chalk10.red("No openpkg.json found. Run `doccov spec` first to generate a spec."));
2906
+ process.exit(1);
2907
+ }
2908
+ try {
2909
+ const specContent = fs6.readFileSync(specPath, "utf-8");
2910
+ const spec = JSON.parse(specContent);
2911
+ const trend = getTrend(spec, cwd);
2912
+ saveSnapshot(trend.current, cwd);
2913
+ console.log(chalk10.green("Recorded coverage snapshot:"));
2914
+ console.log(formatSnapshot(trend.current));
2915
+ if (trend.delta !== undefined) {
2916
+ 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));
2919
+ }
2920
+ return;
2921
+ } catch (error) {
2922
+ console.error(chalk10.red("Failed to read openpkg.json:"), error instanceof Error ? error.message : error);
2923
+ process.exit(1);
2924
+ }
2925
+ }
2926
+ const snapshots = loadSnapshots(cwd);
2927
+ const limit = parseInt(options.limit ?? "10", 10);
2928
+ 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."));
2931
+ return;
2932
+ }
2933
+ if (options.json) {
2934
+ const output = {
2935
+ current: snapshots[0],
2936
+ history: snapshots.slice(1),
2937
+ delta: snapshots.length > 1 ? snapshots[0].coverageScore - snapshots[1].coverageScore : undefined,
2938
+ sparkline: snapshots.slice(0, 10).map((s) => s.coverageScore).reverse()
2939
+ };
2940
+ console.log(JSON.stringify(output, null, 2));
2941
+ return;
2942
+ }
2943
+ const sparklineData = snapshots.slice(0, 10).map((s) => s.coverageScore).reverse();
2944
+ 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}`));
2948
+ console.log("");
2949
+ if (snapshots.length >= 2) {
2950
+ const oldest = snapshots[snapshots.length - 1];
2951
+ const newest = snapshots[0];
2952
+ const overallDelta = newest.coverageScore - oldest.coverageScore;
2953
+ 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)`));
2956
+ console.log("");
2957
+ }
2958
+ if (options.extended) {
2959
+ const specPath = path8.resolve(cwd, "openpkg.json");
2960
+ if (fs6.existsSync(specPath)) {
2961
+ try {
2962
+ const specContent = fs6.readFileSync(specPath, "utf-8");
2963
+ const spec = JSON.parse(specContent);
2964
+ 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)`));
2967
+ console.log("");
2968
+ console.log(" Velocity:");
2969
+ console.log(` 7-day: ${formatVelocity(extended.velocity7d)}`);
2970
+ console.log(` 30-day: ${formatVelocity(extended.velocity30d)}`);
2971
+ if (extended.velocity90d !== undefined) {
2972
+ console.log(` 90-day: ${formatVelocity(extended.velocity90d)}`);
2973
+ }
2974
+ console.log("");
2975
+ const projColor = extended.projected30d >= extended.trend.current.coverageScore ? chalk10.green : chalk10.red;
2976
+ 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}%`)}`);
2979
+ if (extended.dataRange) {
2980
+ const startDate = formatWeekDate(extended.dataRange.start);
2981
+ const endDate = formatWeekDate(extended.dataRange.end);
2982
+ console.log(chalk10.gray(` Data range: ${startDate} - ${endDate}`));
2983
+ }
2984
+ console.log("");
2985
+ if (options.weekly && extended.weeklySummaries.length > 0) {
2986
+ console.log(chalk10.bold("Weekly Summary"));
2987
+ const weekLimit = Math.min(extended.weeklySummaries.length, 8);
2988
+ for (let i = 0;i < weekLimit; i++) {
2989
+ const week = extended.weeklySummaries[i];
2990
+ const weekStart = formatWeekDate(week.weekStart);
2991
+ const weekEnd = formatWeekDate(week.weekEnd);
2992
+ const deltaColor = week.delta > 0 ? chalk10.green : week.delta < 0 ? chalk10.red : chalk10.gray;
2993
+ const deltaStr = week.delta > 0 ? `+${week.delta}%` : `${week.delta}%`;
2994
+ console.log(` ${weekStart} - ${weekEnd}: ${week.avgCoverage}% avg ${deltaColor(deltaStr)} (${week.snapshotCount} snapshots)`);
2995
+ }
2996
+ if (extended.weeklySummaries.length > weekLimit) {
2997
+ console.log(chalk10.gray(` ... and ${extended.weeklySummaries.length - weekLimit} more weeks`));
2998
+ }
2999
+ console.log("");
3000
+ }
3001
+ } catch {
3002
+ console.log(chalk10.yellow("Could not load openpkg.json for extended analysis"));
3003
+ console.log("");
3004
+ }
3005
+ }
3006
+ }
3007
+ console.log(chalk10.bold("History"));
3008
+ const displaySnapshots = snapshots.slice(0, limit);
3009
+ for (let i = 0;i < displaySnapshots.length; i++) {
3010
+ const snapshot = displaySnapshots[i];
3011
+ const prefix = i === 0 ? chalk10.cyan("→") : " ";
3012
+ console.log(`${prefix} ${formatSnapshot(snapshot)}`);
3013
+ }
3014
+ if (snapshots.length > limit) {
3015
+ console.log(chalk10.gray(` ... and ${snapshots.length - limit} more`));
3016
+ }
3017
+ });
3018
+ }
3019
+
2237
3020
  // src/cli.ts
2238
3021
  var __filename2 = fileURLToPath(import.meta.url);
2239
- var __dirname2 = path8.dirname(__filename2);
2240
- var packageJson = JSON.parse(readFileSync3(path8.join(__dirname2, "../package.json"), "utf-8"));
3022
+ var __dirname2 = path9.dirname(__filename2);
3023
+ var packageJson = JSON.parse(readFileSync4(path9.join(__dirname2, "../package.json"), "utf-8"));
2241
3024
  var program = new Command;
2242
3025
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2243
3026
  registerCheckCommand(program);
@@ -2245,6 +3028,7 @@ registerInfoCommand(program);
2245
3028
  registerSpecCommand(program);
2246
3029
  registerDiffCommand(program);
2247
3030
  registerInitCommand(program);
3031
+ registerTrendsCommand(program);
2248
3032
  program.command("*", { hidden: true }).action(() => {
2249
3033
  program.outputHelp();
2250
3034
  });