@doccov/cli 0.16.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
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config/doccov-config.ts
4
- import { access } from "node:fs/promises";
4
+ import { access, readFile } 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";
7
8
 
8
9
  // src/config/schema.ts
9
10
  import { z } from "zod";
@@ -34,13 +35,20 @@ var checkConfigSchema = z.object({
34
35
  var qualityConfigSchema = z.object({
35
36
  rules: z.record(severitySchema).optional()
36
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
+ });
37
44
  var docCovConfigSchema = z.object({
38
45
  include: stringList.optional(),
39
46
  exclude: stringList.optional(),
40
47
  plugins: z.array(z.unknown()).optional(),
41
48
  docs: docsConfigSchema.optional(),
42
49
  check: checkConfigSchema.optional(),
43
- quality: qualityConfigSchema.optional()
50
+ quality: qualityConfigSchema.optional(),
51
+ policies: z.array(policyConfigSchema).optional()
44
52
  });
45
53
  var normalizeList = (value) => {
46
54
  if (!value) {
@@ -78,13 +86,15 @@ var normalizeConfig = (input) => {
78
86
  rules: input.quality.rules
79
87
  };
80
88
  }
89
+ const policies = input.policies;
81
90
  return {
82
91
  include,
83
92
  exclude,
84
93
  plugins: input.plugins,
85
94
  docs,
86
95
  check,
87
- quality
96
+ quality,
97
+ policies
88
98
  };
89
99
  };
90
100
 
@@ -95,7 +105,9 @@ var DOCCOV_CONFIG_FILENAMES = [
95
105
  "doccov.config.cts",
96
106
  "doccov.config.js",
97
107
  "doccov.config.mjs",
98
- "doccov.config.cjs"
108
+ "doccov.config.cjs",
109
+ "doccov.yml",
110
+ "doccov.yaml"
99
111
  ];
100
112
  var fileExists = async (filePath) => {
101
113
  try {
@@ -122,6 +134,11 @@ var findConfigFile = async (cwd) => {
122
134
  }
123
135
  };
124
136
  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
+ }
125
142
  const fileUrl = pathToFileURL(absolutePath);
126
143
  fileUrl.searchParams.set("t", Date.now().toString());
127
144
  const module = await import(fileUrl.href);
@@ -160,8 +177,8 @@ ${formatIssues(issues)}`);
160
177
  // src/config/index.ts
161
178
  var defineConfig = (config) => config;
162
179
  // src/cli.ts
163
- import { readFileSync as readFileSync3 } from "node:fs";
164
- import * as path8 from "node:path";
180
+ import { readFileSync as readFileSync4 } from "node:fs";
181
+ import * as path9 from "node:path";
165
182
  import { fileURLToPath } from "node:url";
166
183
  import { Command } from "commander";
167
184
 
@@ -169,11 +186,14 @@ import { Command } from "commander";
169
186
  import * as fs2 from "node:fs";
170
187
  import * as path4 from "node:path";
171
188
  import {
189
+ analyzeSpecContributors,
190
+ analyzeSpecOwnership,
172
191
  applyEdits,
173
192
  categorizeDrifts,
174
193
  createSourceFile,
175
194
  DocCov,
176
195
  enrichSpec,
196
+ evaluatePolicies,
177
197
  findJSDocLocation,
178
198
  generateFixesForExport,
179
199
  generateReport,
@@ -188,8 +208,75 @@ import {
188
208
  import {
189
209
  DRIFT_CATEGORIES as DRIFT_CATEGORIES2
190
210
  } from "@openpkg-ts/spec";
191
- import chalk3 from "chalk";
211
+ import chalk5 from "chalk";
192
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
+ }
193
280
  // src/reports/diff-markdown.ts
194
281
  import * as path2 from "node:path";
195
282
  function bar(pct, width = 10) {
@@ -891,19 +978,336 @@ function writeReports(options) {
891
978
  }));
892
979
  return results;
893
980
  }
894
- // src/utils/llm-assertion-parser.ts
981
+ // src/utils/ai-client.ts
982
+ import chalk2 from "chalk";
983
+
984
+ // src/utils/ai-generate.ts
895
985
  import { createAnthropic } from "@ai-sdk/anthropic";
896
986
  import { createOpenAI } from "@ai-sdk/openai";
897
987
  import { generateObject } from "ai";
898
988
  import { z as z2 } from "zod";
899
- var AssertionParseSchema = z2.object({
900
- assertions: z2.array(z2.object({
901
- lineNumber: z2.number().describe("1-indexed line number where the assertion appears"),
902
- expected: z2.string().describe("The expected output value"),
903
- originalComment: z2.string().describe("The original comment text"),
904
- 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")
905
1309
  })).describe("List of assertion-like comments found in the code"),
906
- hasAssertions: z2.boolean().describe("Whether any assertion-like comments were found")
1310
+ hasAssertions: z3.boolean().describe("Whether any assertion-like comments were found")
907
1311
  });
908
1312
  var ASSERTION_PARSE_PROMPT = (code) => `Analyze this TypeScript/JavaScript example code for assertion-like comments.
909
1313
 
@@ -930,13 +1334,13 @@ Code:
930
1334
  \`\`\`
931
1335
  ${code}
932
1336
  \`\`\``;
933
- function getModel() {
1337
+ function getModel2() {
934
1338
  const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
935
1339
  if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
936
- const anthropic = createAnthropic();
1340
+ const anthropic = createAnthropic2();
937
1341
  return anthropic("claude-sonnet-4-20250514");
938
1342
  }
939
- const openai = createOpenAI();
1343
+ const openai = createOpenAI2();
940
1344
  return openai("gpt-4o-mini");
941
1345
  }
942
1346
  function isLLMAssertionParsingAvailable() {
@@ -947,8 +1351,8 @@ async function parseAssertionsWithLLM(code) {
947
1351
  return null;
948
1352
  }
949
1353
  try {
950
- const model = getModel();
951
- const { object } = await generateObject({
1354
+ const model = getModel2();
1355
+ const { object } = await generateObject2({
952
1356
  model,
953
1357
  schema: AssertionParseSchema,
954
1358
  prompt: ASSERTION_PARSE_PROMPT(code)
@@ -960,7 +1364,7 @@ async function parseAssertionsWithLLM(code) {
960
1364
  }
961
1365
 
962
1366
  // src/utils/progress.ts
963
- import chalk2 from "chalk";
1367
+ import chalk4 from "chalk";
964
1368
  class StepProgress {
965
1369
  steps;
966
1370
  currentStep = 0;
@@ -988,7 +1392,7 @@ class StepProgress {
988
1392
  this.completeCurrentStep();
989
1393
  if (message) {
990
1394
  const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
991
- console.log(`${chalk2.green("✓")} ${message} ${chalk2.dim(`(${elapsed}s)`)}`);
1395
+ console.log(`${chalk4.green("✓")} ${message} ${chalk4.dim(`(${elapsed}s)`)}`);
992
1396
  }
993
1397
  }
994
1398
  render() {
@@ -996,17 +1400,17 @@ class StepProgress {
996
1400
  if (!step)
997
1401
  return;
998
1402
  const label = step.activeLabel ?? step.label;
999
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1000
- 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)}...`);
1001
1405
  }
1002
1406
  completeCurrentStep() {
1003
1407
  const step = this.steps[this.currentStep];
1004
1408
  if (!step)
1005
1409
  return;
1006
1410
  const elapsed = ((Date.now() - this.stepStartTime) / 1000).toFixed(1);
1007
- const prefix = chalk2.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1411
+ const prefix = chalk4.dim(`[${this.currentStep + 1}/${this.steps.length}]`);
1008
1412
  process.stdout.write(`\r${" ".repeat(80)}\r`);
1009
- console.log(`${prefix} ${step.label} ${chalk2.green("✓")} ${chalk2.dim(`(${elapsed}s)`)}`);
1413
+ console.log(`${prefix} ${step.label} ${chalk4.green("✓")} ${chalk4.dim(`(${elapsed}s)`)}`);
1010
1414
  }
1011
1415
  }
1012
1416
 
@@ -1050,7 +1454,7 @@ function registerCheckCommand(program, dependencies = {}) {
1050
1454
  ...defaultDependencies,
1051
1455
  ...dependencies
1052
1456
  };
1053
- 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) => {
1054
1458
  try {
1055
1459
  const validations = parseExamplesFlag(options.examples);
1056
1460
  const hasExamples = validations.length > 0;
@@ -1077,6 +1481,15 @@ function registerCheckCommand(program, dependencies = {}) {
1077
1481
  const minCoverage = minCoverageRaw !== undefined ? clampPercentage(minCoverageRaw) : undefined;
1078
1482
  const maxDriftRaw = options.maxDrift ?? config?.check?.maxDrift;
1079
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
+ }
1080
1493
  steps.next();
1081
1494
  const resolveExternalTypes = !options.skipResolve;
1082
1495
  let specResult;
@@ -1086,7 +1499,8 @@ function registerCheckCommand(program, dependencies = {}) {
1086
1499
  useCache: options.cache !== false,
1087
1500
  cwd: options.cwd
1088
1501
  });
1089
- specResult = await doccov.analyzeFileWithDiagnostics(entryFile);
1502
+ const analyzeOptions = resolvedFilters.visibility ? { filters: { visibility: resolvedFilters.visibility } } : {};
1503
+ specResult = await doccov.analyzeFileWithDiagnostics(entryFile, analyzeOptions);
1090
1504
  if (!specResult) {
1091
1505
  throw new Error("Failed to analyze documentation coverage.");
1092
1506
  }
@@ -1103,6 +1517,24 @@ function registerCheckCommand(program, dependencies = {}) {
1103
1517
  violations.push({ exportName: exp.name, violation: v });
1104
1518
  }
1105
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
+ }
1106
1538
  let exampleResult;
1107
1539
  const typecheckErrors = [];
1108
1540
  const runtimeDrifts = [];
@@ -1148,12 +1580,12 @@ function registerCheckCommand(program, dependencies = {}) {
1148
1580
  if (allDrifts.length > 0) {
1149
1581
  const { fixable, nonFixable } = categorizeDrifts(allDrifts.map((d) => d.drift));
1150
1582
  if (fixable.length === 0) {
1151
- 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.`));
1152
1584
  } else {
1153
1585
  log("");
1154
- log(chalk3.bold(`Found ${fixable.length} fixable issue(s)`));
1586
+ log(chalk5.bold(`Found ${fixable.length} fixable issue(s)`));
1155
1587
  if (nonFixable.length > 0) {
1156
- log(chalk3.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1588
+ log(chalk5.gray(`(${nonFixable.length} non-fixable issue(s) skipped)`));
1157
1589
  }
1158
1590
  log("");
1159
1591
  const groupedDrifts = groupByExport(allDrifts.filter((d) => fixable.includes(d.drift)));
@@ -1161,22 +1593,22 @@ function registerCheckCommand(program, dependencies = {}) {
1161
1593
  const editsByFile = new Map;
1162
1594
  for (const [exp, drifts] of groupedDrifts) {
1163
1595
  if (!exp.source?.file) {
1164
- log(chalk3.gray(` Skipping ${exp.name}: no source location`));
1596
+ log(chalk5.gray(` Skipping ${exp.name}: no source location`));
1165
1597
  continue;
1166
1598
  }
1167
1599
  if (exp.source.file.endsWith(".d.ts")) {
1168
- log(chalk3.gray(` Skipping ${exp.name}: declaration file`));
1600
+ log(chalk5.gray(` Skipping ${exp.name}: declaration file`));
1169
1601
  continue;
1170
1602
  }
1171
1603
  const filePath = path4.resolve(targetDir, exp.source.file);
1172
1604
  if (!fs2.existsSync(filePath)) {
1173
- log(chalk3.gray(` Skipping ${exp.name}: file not found`));
1605
+ log(chalk5.gray(` Skipping ${exp.name}: file not found`));
1174
1606
  continue;
1175
1607
  }
1176
1608
  const sourceFile = createSourceFile(filePath);
1177
1609
  const location = findJSDocLocation(sourceFile, exp.name, exp.source.line);
1178
1610
  if (!location) {
1179
- log(chalk3.gray(` Skipping ${exp.name}: could not find declaration`));
1611
+ log(chalk5.gray(` Skipping ${exp.name}: could not find declaration`));
1180
1612
  continue;
1181
1613
  }
1182
1614
  let existingPatch = {};
@@ -1209,26 +1641,26 @@ function registerCheckCommand(program, dependencies = {}) {
1209
1641
  }
1210
1642
  if (edits.length > 0) {
1211
1643
  if (options.dryRun) {
1212
- log(chalk3.bold("Dry run - changes that would be made:"));
1644
+ log(chalk5.bold("Dry run - changes that would be made:"));
1213
1645
  log("");
1214
1646
  for (const [filePath, fileEdits] of editsByFile) {
1215
1647
  const relativePath = path4.relative(targetDir, filePath);
1216
- log(chalk3.cyan(` ${relativePath}:`));
1648
+ log(chalk5.cyan(` ${relativePath}:`));
1217
1649
  for (const { export: exp, edit, fixes } of fileEdits) {
1218
1650
  const lineInfo = edit.hasExisting ? `lines ${edit.startLine + 1}-${edit.endLine + 1}` : `line ${edit.startLine + 1}`;
1219
- log(` ${chalk3.bold(exp.name)} [${lineInfo}]`);
1651
+ log(` ${chalk5.bold(exp.name)} [${lineInfo}]`);
1220
1652
  for (const fix of fixes) {
1221
- log(chalk3.green(` + ${fix.description}`));
1653
+ log(chalk5.green(` + ${fix.description}`));
1222
1654
  }
1223
1655
  }
1224
1656
  log("");
1225
1657
  }
1226
- log(chalk3.gray("Run without --dry-run to apply these changes."));
1658
+ log(chalk5.gray("Run without --dry-run to apply these changes."));
1227
1659
  } else {
1228
1660
  const applyResult = await applyEdits(edits);
1229
1661
  if (applyResult.errors.length > 0) {
1230
1662
  for (const err of applyResult.errors) {
1231
- error(chalk3.red(` ${err.file}: ${err.error}`));
1663
+ error(chalk5.red(` ${err.file}: ${err.error}`));
1232
1664
  }
1233
1665
  }
1234
1666
  }
@@ -1239,6 +1671,99 @@ function registerCheckCommand(program, dependencies = {}) {
1239
1671
  driftExports = driftExports.filter((d) => !fixedDriftKeys.has(`${d.name}:${d.issue}`));
1240
1672
  }
1241
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
+ }
1242
1767
  steps.complete("Check complete");
1243
1768
  if (format !== "text") {
1244
1769
  const limit = parseInt(options.limit, 10) || 20;
@@ -1284,7 +1809,8 @@ function registerCheckCommand(program, dependencies = {}) {
1284
1809
  const driftFailed2 = maxDrift !== undefined && driftScore2 > maxDrift;
1285
1810
  const hasQualityErrors2 = violations.filter((v) => v.violation.severity === "error").length > 0;
1286
1811
  const hasTypecheckErrors2 = typecheckErrors.length > 0;
1287
- if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2) {
1812
+ const policiesFailed2 = policyResult && !policyResult.allPassed;
1813
+ if (coverageFailed2 || driftFailed2 || hasQualityErrors2 || hasTypecheckErrors2 || policiesFailed2) {
1288
1814
  process.exit(1);
1289
1815
  }
1290
1816
  return;
@@ -1296,18 +1822,19 @@ function registerCheckCommand(program, dependencies = {}) {
1296
1822
  const driftFailed = maxDrift !== undefined && driftScore > maxDrift;
1297
1823
  const hasQualityErrors = violations.filter((v) => v.violation.severity === "error").length > 0;
1298
1824
  const hasTypecheckErrors = typecheckErrors.length > 0;
1825
+ const policiesFailed = policyResult && !policyResult.allPassed;
1299
1826
  if (specWarnings.length > 0 || specInfos.length > 0) {
1300
1827
  log("");
1301
1828
  for (const diag of specWarnings) {
1302
- log(chalk3.yellow(`⚠ ${diag.message}`));
1829
+ log(chalk5.yellow(`⚠ ${diag.message}`));
1303
1830
  if (diag.suggestion) {
1304
- log(chalk3.gray(` ${diag.suggestion}`));
1831
+ log(chalk5.gray(` ${diag.suggestion}`));
1305
1832
  }
1306
1833
  }
1307
1834
  for (const diag of specInfos) {
1308
- log(chalk3.cyan(`ℹ ${diag.message}`));
1835
+ log(chalk5.cyan(`ℹ ${diag.message}`));
1309
1836
  if (diag.suggestion) {
1310
- log(chalk3.gray(` ${diag.suggestion}`));
1837
+ log(chalk5.gray(` ${diag.suggestion}`));
1311
1838
  }
1312
1839
  }
1313
1840
  }
@@ -1317,23 +1844,23 @@ function registerCheckCommand(program, dependencies = {}) {
1317
1844
  const errorCount = violations.filter((v) => v.violation.severity === "error").length;
1318
1845
  const warnCount = violations.filter((v) => v.violation.severity === "warn").length;
1319
1846
  log("");
1320
- log(chalk3.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1847
+ log(chalk5.bold(`${pkgName}${pkgVersion ? `@${pkgVersion}` : ""}`));
1321
1848
  log("");
1322
1849
  log(` Exports: ${totalExports}`);
1323
1850
  if (minCoverage !== undefined) {
1324
1851
  if (coverageFailed) {
1325
- log(chalk3.red(` Coverage: ✗ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1852
+ log(chalk5.red(` Coverage: ✗ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1326
1853
  } else {
1327
- log(chalk3.green(` Coverage: ✓ ${coverageScore}%`) + chalk3.dim(` (min ${minCoverage}%)`));
1854
+ log(chalk5.green(` Coverage: ✓ ${coverageScore}%`) + chalk5.dim(` (min ${minCoverage}%)`));
1328
1855
  }
1329
1856
  } else {
1330
1857
  log(` Coverage: ${coverageScore}%`);
1331
1858
  }
1332
1859
  if (maxDrift !== undefined) {
1333
1860
  if (driftFailed) {
1334
- log(chalk3.red(` Drift: ✗ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1861
+ log(chalk5.red(` Drift: ✗ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1335
1862
  } else {
1336
- log(chalk3.green(` Drift: ✓ ${driftScore}%`) + chalk3.dim(` (max ${maxDrift}%)`));
1863
+ log(chalk5.green(` Drift: ✓ ${driftScore}%`) + chalk5.dim(` (max ${maxDrift}%)`));
1337
1864
  }
1338
1865
  } else {
1339
1866
  log(` Drift: ${driftScore}%`);
@@ -1343,7 +1870,7 @@ function registerCheckCommand(program, dependencies = {}) {
1343
1870
  if (typecheckCount > 0) {
1344
1871
  log(` Examples: ${typecheckCount} type errors`);
1345
1872
  } else {
1346
- log(chalk3.green(` Examples: ✓ validated`));
1873
+ log(chalk5.green(` Examples: ✓ validated`));
1347
1874
  }
1348
1875
  }
1349
1876
  if (errorCount > 0 || warnCount > 0) {
@@ -1354,8 +1881,57 @@ function registerCheckCommand(program, dependencies = {}) {
1354
1881
  parts.push(`${warnCount} warnings`);
1355
1882
  log(` Quality: ${parts.join(", ")}`);
1356
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
+ }
1357
1933
  log("");
1358
- const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors;
1934
+ const failed = coverageFailed || driftFailed || hasQualityErrors || hasTypecheckErrors || policiesFailed;
1359
1935
  if (!failed) {
1360
1936
  const thresholdParts = [];
1361
1937
  if (minCoverage !== undefined) {
@@ -1364,25 +1940,31 @@ function registerCheckCommand(program, dependencies = {}) {
1364
1940
  if (maxDrift !== undefined) {
1365
1941
  thresholdParts.push(`drift ${driftScore}% ≤ ${maxDrift}%`);
1366
1942
  }
1943
+ if (policyResult) {
1944
+ thresholdParts.push(`${policyResult.passedCount}/${policyResult.totalPolicies} policies`);
1945
+ }
1367
1946
  if (thresholdParts.length > 0) {
1368
- log(chalk3.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1947
+ log(chalk5.green(`✓ Check passed (${thresholdParts.join(", ")})`));
1369
1948
  } else {
1370
- log(chalk3.green("✓ Check passed"));
1371
- 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."));
1372
1951
  }
1373
1952
  return;
1374
1953
  }
1375
1954
  if (hasQualityErrors) {
1376
- log(chalk3.red(`✗ ${errorCount} quality errors`));
1955
+ log(chalk5.red(`✗ ${errorCount} quality errors`));
1377
1956
  }
1378
1957
  if (hasTypecheckErrors) {
1379
- 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`));
1380
1962
  }
1381
1963
  log("");
1382
- 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"));
1383
1965
  process.exit(1);
1384
1966
  } catch (commandError) {
1385
- error(chalk3.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1967
+ error(chalk5.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1386
1968
  process.exit(1);
1387
1969
  }
1388
1970
  });
@@ -1419,35 +2001,36 @@ import {
1419
2001
  hashString,
1420
2002
  parseMarkdownFiles
1421
2003
  } from "@doccov/sdk";
1422
- import chalk4 from "chalk";
2004
+ import { calculateNextVersion, recommendSemverBump } from "@openpkg-ts/spec";
2005
+ import chalk6 from "chalk";
1423
2006
  import { glob } from "glob";
1424
2007
 
1425
2008
  // src/utils/docs-impact-ai.ts
1426
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1427
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1428
- import { generateObject as generateObject2, generateText } from "ai";
1429
- import { z as z3 } from "zod";
1430
- var CodeBlockUsageSchema = z3.object({
1431
- isImpacted: z3.boolean().describe("Whether the code block is affected by the change"),
1432
- reason: z3.string().describe("Explanation of why/why not the code is impacted"),
1433
- usageType: z3.enum(["direct-call", "import-only", "indirect", "not-used"]).describe("How the export is used in this code block"),
1434
- suggestedFix: z3.string().optional().describe("If impacted, the suggested code change"),
1435
- 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")
1436
2019
  });
1437
- var MultiBlockAnalysisSchema = z3.object({
1438
- groups: z3.array(z3.object({
1439
- blockIndices: z3.array(z3.number()).describe("Indices of blocks that should run together"),
1440
- 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")
1441
2024
  })).describe("Groups of related code blocks"),
1442
- 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)")
1443
2026
  });
1444
- function getModel2() {
2027
+ function getModel3() {
1445
2028
  const provider = process.env.DOCCOV_LLM_PROVIDER?.toLowerCase();
1446
2029
  if (provider === "anthropic" || process.env.ANTHROPIC_API_KEY) {
1447
- const anthropic = createAnthropic2();
2030
+ const anthropic = createAnthropic3();
1448
2031
  return anthropic("claude-sonnet-4-20250514");
1449
2032
  }
1450
- const openai = createOpenAI2();
2033
+ const openai = createOpenAI3();
1451
2034
  return openai("gpt-4o-mini");
1452
2035
  }
1453
2036
  function isAIDocsAnalysisAvailable() {
@@ -1462,7 +2045,7 @@ async function generateImpactSummary(impacts) {
1462
2045
  }
1463
2046
  try {
1464
2047
  const { text } = await generateText({
1465
- model: getModel2(),
2048
+ model: getModel3(),
1466
2049
  prompt: `Summarize these documentation impacts for a GitHub PR comment.
1467
2050
 
1468
2051
  Impacts:
@@ -1507,7 +2090,7 @@ function registerDiffCommand(program, dependencies = {}) {
1507
2090
  ...defaultDependencies2,
1508
2091
  ...dependencies
1509
2092
  };
1510
- 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) => {
1511
2094
  try {
1512
2095
  const baseFile = options.base ?? baseArg;
1513
2096
  const headFile = options.head ?? headArg;
@@ -1538,6 +2121,29 @@ function registerDiffCommand(program, dependencies = {}) {
1538
2121
  }
1539
2122
  const minCoverage = resolveThreshold(options.minCoverage, config?.check?.minCoverage);
1540
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
+ }
1541
2147
  const format = options.format ?? "text";
1542
2148
  const limit = parseInt(options.limit, 10) || 10;
1543
2149
  const checks = getStrictChecks(options.strict);
@@ -1565,8 +2171,8 @@ function registerDiffCommand(program, dependencies = {}) {
1565
2171
  silent: true
1566
2172
  });
1567
2173
  }
1568
- const cacheNote = fromCache ? chalk4.cyan(" (cached)") : "";
1569
- log(chalk4.dim(`Report: ${jsonPath}`) + cacheNote);
2174
+ const cacheNote = fromCache ? chalk6.cyan(" (cached)") : "";
2175
+ log(chalk6.dim(`Report: ${jsonPath}`) + cacheNote);
1570
2176
  }
1571
2177
  break;
1572
2178
  case "json": {
@@ -1627,6 +2233,28 @@ function registerDiffCommand(program, dependencies = {}) {
1627
2233
  log(content);
1628
2234
  break;
1629
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
+ }
1630
2258
  }
1631
2259
  const failures = validateDiff(diff, headSpec, {
1632
2260
  minCoverage,
@@ -1634,18 +2262,18 @@ function registerDiffCommand(program, dependencies = {}) {
1634
2262
  checks
1635
2263
  });
1636
2264
  if (failures.length > 0) {
1637
- log(chalk4.red(`
2265
+ log(chalk6.red(`
1638
2266
  ✗ Check failed`));
1639
2267
  for (const f of failures) {
1640
- log(chalk4.red(` - ${f}`));
2268
+ log(chalk6.red(` - ${f}`));
1641
2269
  }
1642
2270
  process.exitCode = 1;
1643
2271
  } else if (options.strict || minCoverage !== undefined || maxDrift !== undefined) {
1644
- log(chalk4.green(`
2272
+ log(chalk6.green(`
1645
2273
  ✓ All checks passed`));
1646
2274
  }
1647
2275
  } catch (commandError) {
1648
- error(chalk4.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2276
+ error(chalk6.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
1649
2277
  process.exitCode = 1;
1650
2278
  }
1651
2279
  });
@@ -1672,7 +2300,7 @@ async function generateDiff(baseSpec, headSpec, options, config, log) {
1672
2300
  if (!docsPatterns || docsPatterns.length === 0) {
1673
2301
  if (config?.docs?.include) {
1674
2302
  docsPatterns = config.docs.include;
1675
- log(chalk4.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
2303
+ log(chalk6.gray(`Using docs patterns from config: ${docsPatterns.join(", ")}`));
1676
2304
  }
1677
2305
  }
1678
2306
  if (docsPatterns && docsPatterns.length > 0) {
@@ -1695,46 +2323,46 @@ function loadSpec(filePath, readFileSync2) {
1695
2323
  }
1696
2324
  function printSummary(diff, baseName, headName, fromCache, log) {
1697
2325
  log("");
1698
- const cacheIndicator = fromCache ? chalk4.cyan(" (cached)") : "";
1699
- log(chalk4.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
2326
+ const cacheIndicator = fromCache ? chalk6.cyan(" (cached)") : "";
2327
+ log(chalk6.bold(`Comparing: ${baseName} → ${headName}`) + cacheIndicator);
1700
2328
  log("─".repeat(40));
1701
2329
  log("");
1702
- 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;
1703
2331
  const coverageSign = diff.coverageDelta > 0 ? "+" : "";
1704
2332
  log(` Coverage: ${diff.oldCoverage}% → ${diff.newCoverage}% ${coverageColor(`(${coverageSign}${diff.coverageDelta}%)`)}`);
1705
2333
  const breakingCount = diff.breaking.length;
1706
2334
  const highSeverity = diff.categorizedBreaking?.filter((c) => c.severity === "high").length ?? 0;
1707
2335
  if (breakingCount > 0) {
1708
- const severityNote = highSeverity > 0 ? chalk4.red(` (${highSeverity} high severity)`) : "";
1709
- 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}`);
1710
2338
  } else {
1711
- log(` Breaking: ${chalk4.green("0")} changes`);
2339
+ log(` Breaking: ${chalk6.green("0")} changes`);
1712
2340
  }
1713
2341
  const newCount = diff.nonBreaking.length;
1714
2342
  const undocCount = diff.newUndocumented.length;
1715
2343
  if (newCount > 0) {
1716
- const undocNote = undocCount > 0 ? chalk4.yellow(` (${undocCount} undocumented)`) : "";
1717
- log(` New: ${chalk4.green(newCount)} exports${undocNote}`);
2344
+ const undocNote = undocCount > 0 ? chalk6.yellow(` (${undocCount} undocumented)`) : "";
2345
+ log(` New: ${chalk6.green(newCount)} exports${undocNote}`);
1718
2346
  }
1719
2347
  if (diff.driftIntroduced > 0 || diff.driftResolved > 0) {
1720
2348
  const parts = [];
1721
2349
  if (diff.driftIntroduced > 0)
1722
- parts.push(chalk4.red(`+${diff.driftIntroduced}`));
2350
+ parts.push(chalk6.red(`+${diff.driftIntroduced}`));
1723
2351
  if (diff.driftResolved > 0)
1724
- parts.push(chalk4.green(`-${diff.driftResolved}`));
2352
+ parts.push(chalk6.green(`-${diff.driftResolved}`));
1725
2353
  log(` Drift: ${parts.join(", ")}`);
1726
2354
  }
1727
2355
  log("");
1728
2356
  }
1729
2357
  async function printAISummary(diff, log) {
1730
2358
  if (!isAIDocsAnalysisAvailable()) {
1731
- log(chalk4.yellow(`
2359
+ log(chalk6.yellow(`
1732
2360
  ⚠ AI analysis unavailable (set OPENAI_API_KEY or ANTHROPIC_API_KEY)`));
1733
2361
  return;
1734
2362
  }
1735
2363
  if (!diff.docsImpact)
1736
2364
  return;
1737
- log(chalk4.gray(`
2365
+ log(chalk6.gray(`
1738
2366
  Generating AI summary...`));
1739
2367
  const impacts = diff.docsImpact.impactedFiles.flatMap((f) => f.references.map((r) => ({
1740
2368
  file: f.file,
@@ -1745,8 +2373,8 @@ Generating AI summary...`));
1745
2373
  const summary = await generateImpactSummary(impacts);
1746
2374
  if (summary) {
1747
2375
  log("");
1748
- log(chalk4.bold("AI Summary"));
1749
- log(chalk4.cyan(` ${summary}`));
2376
+ log(chalk6.bold("AI Summary"));
2377
+ log(chalk6.cyan(` ${summary}`));
1750
2378
  }
1751
2379
  }
1752
2380
  function validateDiff(diff, headSpec, options) {
@@ -1827,7 +2455,7 @@ function printGitHubAnnotations(diff, log) {
1827
2455
 
1828
2456
  // src/commands/info.ts
1829
2457
  import { DocCov as DocCov2, enrichSpec as enrichSpec2, NodeFileSystem as NodeFileSystem2, resolveTarget as resolveTarget2 } from "@doccov/sdk";
1830
- import chalk5 from "chalk";
2458
+ import chalk7 from "chalk";
1831
2459
  function registerInfoCommand(program) {
1832
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) => {
1833
2461
  try {
@@ -1849,14 +2477,14 @@ function registerInfoCommand(program) {
1849
2477
  const spec = enrichSpec2(specResult.spec);
1850
2478
  const stats = computeStats(spec);
1851
2479
  console.log("");
1852
- console.log(chalk5.bold(`${stats.packageName}@${stats.version}`));
2480
+ console.log(chalk7.bold(`${stats.packageName}@${stats.version}`));
1853
2481
  console.log("");
1854
- console.log(` Exports: ${chalk5.bold(stats.totalExports.toString())}`);
1855
- console.log(` Coverage: ${chalk5.bold(`${stats.coverageScore}%`)}`);
1856
- 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}%`)}`);
1857
2485
  console.log("");
1858
2486
  } catch (err) {
1859
- console.error(chalk5.red("Error:"), err instanceof Error ? err.message : err);
2487
+ console.error(chalk7.red("Error:"), err instanceof Error ? err.message : err);
1860
2488
  process.exit(1);
1861
2489
  }
1862
2490
  });
@@ -1865,7 +2493,7 @@ function registerInfoCommand(program) {
1865
2493
  // src/commands/init.ts
1866
2494
  import * as fs4 from "node:fs";
1867
2495
  import * as path6 from "node:path";
1868
- import chalk6 from "chalk";
2496
+ import chalk8 from "chalk";
1869
2497
  var defaultDependencies3 = {
1870
2498
  fileExists: fs4.existsSync,
1871
2499
  writeFileSync: fs4.writeFileSync,
@@ -1878,39 +2506,39 @@ function registerInitCommand(program, dependencies = {}) {
1878
2506
  ...defaultDependencies3,
1879
2507
  ...dependencies
1880
2508
  };
1881
- program.command("init").description("Create a DocCov configuration file").option("--cwd <dir>", "Working directory", process.cwd()).option("--format <format>", "Config format: auto, mjs, js, cjs", "auto").action((options) => {
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) => {
1882
2510
  const cwd = path6.resolve(options.cwd);
1883
2511
  const formatOption = String(options.format ?? "auto").toLowerCase();
1884
2512
  if (!isValidFormat(formatOption)) {
1885
- error(chalk6.red(`Invalid format "${formatOption}". Use auto, mjs, js, or cjs.`));
2513
+ error(chalk8.red(`Invalid format "${formatOption}". Use auto, mjs, js, cjs, or yaml.`));
1886
2514
  process.exitCode = 1;
1887
2515
  return;
1888
2516
  }
1889
2517
  const existing = findExistingConfig(cwd, fileExists2);
1890
2518
  if (existing) {
1891
- 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.*"}.`));
1892
2520
  process.exitCode = 1;
1893
2521
  return;
1894
2522
  }
1895
2523
  const packageType = detectPackageType(cwd, fileExists2, readFileSync3);
1896
2524
  const targetFormat = resolveFormat(formatOption, packageType);
1897
2525
  if (targetFormat === "js" && packageType !== "module") {
1898
- 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.'));
1899
2527
  }
1900
- const fileName = `doccov.config.${targetFormat}`;
2528
+ const fileName = targetFormat === "yaml" ? "doccov.yml" : `doccov.config.${targetFormat}`;
1901
2529
  const outputPath = path6.join(cwd, fileName);
1902
2530
  if (fileExists2(outputPath)) {
1903
- error(chalk6.red(`Cannot create ${fileName}; file already exists.`));
2531
+ error(chalk8.red(`Cannot create ${fileName}; file already exists.`));
1904
2532
  process.exitCode = 1;
1905
2533
  return;
1906
2534
  }
1907
2535
  const template = buildTemplate(targetFormat);
1908
2536
  writeFileSync3(outputPath, template, { encoding: "utf8" });
1909
- log(chalk6.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
2537
+ log(chalk8.green(`✓ Created ${path6.relative(process.cwd(), outputPath)}`));
1910
2538
  });
1911
2539
  }
1912
2540
  var isValidFormat = (value) => {
1913
- return value === "auto" || value === "mjs" || value === "js" || value === "cjs";
2541
+ return ["auto", "mjs", "js", "cjs", "yaml"].includes(value);
1914
2542
  };
1915
2543
  var findExistingConfig = (cwd, fileExists2) => {
1916
2544
  let current = path6.resolve(cwd);
@@ -1962,12 +2590,34 @@ var findNearestPackageJson = (cwd, fileExists2) => {
1962
2590
  return null;
1963
2591
  };
1964
2592
  var resolveFormat = (format, packageType) => {
2593
+ if (format === "yaml")
2594
+ return "yaml";
1965
2595
  if (format === "auto") {
1966
2596
  return packageType === "module" ? "js" : "mjs";
1967
2597
  }
1968
2598
  return format;
1969
2599
  };
1970
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
2613
+
2614
+ quality:
2615
+ rules:
2616
+ # has-description: warn
2617
+ # has-params: off
2618
+ # has-returns: off
2619
+ `;
2620
+ }
1971
2621
  const configBody = `{
1972
2622
  // Filter which exports to analyze
1973
2623
  // include: ['MyClass', 'myFunction'],
@@ -2018,42 +2668,11 @@ var buildTemplate = (format) => {
2018
2668
  // src/commands/spec.ts
2019
2669
  import * as fs5 from "node:fs";
2020
2670
  import * as path7 from "node:path";
2021
- 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";
2022
2672
  import { normalize, validateSpec } from "@openpkg-ts/spec";
2023
- import chalk8 from "chalk";
2673
+ import chalk9 from "chalk";
2024
2674
  // package.json
2025
- var version = "0.15.1";
2026
-
2027
- // src/utils/filter-options.ts
2028
- import { mergeFilters, parseListFlag } from "@doccov/sdk";
2029
- import chalk7 from "chalk";
2030
- var formatList = (label, values) => `${label}: ${values.map((value) => chalk7.cyan(value)).join(", ")}`;
2031
- var mergeFilterOptions = (config, cliOptions) => {
2032
- const messages = [];
2033
- if (config?.include) {
2034
- messages.push(formatList("include filters from config", config.include));
2035
- }
2036
- if (config?.exclude) {
2037
- messages.push(formatList("exclude filters from config", config.exclude));
2038
- }
2039
- if (cliOptions.include) {
2040
- messages.push(formatList("apply include filters from CLI", cliOptions.include));
2041
- }
2042
- if (cliOptions.exclude) {
2043
- messages.push(formatList("apply exclude filters from CLI", cliOptions.exclude));
2044
- }
2045
- const resolved = mergeFilters(config, cliOptions);
2046
- if (!resolved.include && !resolved.exclude) {
2047
- return { messages };
2048
- }
2049
- const source = resolved.source === "override" ? "cli" : resolved.source;
2050
- return {
2051
- include: resolved.include,
2052
- exclude: resolved.exclude,
2053
- source,
2054
- messages
2055
- };
2056
- };
2675
+ var version = "0.17.0";
2057
2676
 
2058
2677
  // src/commands/spec.ts
2059
2678
  var defaultDependencies4 = {
@@ -2068,7 +2687,7 @@ function getArrayLength(value) {
2068
2687
  function formatDiagnosticOutput(prefix, diagnostic, baseDir) {
2069
2688
  const location = diagnostic.location;
2070
2689
  const relativePath = location?.file ? path7.relative(baseDir, location.file) || location.file : undefined;
2071
- 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;
2072
2691
  const locationPrefix = locationText ? `${locationText} ` : "";
2073
2692
  return `${prefix} ${locationPrefix}${diagnostic.message}`;
2074
2693
  }
@@ -2077,7 +2696,7 @@ function registerSpecCommand(program, dependencies = {}) {
2077
2696
  ...defaultDependencies4,
2078
2697
  ...dependencies
2079
2698
  };
2080
- 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) => {
2081
2700
  try {
2082
2701
  const steps = new StepProgress([
2083
2702
  { label: "Resolved target", activeLabel: "Resolving target" },
@@ -2099,13 +2718,14 @@ function registerSpecCommand(program, dependencies = {}) {
2099
2718
  try {
2100
2719
  config = await loadDocCovConfig(targetDir);
2101
2720
  } catch (configError) {
2102
- 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);
2103
2722
  process.exit(1);
2104
2723
  }
2105
2724
  steps.next();
2106
2725
  const cliFilters = {
2107
2726
  include: parseListFlag(options.include),
2108
- exclude: parseListFlag(options.exclude)
2727
+ exclude: parseListFlag(options.exclude),
2728
+ visibility: parseVisibilityFlag(options.visibility)
2109
2729
  };
2110
2730
  const resolvedFilters = mergeFilterOptions(config, cliFilters);
2111
2731
  const resolveExternalTypes = !options.skipResolve;
@@ -2125,10 +2745,11 @@ function registerSpecCommand(program, dependencies = {}) {
2125
2745
  isMonorepo: resolved.isMonorepo,
2126
2746
  targetPackage: packageInfo?.name
2127
2747
  };
2128
- const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude ? {
2748
+ const analyzeOptions = resolvedFilters.include || resolvedFilters.exclude || resolvedFilters.visibility ? {
2129
2749
  filters: {
2130
2750
  include: resolvedFilters.include,
2131
- exclude: resolvedFilters.exclude
2751
+ exclude: resolvedFilters.exclude,
2752
+ visibility: resolvedFilters.visibility
2132
2753
  },
2133
2754
  generationInput
2134
2755
  } : { generationInput };
@@ -2140,74 +2761,266 @@ function registerSpecCommand(program, dependencies = {}) {
2140
2761
  const normalized = normalize(result.spec);
2141
2762
  const validation = validateSpec(normalized);
2142
2763
  if (!validation.ok) {
2143
- error(chalk8.red("Spec failed schema validation"));
2764
+ error(chalk9.red("Spec failed schema validation"));
2144
2765
  for (const err of validation.errors) {
2145
- error(chalk8.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2766
+ error(chalk9.red(`schema: ${err.instancePath || "/"} ${err.message}`));
2146
2767
  }
2147
2768
  process.exit(1);
2148
2769
  }
2149
2770
  steps.next();
2150
- const outputPath = path7.resolve(process.cwd(), options.output);
2151
- writeFileSync4(outputPath, JSON.stringify(normalized, null, 2));
2152
- steps.complete(`Generated ${options.output}`);
2153
- log(chalk8.gray(` ${getArrayLength(normalized.exports)} exports`));
2154
- 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`));
2155
2783
  if (options.verbose && normalized.generation) {
2156
2784
  const gen = normalized.generation;
2157
2785
  log("");
2158
- log(chalk8.bold("Generation Info"));
2159
- log(chalk8.gray(` Timestamp: ${gen.timestamp}`));
2160
- log(chalk8.gray(` Generator: ${gen.generator.name}@${gen.generator.version}`));
2161
- log(chalk8.gray(` Entry point: ${gen.analysis.entryPoint}`));
2162
- log(chalk8.gray(` Detected via: ${gen.analysis.entryPointSource}`));
2163
- log(chalk8.gray(` Declaration only: ${gen.analysis.isDeclarationOnly ? "yes" : "no"}`));
2164
- 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"}`));
2165
2793
  if (gen.analysis.maxTypeDepth) {
2166
- log(chalk8.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2794
+ log(chalk9.gray(` Max type depth: ${gen.analysis.maxTypeDepth}`));
2167
2795
  }
2168
2796
  log("");
2169
- log(chalk8.bold("Environment"));
2170
- 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"}`));
2171
2799
  if (gen.environment.packageManager) {
2172
- log(chalk8.gray(` Package manager: ${gen.environment.packageManager}`));
2800
+ log(chalk9.gray(` Package manager: ${gen.environment.packageManager}`));
2173
2801
  }
2174
2802
  if (gen.environment.isMonorepo) {
2175
- log(chalk8.gray(` Monorepo: yes`));
2803
+ log(chalk9.gray(` Monorepo: yes`));
2176
2804
  }
2177
2805
  if (gen.environment.targetPackage) {
2178
- log(chalk8.gray(` Target package: ${gen.environment.targetPackage}`));
2806
+ log(chalk9.gray(` Target package: ${gen.environment.targetPackage}`));
2179
2807
  }
2180
2808
  if (gen.issues.length > 0) {
2181
2809
  log("");
2182
- log(chalk8.bold("Issues"));
2810
+ log(chalk9.bold("Issues"));
2183
2811
  for (const issue of gen.issues) {
2184
- 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(">");
2185
2813
  log(`${prefix} [${issue.code}] ${issue.message}`);
2186
2814
  if (issue.suggestion) {
2187
- log(chalk8.gray(` ${issue.suggestion}`));
2815
+ log(chalk9.gray(` ${issue.suggestion}`));
2188
2816
  }
2189
2817
  }
2190
2818
  }
2191
2819
  }
2192
2820
  if (options.showDiagnostics && result.diagnostics.length > 0) {
2193
2821
  log("");
2194
- log(chalk8.bold("Diagnostics"));
2822
+ log(chalk9.bold("Diagnostics"));
2195
2823
  for (const diagnostic of result.diagnostics) {
2196
- 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(">");
2197
2825
  log(formatDiagnosticOutput(prefix, diagnostic, targetDir));
2198
2826
  }
2199
2827
  }
2200
2828
  } catch (commandError) {
2201
- error(chalk8.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2829
+ error(chalk9.red("Error:"), commandError instanceof Error ? commandError.message : commandError);
2202
2830
  process.exit(1);
2203
2831
  }
2204
2832
  });
2205
2833
  }
2206
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
+
2207
3020
  // src/cli.ts
2208
3021
  var __filename2 = fileURLToPath(import.meta.url);
2209
- var __dirname2 = path8.dirname(__filename2);
2210
- 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"));
2211
3024
  var program = new Command;
2212
3025
  program.name("doccov").description("DocCov - Documentation coverage and drift detection for TypeScript").version(packageJson.version);
2213
3026
  registerCheckCommand(program);
@@ -2215,6 +3028,7 @@ registerInfoCommand(program);
2215
3028
  registerSpecCommand(program);
2216
3029
  registerDiffCommand(program);
2217
3030
  registerInitCommand(program);
3031
+ registerTrendsCommand(program);
2218
3032
  program.command("*", { hidden: true }).action(() => {
2219
3033
  program.outputHelp();
2220
3034
  });