@doccov/sdk 0.3.7 → 0.5.7

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/index.d.ts CHANGED
@@ -116,7 +116,11 @@ interface ExportReference {
116
116
  /**
117
117
  * Change type for an impacted reference
118
118
  */
119
- type DocsChangeType = "signature-changed" | "removed" | "deprecated";
119
+ type DocsChangeType = "signature-changed" | "removed" | "deprecated" | "method-removed" | "method-changed" | "method-deprecated";
120
+ /**
121
+ * Member-level change type
122
+ */
123
+ type MemberChangeType = "added" | "removed" | "signature-changed";
120
124
  /**
121
125
  * An impacted reference in a documentation file
122
126
  */
@@ -131,6 +135,14 @@ interface DocsImpactReference {
131
135
  suggestion?: string;
132
136
  /** Context around the reference */
133
137
  context?: string;
138
+ /** Member/method name if this is a member-level change */
139
+ memberName?: string;
140
+ /** Type of member change (added, removed, signature-changed) */
141
+ memberChangeType?: MemberChangeType;
142
+ /** Suggested replacement for removed/changed members */
143
+ replacementSuggestion?: string;
144
+ /** True if this is just a class instantiation (new ClassName()) */
145
+ isInstantiation?: boolean;
134
146
  }
135
147
  /**
136
148
  * Documentation file impact summary
@@ -147,8 +159,10 @@ interface DocsImpact {
147
159
  interface DocsImpactResult {
148
160
  /** Files with impacted references */
149
161
  impactedFiles: DocsImpact[];
150
- /** New exports that have no documentation */
162
+ /** New exports (from this diff) that have no documentation */
151
163
  missingDocs: string[];
164
+ /** ALL exports from the spec that have no documentation in the scanned files */
165
+ allUndocumented: string[];
152
166
  /** Statistics */
153
167
  stats: {
154
168
  /** Total markdown files scanned */
@@ -159,6 +173,10 @@ interface DocsImpactResult {
159
173
  referencesFound: number;
160
174
  /** References impacted by changes */
161
175
  impactedReferences: number;
176
+ /** Total exports in the spec */
177
+ totalExports: number;
178
+ /** Exports found documented in markdown */
179
+ documentedExports: number;
162
180
  };
163
181
  }
164
182
  /**
@@ -198,14 +216,32 @@ declare function findExportReferences(files: MarkdownDocFile[], exportNames: str
198
216
  */
199
217
  declare function blockReferencesExport(block: MarkdownCodeBlock, exportName: string): boolean;
200
218
  import { SpecDiff } from "@openpkg-ts/spec";
219
+ type MemberChangeType2 = "added" | "removed" | "signature-changed";
220
+ interface MemberChange {
221
+ /** The class this member belongs to */
222
+ className: string;
223
+ /** The member name (e.g., "evaluateChainhook") */
224
+ memberName: string;
225
+ /** Kind of member */
226
+ memberKind: "method" | "property" | "accessor" | "constructor";
227
+ /** Type of change */
228
+ changeType: MemberChangeType2;
229
+ /** Old signature string (for signature changes) */
230
+ oldSignature?: string;
231
+ /** New signature string (for signature changes) */
232
+ newSignature?: string;
233
+ /** Suggested replacement (e.g., "Use replayChainhook instead") */
234
+ suggestion?: string;
235
+ }
201
236
  /**
202
237
  * Analyze docs impact from a spec diff
203
238
  *
204
239
  * @param diff - The spec diff result
205
240
  * @param markdownFiles - Parsed markdown files
206
241
  * @param newExportNames - All names in the new spec (for missing docs detection)
242
+ * @param memberChanges - Optional member-level changes for granular detection
207
243
  */
208
- declare function analyzeDocsImpact(diff: SpecDiff, markdownFiles: MarkdownDocFile[], newExportNames?: string[]): DocsImpactResult;
244
+ declare function analyzeDocsImpact(diff: SpecDiff, markdownFiles: MarkdownDocFile[], newExportNames?: string[], memberChanges?: MemberChange[]): DocsImpactResult;
209
245
  /**
210
246
  * Find references to deprecated exports
211
247
  */
@@ -225,13 +261,17 @@ declare function getDocumentedExports(markdownFiles: MarkdownDocFile[], exportNa
225
261
  * Get all exports that lack documentation
226
262
  */
227
263
  declare function getUndocumentedExports(markdownFiles: MarkdownDocFile[], exportNames: string[]): string[];
228
- import { OpenPkg as OpenPkg2, SpecDiff as SpecDiff2 } from "@openpkg-ts/spec";
264
+ import { CategorizedBreaking, OpenPkg as OpenPkg3, SpecDiff as SpecDiff2 } from "@openpkg-ts/spec";
229
265
  /**
230
266
  * Extended spec diff result with docs impact
231
267
  */
232
268
  interface SpecDiffWithDocs extends SpecDiff2 {
233
269
  /** Docs impact analysis (only present if markdown files provided) */
234
270
  docsImpact?: DocsImpactResult;
271
+ /** Member-level changes for classes (methods added/removed/changed) */
272
+ memberChanges?: MemberChange[];
273
+ /** Breaking changes categorized by severity (high/medium/low) */
274
+ categorizedBreaking?: CategorizedBreaking[];
235
275
  }
236
276
  /**
237
277
  * Options for diffSpecWithDocs
@@ -263,7 +303,7 @@ interface DiffWithDocsOptions {
263
303
  * }
264
304
  * ```
265
305
  */
266
- declare function diffSpecWithDocs(oldSpec: OpenPkg2, newSpec: OpenPkg2, options?: DiffWithDocsOptions): SpecDiffWithDocs;
306
+ declare function diffSpecWithDocs(oldSpec: OpenPkg3, newSpec: OpenPkg3, options?: DiffWithDocsOptions): SpecDiffWithDocs;
267
307
  /**
268
308
  * Check if a diff has any docs impact
269
309
  */
@@ -276,6 +316,7 @@ declare function getDocsImpactSummary(diff: SpecDiffWithDocs): {
276
316
  impactedReferenceCount: number;
277
317
  missingDocsCount: number;
278
318
  totalIssues: number;
319
+ memberChangesCount: number;
279
320
  };
280
321
  interface DocCovOptions {
281
322
  includePrivate?: boolean;
@@ -466,7 +507,7 @@ declare class DocCov {
466
507
  declare function analyze(code: string, options?: AnalyzeOptions): Promise<OpenPkgSpec>;
467
508
  declare function analyzeFile(filePath: string, options?: AnalyzeOptions): Promise<OpenPkgSpec>;
468
509
  /** @deprecated Use DocCov instead */
469
- declare const OpenPkg3: typeof DocCov;
510
+ declare const OpenPkg4: typeof DocCov;
470
511
  /**
471
512
  * Project detection types for I/O-agnostic project analysis.
472
513
  * Used by both CLI (NodeFileSystem) and API (SandboxFileSystem).
@@ -723,4 +764,4 @@ declare function readPackageJson(fs: FileSystem, dir: string): Promise<PackageJs
723
764
  * ```
724
765
  */
725
766
  declare function analyzeProject2(fs: FileSystem, options?: AnalyzeProjectOptions): Promise<ProjectInfo>;
726
- export { serializeJSDoc, safeParseJson, runExamplesWithPackage, runExamples, runExample, readPackageJson, parseMarkdownFiles, parseMarkdownFile, parseJSDocToPatch, parseAssertions, mergeFixes, isFixableDrift, isExecutableLang, hasNonAssertionComments, hasDocsImpact, hasDocsForExport, getUndocumentedExports, getRunCommand, getPrimaryBuildScript, getInstallCommand, getDocumentedExports, getDocsImpactSummary, generateFixesForExport, generateFix, formatPackageList, findRemovedReferences, findPackageByName, findJSDocLocation, findExportReferences, findDeprecatedReferences, extractPackageSpec, extractImports, extractFunctionCalls, diffSpecWithDocs, detectPackageManager, detectMonorepo, detectExampleRuntimeErrors, detectExampleAssertionFailures, detectEntryPoint, detectBuildInfo, createSourceFile, categorizeDrifts, blockReferencesExport, applyPatchToJSDoc, applyEdits, analyzeProject2 as analyzeProject, analyzeFile, analyzeDocsImpact, analyze, WorkspacePackage, SpecDiffWithDocs, SandboxFileSystem, RunExamplesWithPackageResult, RunExamplesWithPackageOptions, RunExampleOptions, ProjectInfo, PackageManagerInfo, PackageManager, PackageJson, PackageExports, OpenPkgSpec, OpenPkgOptions, OpenPkg3 as OpenPkg, NodeFileSystem, MonorepoType, MonorepoInfo, MarkdownDocFile, MarkdownCodeBlock, JSDocTag, JSDocReturn, JSDocPatch, JSDocParam, JSDocEdit, FixType, FixSuggestion, FilterOptions, FileSystem, ExportReference, ExampleRunResult, EntryPointSource, EntryPointInfo, DocsImpactResult, DocsImpactReference, DocsImpact, DocsChangeType, DocCovOptions, DocCov, DiffWithDocsOptions, Diagnostic, BuildInfo, ApplyEditsResult, AnalyzeProjectOptions, AnalyzeOptions, AnalysisResult };
767
+ export { serializeJSDoc, safeParseJson, runExamplesWithPackage, runExamples, runExample, readPackageJson, parseMarkdownFiles, parseMarkdownFile, parseJSDocToPatch, parseAssertions, mergeFixes, isFixableDrift, isExecutableLang, hasNonAssertionComments, hasDocsImpact, hasDocsForExport, getUndocumentedExports, getRunCommand, getPrimaryBuildScript, getInstallCommand, getDocumentedExports, getDocsImpactSummary, generateFixesForExport, generateFix, formatPackageList, findRemovedReferences, findPackageByName, findJSDocLocation, findExportReferences, findDeprecatedReferences, extractPackageSpec, extractImports, extractFunctionCalls, diffSpecWithDocs, detectPackageManager, detectMonorepo, detectExampleRuntimeErrors, detectExampleAssertionFailures, detectEntryPoint, detectBuildInfo, createSourceFile, categorizeDrifts, blockReferencesExport, applyPatchToJSDoc, applyEdits, analyzeProject2 as analyzeProject, analyzeFile, analyzeDocsImpact, analyze, WorkspacePackage, SpecDiffWithDocs, SandboxFileSystem, RunExamplesWithPackageResult, RunExamplesWithPackageOptions, RunExampleOptions, ProjectInfo, PackageManagerInfo, PackageManager, PackageJson, PackageExports, OpenPkgSpec, OpenPkgOptions, OpenPkg4 as OpenPkg, NodeFileSystem, MonorepoType, MonorepoInfo, MemberChange, MarkdownDocFile, MarkdownCodeBlock, JSDocTag, JSDocReturn, JSDocPatch, JSDocParam, JSDocEdit, FixType, FixSuggestion, FilterOptions, FileSystem, ExportReference, ExampleRunResult, EntryPointSource, EntryPointInfo, DocsImpactResult, DocsImpactReference, DocsImpact, DocsChangeType, DocCovOptions, DocCov, DiffWithDocsOptions, Diagnostic, BuildInfo, ApplyEditsResult, AnalyzeProjectOptions, AnalyzeOptions, AnalysisResult };
package/dist/index.js CHANGED
@@ -1107,6 +1107,39 @@ function extractFunctionCalls(code) {
1107
1107
  }
1108
1108
  return Array.from(calls);
1109
1109
  }
1110
+ function extractMethodCalls(code) {
1111
+ const calls = [];
1112
+ const lines = code.split(`
1113
+ `);
1114
+ const methodCallRegex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
1115
+ const skipObjects = new Set(["console", "Math", "JSON", "Object", "Array", "String", "Number"]);
1116
+ for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
1117
+ const line = lines[lineIndex];
1118
+ let match;
1119
+ methodCallRegex.lastIndex = 0;
1120
+ while ((match = methodCallRegex.exec(line)) !== null) {
1121
+ const objectName = match[1];
1122
+ const methodName = match[2];
1123
+ if (skipObjects.has(objectName)) {
1124
+ continue;
1125
+ }
1126
+ calls.push({
1127
+ objectName,
1128
+ methodName,
1129
+ line: lineIndex,
1130
+ context: line.trim()
1131
+ });
1132
+ }
1133
+ }
1134
+ return calls;
1135
+ }
1136
+ function hasInstantiation(code, className) {
1137
+ const regex = new RegExp(`\\bnew\\s+${escapeRegex(className)}\\s*\\(`, "g");
1138
+ return regex.test(code);
1139
+ }
1140
+ function escapeRegex(str) {
1141
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1142
+ }
1110
1143
  function findExportReferences(files, exportNames) {
1111
1144
  const references = [];
1112
1145
  const exportSet = new Set(exportNames);
@@ -1168,58 +1201,138 @@ function blockReferencesExport(block, exportName) {
1168
1201
  return calls.includes(exportName);
1169
1202
  }
1170
1203
  // src/markdown/analyzer.ts
1171
- function getChangeType(exportName, diff) {
1204
+ function getChangeType(exportName, diff, newExportNames) {
1172
1205
  if (diff.breaking.includes(exportName)) {
1206
+ if (!newExportNames.includes(exportName)) {
1207
+ return "removed";
1208
+ }
1173
1209
  return "signature-changed";
1174
1210
  }
1175
1211
  return null;
1176
1212
  }
1177
- function analyzeDocsImpact(diff, markdownFiles, newExportNames = []) {
1213
+ function mapMemberChangeType(memberChangeType) {
1214
+ switch (memberChangeType) {
1215
+ case "removed":
1216
+ return "method-removed";
1217
+ case "signature-changed":
1218
+ return "method-changed";
1219
+ default:
1220
+ return "signature-changed";
1221
+ }
1222
+ }
1223
+ function analyzeDocsImpact(diff, markdownFiles, newExportNames = [], memberChanges) {
1178
1224
  const changedExports = [
1179
1225
  ...diff.breaking
1180
1226
  ];
1181
- const references = findExportReferences(markdownFiles, changedExports);
1227
+ const memberChangesByMethod = new Map;
1228
+ if (memberChanges) {
1229
+ for (const mc of memberChanges) {
1230
+ memberChangesByMethod.set(mc.memberName, mc);
1231
+ }
1232
+ }
1233
+ const classesWithMemberChanges = new Set(memberChanges?.map((mc) => mc.className) ?? []);
1182
1234
  const impactByFile = new Map;
1183
- for (const ref of references) {
1184
- const changeType = getChangeType(ref.exportName, diff);
1185
- if (!changeType)
1186
- continue;
1187
- let impact = impactByFile.get(ref.file);
1235
+ const addReference = (file, ref) => {
1236
+ let impact = impactByFile.get(file);
1188
1237
  if (!impact) {
1189
- impact = { file: ref.file, references: [] };
1190
- impactByFile.set(ref.file, impact);
1191
- }
1192
- impact.references.push({
1193
- exportName: ref.exportName,
1194
- line: ref.line,
1195
- changeType,
1196
- context: ref.context
1197
- });
1238
+ impact = { file, references: [] };
1239
+ impactByFile.set(file, impact);
1240
+ }
1241
+ impact.references.push(ref);
1242
+ };
1243
+ for (const mdFile of markdownFiles) {
1244
+ for (const block of mdFile.codeBlocks) {
1245
+ const reportedRefs = new Set;
1246
+ if (memberChanges && memberChanges.length > 0) {
1247
+ const methodCalls = extractMethodCalls(block.code);
1248
+ for (const call of methodCalls) {
1249
+ const memberChange = memberChangesByMethod.get(call.methodName);
1250
+ if (memberChange) {
1251
+ const refKey = `${mdFile.path}:${block.lineStart + call.line}:${call.methodName}`;
1252
+ if (!reportedRefs.has(refKey)) {
1253
+ reportedRefs.add(refKey);
1254
+ addReference(mdFile.path, {
1255
+ exportName: memberChange.className,
1256
+ memberName: call.methodName,
1257
+ memberChangeType: memberChange.changeType,
1258
+ changeType: mapMemberChangeType(memberChange.changeType),
1259
+ replacementSuggestion: memberChange.suggestion,
1260
+ line: block.lineStart + call.line,
1261
+ context: call.context,
1262
+ isInstantiation: false
1263
+ });
1264
+ }
1265
+ }
1266
+ }
1267
+ for (const className of classesWithMemberChanges) {
1268
+ if (hasInstantiation(block.code, className)) {
1269
+ const refKey = `${mdFile.path}:${block.lineStart}:new ${className}`;
1270
+ if (!reportedRefs.has(refKey)) {
1271
+ const hasMethodRefs = Array.from(reportedRefs).some((key) => key.startsWith(`${mdFile.path}:`) && memberChanges.some((mc) => mc.className === className && key.includes(mc.memberName)));
1272
+ if (!hasMethodRefs) {
1273
+ reportedRefs.add(refKey);
1274
+ addReference(mdFile.path, {
1275
+ exportName: className,
1276
+ line: block.lineStart,
1277
+ changeType: "signature-changed",
1278
+ context: `new ${className}(...)`,
1279
+ isInstantiation: true
1280
+ });
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ const exportsWithoutMemberChanges = changedExports.filter((name) => !classesWithMemberChanges.has(name));
1287
+ if (exportsWithoutMemberChanges.length > 0) {
1288
+ const refs = findExportReferences([{ ...mdFile, codeBlocks: [block] }], exportsWithoutMemberChanges);
1289
+ for (const ref of refs) {
1290
+ const changeType = getChangeType(ref.exportName, diff, newExportNames);
1291
+ if (!changeType)
1292
+ continue;
1293
+ const refKey = `${ref.file}:${ref.line}:${ref.exportName}`;
1294
+ if (!reportedRefs.has(refKey)) {
1295
+ reportedRefs.add(refKey);
1296
+ addReference(ref.file, {
1297
+ exportName: ref.exportName,
1298
+ line: ref.line,
1299
+ changeType,
1300
+ context: ref.context
1301
+ });
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1198
1306
  }
1199
- const documentedExports = new Set;
1307
+ const documentedExportsSet = new Set;
1200
1308
  for (const file of markdownFiles) {
1201
1309
  for (const block of file.codeBlocks) {
1202
1310
  for (const exportName of newExportNames) {
1203
1311
  if (block.code.includes(exportName)) {
1204
- documentedExports.add(exportName);
1312
+ documentedExportsSet.add(exportName);
1205
1313
  }
1206
1314
  }
1207
1315
  }
1208
1316
  }
1209
- const missingDocs = diff.nonBreaking.filter((name) => !documentedExports.has(name));
1317
+ const missingDocs = diff.nonBreaking.filter((name) => !documentedExportsSet.has(name));
1318
+ const allUndocumented = newExportNames.filter((name) => !documentedExportsSet.has(name));
1210
1319
  const totalCodeBlocks = markdownFiles.reduce((sum, f) => sum + f.codeBlocks.length, 0);
1211
- const allReferences = findExportReferences(markdownFiles, [
1212
- ...changedExports,
1213
- ...diff.nonBreaking
1214
- ]);
1320
+ const allReferences = findExportReferences(markdownFiles, [...changedExports, ...diff.nonBreaking]);
1321
+ let impactedReferences = 0;
1322
+ for (const impact of impactByFile.values()) {
1323
+ impactedReferences += impact.references.length;
1324
+ }
1215
1325
  return {
1216
1326
  impactedFiles: Array.from(impactByFile.values()),
1217
1327
  missingDocs,
1328
+ allUndocumented,
1218
1329
  stats: {
1219
1330
  filesScanned: markdownFiles.length,
1220
1331
  codeBlocksFound: totalCodeBlocks,
1221
1332
  referencesFound: allReferences.length,
1222
- impactedReferences: references.length
1333
+ impactedReferences,
1334
+ totalExports: newExportNames.length,
1335
+ documentedExports: documentedExportsSet.size
1223
1336
  }
1224
1337
  };
1225
1338
  }
@@ -1246,18 +1359,251 @@ function getUndocumentedExports(markdownFiles, exportNames) {
1246
1359
  const documented = new Set(getDocumentedExports(markdownFiles, exportNames));
1247
1360
  return exportNames.filter((name) => !documented.has(name));
1248
1361
  }
1362
+ // src/markdown/member-diff.ts
1363
+ function diffMemberChanges(oldSpec, newSpec, changedClassNames) {
1364
+ const changes = [];
1365
+ const oldExportMap = toExportMap(oldSpec.exports ?? []);
1366
+ const newExportMap = toExportMap(newSpec.exports ?? []);
1367
+ for (const className of changedClassNames) {
1368
+ const oldExport = oldExportMap.get(className);
1369
+ const newExport = newExportMap.get(className);
1370
+ if (!oldExport?.members && !newExport?.members) {
1371
+ continue;
1372
+ }
1373
+ const oldMembers = toMemberMap(oldExport?.members ?? []);
1374
+ const newMembers = toMemberMap(newExport?.members ?? []);
1375
+ const addedMemberNames = [];
1376
+ for (const [memberName, newMember] of newMembers) {
1377
+ if (!oldMembers.has(memberName)) {
1378
+ addedMemberNames.push(memberName);
1379
+ changes.push({
1380
+ className,
1381
+ memberName,
1382
+ memberKind: getMemberKind(newMember),
1383
+ changeType: "added",
1384
+ newSignature: formatSignature(newMember)
1385
+ });
1386
+ }
1387
+ }
1388
+ for (const [memberName, oldMember] of oldMembers) {
1389
+ if (!newMembers.has(memberName)) {
1390
+ const suggestion = findSimilarMember(memberName, newMembers, addedMemberNames);
1391
+ changes.push({
1392
+ className,
1393
+ memberName,
1394
+ memberKind: getMemberKind(oldMember),
1395
+ changeType: "removed",
1396
+ oldSignature: formatSignature(oldMember),
1397
+ suggestion
1398
+ });
1399
+ }
1400
+ }
1401
+ for (const [memberName, oldMember] of oldMembers) {
1402
+ const newMember = newMembers.get(memberName);
1403
+ if (newMember && hasSignatureChanged(oldMember, newMember)) {
1404
+ changes.push({
1405
+ className,
1406
+ memberName,
1407
+ memberKind: getMemberKind(newMember),
1408
+ changeType: "signature-changed",
1409
+ oldSignature: formatSignature(oldMember),
1410
+ newSignature: formatSignature(newMember)
1411
+ });
1412
+ }
1413
+ }
1414
+ }
1415
+ return deduplicateMemberChanges(changes);
1416
+ }
1417
+ function deduplicateMemberChanges(changes) {
1418
+ const seen = new Set;
1419
+ return changes.filter((mc) => {
1420
+ const key = `${mc.className}:${mc.memberName}:${mc.changeType}`;
1421
+ if (seen.has(key))
1422
+ return false;
1423
+ seen.add(key);
1424
+ return true;
1425
+ });
1426
+ }
1427
+ function toExportMap(exports) {
1428
+ const map = new Map;
1429
+ for (const exp of exports) {
1430
+ if (exp?.name) {
1431
+ map.set(exp.name, exp);
1432
+ }
1433
+ }
1434
+ return map;
1435
+ }
1436
+ function toMemberMap(members) {
1437
+ const map = new Map;
1438
+ for (const member of members) {
1439
+ if (member?.name) {
1440
+ map.set(member.name, member);
1441
+ }
1442
+ }
1443
+ return map;
1444
+ }
1445
+ function getMemberKind(member) {
1446
+ switch (member.kind) {
1447
+ case "method":
1448
+ return "method";
1449
+ case "property":
1450
+ return "property";
1451
+ case "accessor":
1452
+ return "accessor";
1453
+ case "constructor":
1454
+ return "constructor";
1455
+ default:
1456
+ return "method";
1457
+ }
1458
+ }
1459
+ function formatSignature(member) {
1460
+ if (!member.signatures?.length) {
1461
+ return member.name ?? "";
1462
+ }
1463
+ const sig = member.signatures[0];
1464
+ const params = sig.parameters?.map((p) => {
1465
+ const optional = p.required === false ? "?" : "";
1466
+ const typeName = extractTypeName(p.schema);
1467
+ return typeName ? `${p.name}${optional}: ${typeName}` : `${p.name}${optional}`;
1468
+ }) ?? [];
1469
+ return `${member.name}(${params.join(", ")})`;
1470
+ }
1471
+ function extractTypeName(schema) {
1472
+ if (!schema || typeof schema !== "object") {
1473
+ return;
1474
+ }
1475
+ const s = schema;
1476
+ if (typeof s.$ref === "string") {
1477
+ const parts = s.$ref.split("/");
1478
+ return parts[parts.length - 1];
1479
+ }
1480
+ if (typeof s.type === "string") {
1481
+ return s.type;
1482
+ }
1483
+ if (typeof s.tsType === "string") {
1484
+ const tsType = s.tsType;
1485
+ if (tsType.length > 30) {
1486
+ return tsType.slice(0, 27) + "...";
1487
+ }
1488
+ return tsType;
1489
+ }
1490
+ return;
1491
+ }
1492
+ function hasSignatureChanged(oldMember, newMember) {
1493
+ const oldSigs = oldMember.signatures ?? [];
1494
+ const newSigs = newMember.signatures ?? [];
1495
+ if (oldSigs.length !== newSigs.length) {
1496
+ return true;
1497
+ }
1498
+ for (let i = 0;i < oldSigs.length; i++) {
1499
+ if (!signaturesEqual(oldSigs[i], newSigs[i])) {
1500
+ return true;
1501
+ }
1502
+ }
1503
+ return false;
1504
+ }
1505
+ function signaturesEqual(a, b) {
1506
+ const aParams = a.parameters ?? [];
1507
+ const bParams = b.parameters ?? [];
1508
+ if (aParams.length !== bParams.length) {
1509
+ return false;
1510
+ }
1511
+ for (let i = 0;i < aParams.length; i++) {
1512
+ const ap = aParams[i];
1513
+ const bp = bParams[i];
1514
+ if (ap.name !== bp.name)
1515
+ return false;
1516
+ if (ap.required !== bp.required)
1517
+ return false;
1518
+ if (JSON.stringify(ap.schema) !== JSON.stringify(bp.schema))
1519
+ return false;
1520
+ }
1521
+ if (JSON.stringify(a.returns) !== JSON.stringify(b.returns)) {
1522
+ return false;
1523
+ }
1524
+ return true;
1525
+ }
1526
+ function findSimilarMember(removedName, newMembers, addedMembers) {
1527
+ const candidates = addedMembers.length > 0 ? addedMembers : Array.from(newMembers.keys());
1528
+ let bestMatch;
1529
+ let bestScore = 0;
1530
+ for (const name of candidates) {
1531
+ if (name === removedName)
1532
+ continue;
1533
+ const removedWords = splitCamelCase(removedName);
1534
+ const newWords = splitCamelCase(name);
1535
+ let matchingWords = 0;
1536
+ let suffixMatch = false;
1537
+ if (removedWords.length > 0 && newWords.length > 0) {
1538
+ const removedSuffix = removedWords[removedWords.length - 1];
1539
+ const newSuffix = newWords[newWords.length - 1];
1540
+ if (removedSuffix === newSuffix) {
1541
+ suffixMatch = true;
1542
+ matchingWords += 2;
1543
+ }
1544
+ }
1545
+ for (const word of removedWords) {
1546
+ if (newWords.includes(word)) {
1547
+ matchingWords++;
1548
+ }
1549
+ }
1550
+ const wordScore = matchingWords / Math.max(removedWords.length, newWords.length);
1551
+ const editDistance = levenshteinDistance(removedName.toLowerCase(), name.toLowerCase());
1552
+ const maxLen = Math.max(removedName.length, name.length);
1553
+ const levenScore = 1 - editDistance / maxLen;
1554
+ const totalScore = suffixMatch ? wordScore * 1.5 + levenScore : wordScore + levenScore * 0.5;
1555
+ if (totalScore > bestScore && totalScore >= 0.5) {
1556
+ bestScore = totalScore;
1557
+ bestMatch = name;
1558
+ }
1559
+ }
1560
+ return bestMatch ? `Use ${bestMatch} instead` : undefined;
1561
+ }
1562
+ function levenshteinDistance(a, b) {
1563
+ const matrix = [];
1564
+ for (let i = 0;i <= b.length; i++) {
1565
+ matrix[i] = [i];
1566
+ }
1567
+ for (let j = 0;j <= a.length; j++) {
1568
+ matrix[0][j] = j;
1569
+ }
1570
+ for (let i = 1;i <= b.length; i++) {
1571
+ for (let j = 1;j <= a.length; j++) {
1572
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
1573
+ matrix[i][j] = matrix[i - 1][j - 1];
1574
+ } else {
1575
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
1576
+ }
1577
+ }
1578
+ }
1579
+ return matrix[b.length][a.length];
1580
+ }
1581
+ function splitCamelCase(str) {
1582
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
1583
+ }
1249
1584
  // src/markdown/diff-with-docs.ts
1250
- import { diffSpec } from "@openpkg-ts/spec";
1585
+ import {
1586
+ categorizeBreakingChanges,
1587
+ diffSpec
1588
+ } from "@openpkg-ts/spec";
1251
1589
  function diffSpecWithDocs(oldSpec, newSpec, options = {}) {
1252
1590
  const baseDiff = diffSpec(oldSpec, newSpec);
1591
+ const memberChanges = diffMemberChanges(oldSpec, newSpec, baseDiff.breaking);
1592
+ const categorizedBreaking = categorizeBreakingChanges(baseDiff.breaking, oldSpec, newSpec, memberChanges);
1253
1593
  if (!options.markdownFiles?.length) {
1254
- return baseDiff;
1594
+ return {
1595
+ ...baseDiff,
1596
+ memberChanges: memberChanges.length > 0 ? memberChanges : undefined,
1597
+ categorizedBreaking: categorizedBreaking.length > 0 ? categorizedBreaking : undefined
1598
+ };
1255
1599
  }
1256
1600
  const newExportNames = newSpec.exports?.map((e) => e.name) ?? [];
1257
- const docsImpact = analyzeDocsImpact(baseDiff, options.markdownFiles, newExportNames);
1601
+ const docsImpact = analyzeDocsImpact(baseDiff, options.markdownFiles, newExportNames, memberChanges);
1258
1602
  return {
1259
1603
  ...baseDiff,
1260
- docsImpact
1604
+ docsImpact,
1605
+ memberChanges: memberChanges.length > 0 ? memberChanges : undefined,
1606
+ categorizedBreaking: categorizedBreaking.length > 0 ? categorizedBreaking : undefined
1261
1607
  };
1262
1608
  }
1263
1609
  function hasDocsImpact(diff) {
@@ -1271,7 +1617,8 @@ function getDocsImpactSummary(diff) {
1271
1617
  impactedFileCount: 0,
1272
1618
  impactedReferenceCount: 0,
1273
1619
  missingDocsCount: 0,
1274
- totalIssues: 0
1620
+ totalIssues: 0,
1621
+ memberChangesCount: diff.memberChanges?.length ?? 0
1275
1622
  };
1276
1623
  }
1277
1624
  const impactedFileCount = diff.docsImpact.impactedFiles.length;
@@ -1281,7 +1628,8 @@ function getDocsImpactSummary(diff) {
1281
1628
  impactedFileCount,
1282
1629
  impactedReferenceCount,
1283
1630
  missingDocsCount,
1284
- totalIssues: impactedReferenceCount + missingDocsCount
1631
+ totalIssues: impactedReferenceCount + missingDocsCount,
1632
+ memberChangesCount: diff.memberChanges?.length ?? 0
1285
1633
  };
1286
1634
  }
1287
1635
  // src/analysis/run-analysis.ts
@@ -4122,7 +4470,7 @@ function generateAssertionFix(drift, exportEntry) {
4122
4470
  const oldValue = oldValueMatch?.[1];
4123
4471
  if (!oldValue)
4124
4472
  return null;
4125
- const updatedExample = oldExample.replace(new RegExp(`//\\s*=>\\s*${escapeRegex(oldValue)}`, "g"), `// => ${newValue}`);
4473
+ const updatedExample = oldExample.replace(new RegExp(`//\\s*=>\\s*${escapeRegex2(oldValue)}`, "g"), `// => ${newValue}`);
4126
4474
  const updatedExamples = [...examples];
4127
4475
  updatedExamples[exampleIndex] = updatedExample;
4128
4476
  return {
@@ -4240,7 +4588,7 @@ function stringifySchema(schema) {
4240
4588
  }
4241
4589
  return "unknown";
4242
4590
  }
4243
- function escapeRegex(str) {
4591
+ function escapeRegex2(str) {
4244
4592
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4245
4593
  }
4246
4594
  function categorizeDrifts(drifts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/sdk",
3
- "version": "0.3.7",
3
+ "version": "0.5.7",
4
4
  "description": "DocCov SDK - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",
@@ -39,7 +39,7 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@openpkg-ts/spec": "^0.3.1",
42
+ "@openpkg-ts/spec": "^0.4.0",
43
43
  "@vercel/sandbox": "^1.0.3",
44
44
  "mdast": "^3.0.0",
45
45
  "remark-mdx": "^3.1.0",