@doccov/sdk 0.22.0 → 0.22.1

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
@@ -331,9 +331,28 @@ type DriftResult = {
331
331
  exports: Map<string, SpecDocDrift[]>;
332
332
  };
333
333
  /**
334
+ * Information about an for context-aware suggestions.
335
+ */
336
+ interface ExportInfo {
337
+ name: string;
338
+ kind: string;
339
+ isCallable: boolean;
340
+ }
341
+ /**
342
+ * Registry of exports and types for cross-reference validation.
343
+ */
344
+ interface ExportRegistry {
345
+ /** Map of names to their info (for context-aware suggestions) */
346
+ exports: Map<string, ExportInfo>;
347
+ /** Set of type names (interfaces, type aliases, etc.) */
348
+ types: Set<string>;
349
+ /** Combined set of all names (for backward compatibility) */
350
+ all: Set<string>;
351
+ }
352
+ /**
334
353
  * Build a registry of all export/type names for cross-reference validation.
335
354
  */
336
- declare function buildExportRegistry(spec: OpenPkgSpec): Set<string>;
355
+ declare function buildExportRegistry(spec: OpenPkgSpec): ExportRegistry;
337
356
  /**
338
357
  * Compute drift for all exports in a spec.
339
358
  *
@@ -344,10 +363,10 @@ declare function computeDrift(spec: OpenPkgSpec): DriftResult;
344
363
  * Compute drift for a single export.
345
364
  *
346
365
  * @param entry - The to analyze
347
- * @param exportRegistry - Registry of known names for link validation
366
+ * @param registry - Registry of known exports and types for validation
348
367
  * @returns Array of drift issues detected
349
368
  */
350
- declare function computeExportDrift(entry: SpecExport, exportRegistry?: Set<string>): SpecDocDrift[];
369
+ declare function computeExportDrift(entry: SpecExport, registry?: ExportRegistry): SpecDocDrift[];
351
370
  /**
352
371
  * Detect runtime errors in @example blocks.
353
372
  * Results are provided externally after running examples via runExamples().
package/dist/index.js CHANGED
@@ -1395,27 +1395,42 @@ var LIBRARY_INTERNAL_PATTERNS = [
1395
1395
 
1396
1396
  // src/analysis/docs-coverage.ts
1397
1397
  function buildExportRegistry(spec) {
1398
- const registry = new Set;
1398
+ const exports = new Map;
1399
+ const types = new Set;
1400
+ const all = new Set;
1399
1401
  for (const entry of spec.exports ?? []) {
1400
- registry.add(entry.name);
1401
- registry.add(entry.id);
1402
+ const info = {
1403
+ name: entry.name,
1404
+ kind: entry.kind ?? "unknown",
1405
+ isCallable: ["function", "class"].includes(entry.kind ?? "")
1406
+ };
1407
+ exports.set(entry.name, info);
1408
+ if (entry.id)
1409
+ exports.set(entry.id, info);
1410
+ all.add(entry.name);
1411
+ if (entry.id)
1412
+ all.add(entry.id);
1402
1413
  }
1403
1414
  for (const type of spec.types ?? []) {
1404
- registry.add(type.name);
1405
- registry.add(type.id);
1415
+ types.add(type.name);
1416
+ if (type.id)
1417
+ types.add(type.id);
1418
+ all.add(type.name);
1419
+ if (type.id)
1420
+ all.add(type.id);
1406
1421
  }
1407
- return registry;
1422
+ return { exports, types, all };
1408
1423
  }
1409
1424
  function computeDrift(spec) {
1410
- const exportRegistry = buildExportRegistry(spec);
1425
+ const registry = buildExportRegistry(spec);
1411
1426
  const exports = new Map;
1412
1427
  for (const entry of spec.exports ?? []) {
1413
- const drift = computeExportDrift(entry, exportRegistry);
1428
+ const drift = computeExportDrift(entry, registry);
1414
1429
  exports.set(entry.id ?? entry.name, drift);
1415
1430
  }
1416
1431
  return { exports };
1417
1432
  }
1418
- function computeExportDrift(entry, exportRegistry) {
1433
+ function computeExportDrift(entry, registry) {
1419
1434
  return [
1420
1435
  ...detectParamDrift(entry),
1421
1436
  ...detectOptionalityDrift(entry),
@@ -1424,8 +1439,8 @@ function computeExportDrift(entry, exportRegistry) {
1424
1439
  ...detectGenericConstraintDrift(entry),
1425
1440
  ...detectDeprecatedDrift(entry),
1426
1441
  ...detectVisibilityDrift(entry),
1427
- ...detectExampleDrift(entry, exportRegistry),
1428
- ...detectBrokenLinks(entry, exportRegistry),
1442
+ ...detectExampleDrift(entry, registry),
1443
+ ...detectBrokenLinks(entry, registry),
1429
1444
  ...detectExampleSyntaxErrors(entry),
1430
1445
  ...detectAsyncMismatch(entry),
1431
1446
  ...detectPropertyTypeDrift(entry)
@@ -1472,24 +1487,39 @@ function detectParamDrift(entry) {
1472
1487
  if (properties.has(firstProperty)) {
1473
1488
  continue;
1474
1489
  }
1475
- const suggestion2 = findClosestMatch(firstProperty, Array.from(properties));
1490
+ const propsArray = Array.from(properties);
1491
+ const suggestion2 = findClosestMatch(firstProperty, propsArray);
1492
+ let suggestionText2;
1493
+ if (suggestion2) {
1494
+ suggestionText2 = `Did you mean "${prefix}.${suggestion2.value}"?`;
1495
+ } else if (propsArray.length > 0 && propsArray.length <= 8) {
1496
+ const propsList = propsArray.slice(0, 5).map((p) => `${prefix}.${p}`);
1497
+ suggestionText2 = propsArray.length > 5 ? `Available: ${propsList.join(", ")}... (${propsArray.length} total)` : `Available: ${propsList.join(", ")}`;
1498
+ }
1476
1499
  drifts.push({
1477
1500
  type: "param-mismatch",
1478
1501
  target: documentedName,
1479
1502
  issue: `JSDoc documents property "${propertyPath}" on parameter "${prefix}" which does not exist.`,
1480
- suggestion: suggestion2?.distance !== undefined && suggestion2.distance <= 3 ? `${prefix}.${suggestion2.value}` : undefined
1503
+ suggestion: suggestionText2
1481
1504
  });
1482
1505
  continue;
1483
1506
  }
1484
1507
  continue;
1485
1508
  }
1486
1509
  }
1487
- const suggestion = findClosestMatch(documentedName, Array.from(actualParamNames));
1510
+ const paramsArray = Array.from(actualParamNames);
1511
+ const suggestion = findClosestMatch(documentedName, paramsArray);
1512
+ let suggestionText;
1513
+ if (suggestion) {
1514
+ suggestionText = `Did you mean "${suggestion.value}"?`;
1515
+ } else if (paramsArray.length > 0 && paramsArray.length <= 6) {
1516
+ suggestionText = `Available parameters: ${paramsArray.join(", ")}`;
1517
+ }
1488
1518
  drifts.push({
1489
1519
  type: "param-mismatch",
1490
1520
  target: documentedName,
1491
1521
  issue: `JSDoc documents parameter "${documentedName}" which is not present in the signature.`,
1492
- suggestion: suggestion?.distance !== undefined && suggestion.distance <= 3 ? suggestion.value : undefined
1522
+ suggestion: suggestionText
1493
1523
  });
1494
1524
  }
1495
1525
  return drifts;
@@ -1960,26 +1990,53 @@ function buildGenericConstraintSuggestion(templateName, actualConstraint) {
1960
1990
  }
1961
1991
  return `Remove the constraint from @template ${templateName} to match the declaration.`;
1962
1992
  }
1993
+ function splitCamelCase(str) {
1994
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/).filter(Boolean);
1995
+ }
1963
1996
  function findClosestMatch(source, candidates) {
1964
1997
  if (candidates.length === 0) {
1965
1998
  return;
1966
1999
  }
1967
- const normalizedSource = source.toLowerCase();
1968
- const substringCandidate = candidates.find((candidate) => {
1969
- const normalizedCandidate = candidate.toLowerCase();
1970
- return normalizedCandidate.includes(normalizedSource) || normalizedSource.includes(normalizedCandidate);
1971
- });
1972
- if (substringCandidate && substringCandidate !== source) {
1973
- return { value: substringCandidate, distance: 0 };
1974
- }
1975
- let best;
2000
+ const sourceWords = splitCamelCase(source);
2001
+ let bestMatch;
2002
+ let bestScore = 0;
1976
2003
  for (const candidate of candidates) {
1977
- const distance = levenshtein(source, candidate);
1978
- if (!best || distance < best.distance) {
1979
- best = { value: candidate, distance };
2004
+ if (candidate === source)
2005
+ continue;
2006
+ const candidateWords = splitCamelCase(candidate);
2007
+ let matchingWords = 0;
2008
+ let suffixMatch = false;
2009
+ if (sourceWords.length > 0 && candidateWords.length > 0) {
2010
+ const sourceSuffix = sourceWords[sourceWords.length - 1];
2011
+ const candidateSuffix = candidateWords[candidateWords.length - 1];
2012
+ if (sourceSuffix === candidateSuffix) {
2013
+ suffixMatch = true;
2014
+ matchingWords += 1.5;
2015
+ }
2016
+ }
2017
+ const suffixWord = suffixMatch ? sourceWords[sourceWords.length - 1] : null;
2018
+ for (const word of sourceWords) {
2019
+ if (word !== suffixWord && candidateWords.includes(word)) {
2020
+ matchingWords++;
2021
+ }
2022
+ }
2023
+ if (matchingWords < 2)
2024
+ continue;
2025
+ const wordScore = matchingWords / Math.max(sourceWords.length, candidateWords.length);
2026
+ const editDistance = levenshtein(source.toLowerCase(), candidate.toLowerCase());
2027
+ const maxLen = Math.max(source.length, candidate.length);
2028
+ const levScore = 1 - editDistance / maxLen;
2029
+ const totalScore = suffixMatch ? wordScore * 1.5 + levScore : wordScore + levScore * 0.5;
2030
+ if (totalScore > bestScore && totalScore >= 0.5) {
2031
+ bestScore = totalScore;
2032
+ bestMatch = candidate;
1980
2033
  }
1981
2034
  }
1982
- return best;
2035
+ if (!bestMatch) {
2036
+ return;
2037
+ }
2038
+ const distance = Math.round((1 - bestScore) * 10);
2039
+ return { value: bestMatch, distance };
1983
2040
  }
1984
2041
  function levenshtein(a, b) {
1985
2042
  if (a === b) {
@@ -2009,8 +2066,22 @@ function levenshtein(a, b) {
2009
2066
  }
2010
2067
  return matrix[b.length][a.length];
2011
2068
  }
2012
- function detectExampleDrift(entry, exportRegistry) {
2013
- if (!exportRegistry || !entry.examples?.length)
2069
+ function getIdentifierContext(node) {
2070
+ const parent = node.parent;
2071
+ if (!parent)
2072
+ return "value";
2073
+ if (ts2.isCallExpression(parent) && parent.expression === node)
2074
+ return "call";
2075
+ if (ts2.isNewExpression(parent) && parent.expression === node)
2076
+ return "call";
2077
+ if (ts2.isTypeReferenceNode(parent))
2078
+ return "type";
2079
+ if (ts2.isExpressionWithTypeArguments(parent))
2080
+ return "type";
2081
+ return "value";
2082
+ }
2083
+ function detectExampleDrift(entry, registry) {
2084
+ if (!registry || !entry.examples?.length)
2014
2085
  return [];
2015
2086
  const drifts = [];
2016
2087
  for (const example of entry.examples) {
@@ -2024,7 +2095,11 @@ function detectExampleDrift(entry, exportRegistry) {
2024
2095
  if (isLocalDeclaration(node)) {
2025
2096
  localDeclarations.add(text);
2026
2097
  } else if (isIdentifierReference(node) && !isBuiltInIdentifier(text)) {
2027
- referencedIdentifiers.add(text);
2098
+ const context = getIdentifierContext(node);
2099
+ const existing = referencedIdentifiers.get(text);
2100
+ if (!existing || context === "call") {
2101
+ referencedIdentifiers.set(text, context);
2102
+ }
2028
2103
  }
2029
2104
  }
2030
2105
  ts2.forEachChild(node, visit);
@@ -2036,16 +2111,27 @@ function detectExampleDrift(entry, exportRegistry) {
2036
2111
  continue;
2037
2112
  const sourceFile = ts2.createSourceFile("example.ts", codeContent, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
2038
2113
  const localDeclarations = new Set;
2039
- const referencedIdentifiers = new Set;
2114
+ const referencedIdentifiers = new Map;
2040
2115
  visit(sourceFile);
2041
2116
  for (const local of localDeclarations) {
2042
2117
  referencedIdentifiers.delete(local);
2043
2118
  }
2044
- for (const identifier of referencedIdentifiers) {
2045
- if (!exportRegistry.has(identifier)) {
2046
- const suggestion = findClosestMatch(identifier, Array.from(exportRegistry));
2119
+ for (const [identifier, context] of referencedIdentifiers) {
2120
+ if (!registry.all.has(identifier)) {
2121
+ let candidates;
2122
+ if (context === "call") {
2123
+ candidates = Array.from(registry.exports.values()).filter((e) => e.isCallable).map((e) => e.name);
2124
+ } else if (context === "type") {
2125
+ candidates = [
2126
+ ...Array.from(registry.types),
2127
+ ...Array.from(registry.exports.values()).filter((e) => ["class", "interface", "type", "enum"].includes(e.kind)).map((e) => e.name)
2128
+ ];
2129
+ } else {
2130
+ candidates = Array.from(registry.exports.keys());
2131
+ }
2132
+ const suggestion = findClosestMatch(identifier, candidates);
2047
2133
  const isPascal = /^[A-Z]/.test(identifier);
2048
- const hasCloseMatch = suggestion && suggestion.distance <= 3;
2134
+ const hasCloseMatch = suggestion && suggestion.distance <= 5;
2049
2135
  if (hasCloseMatch || isPascal) {
2050
2136
  drifts.push({
2051
2137
  type: "example-drift",
@@ -2087,8 +2173,8 @@ function isIdentifierReference(node) {
2087
2173
  return false;
2088
2174
  return true;
2089
2175
  }
2090
- function detectBrokenLinks(entry, exportRegistry) {
2091
- if (!exportRegistry) {
2176
+ function detectBrokenLinks(entry, registry) {
2177
+ if (!registry) {
2092
2178
  return [];
2093
2179
  }
2094
2180
  const drifts = [];
@@ -2115,13 +2201,13 @@ function detectBrokenLinks(entry, exportRegistry) {
2115
2201
  if (target.includes("/") || target.includes("@")) {
2116
2202
  continue;
2117
2203
  }
2118
- if (!exportRegistry.has(rootName) && !exportRegistry.has(target)) {
2119
- const suggestion = findClosestMatch(rootName, Array.from(exportRegistry));
2204
+ if (!registry.all.has(rootName) && !registry.all.has(target)) {
2205
+ const suggestion = findClosestMatch(rootName, Array.from(registry.all));
2120
2206
  drifts.push({
2121
2207
  type: "broken-link",
2122
2208
  target,
2123
2209
  issue: `{${type} ${target}} references a symbol that does not exist.`,
2124
- suggestion: suggestion && suggestion.distance <= 3 ? `Did you mean "${suggestion.value}"?` : undefined
2210
+ suggestion: suggestion ? `Did you mean "${suggestion.value}"?` : undefined
2125
2211
  });
2126
2212
  }
2127
2213
  }
@@ -8532,8 +8618,8 @@ function findSimilarMember(removedName, newMembers, addedMembers) {
8532
8618
  for (const name of candidates) {
8533
8619
  if (name === removedName)
8534
8620
  continue;
8535
- const removedWords = splitCamelCase(removedName);
8536
- const newWords = splitCamelCase(name);
8621
+ const removedWords = splitCamelCase2(removedName);
8622
+ const newWords = splitCamelCase2(name);
8537
8623
  let matchingWords = 0;
8538
8624
  let suffixMatch = false;
8539
8625
  if (removedWords.length > 0 && newWords.length > 0) {
@@ -8580,7 +8666,7 @@ function levenshteinDistance(a, b) {
8580
8666
  }
8581
8667
  return matrix[b.length][a.length];
8582
8668
  }
8583
- function splitCamelCase(str) {
8669
+ function splitCamelCase2(str) {
8584
8670
  return str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
8585
8671
  }
8586
8672
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/sdk",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "DocCov SDK - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",