@doccov/sdk 0.21.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
@@ -125,6 +125,8 @@ interface DetectedSchema {
125
125
  interface SchemaDetectionResult {
126
126
  schemas: Map<string, DetectedSchema>;
127
127
  errors: string[];
128
+ /** Warning when runtime was requested but compiled JS not found */
129
+ noCompiledJsWarning?: boolean;
128
130
  }
129
131
  declare function detectRuntimeSchemas(context: SchemaDetectionContext): Promise<SchemaDetectionResult>;
130
132
  declare function clearSchemaCache(): void;
@@ -329,9 +331,28 @@ type DriftResult = {
329
331
  exports: Map<string, SpecDocDrift[]>;
330
332
  };
331
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
+ /**
332
353
  * Build a registry of all export/type names for cross-reference validation.
333
354
  */
334
- declare function buildExportRegistry(spec: OpenPkgSpec): Set<string>;
355
+ declare function buildExportRegistry(spec: OpenPkgSpec): ExportRegistry;
335
356
  /**
336
357
  * Compute drift for all exports in a spec.
337
358
  *
@@ -342,10 +363,10 @@ declare function computeDrift(spec: OpenPkgSpec): DriftResult;
342
363
  * Compute drift for a single export.
343
364
  *
344
365
  * @param entry - The to analyze
345
- * @param exportRegistry - Registry of known names for link validation
366
+ * @param registry - Registry of known exports and types for validation
346
367
  * @returns Array of drift issues detected
347
368
  */
348
- declare function computeExportDrift(entry: SpecExport, exportRegistry?: Set<string>): SpecDocDrift[];
369
+ declare function computeExportDrift(entry: SpecExport, registry?: ExportRegistry): SpecDocDrift[];
349
370
  /**
350
371
  * Detect runtime errors in @example blocks.
351
372
  * Results are provided externally after running examples via runExamples().
package/dist/index.js CHANGED
@@ -204,7 +204,8 @@ async function detectRuntimeSchemas(context) {
204
204
  if (!compiledPath) {
205
205
  return {
206
206
  schemas: new Map,
207
- errors: []
207
+ errors: [],
208
+ noCompiledJsWarning: true
208
209
  };
209
210
  }
210
211
  const extraction = await extractStandardSchemasFromProject(entryFile, baseDir);
@@ -646,7 +647,7 @@ function generateReturnTypeFix(drift, exportEntry, existingPatch) {
646
647
  const actualReturn = signature?.returns;
647
648
  if (!actualReturn)
648
649
  return null;
649
- const correctType = actualReturn.tsType ?? stringifySchema(actualReturn.schema);
650
+ const correctType = stringifySchema(actualReturn.schema);
650
651
  const updatedReturn = {
651
652
  ...existingPatch?.returns,
652
653
  type: correctType
@@ -1394,27 +1395,42 @@ var LIBRARY_INTERNAL_PATTERNS = [
1394
1395
 
1395
1396
  // src/analysis/docs-coverage.ts
1396
1397
  function buildExportRegistry(spec) {
1397
- const registry = new Set;
1398
+ const exports = new Map;
1399
+ const types = new Set;
1400
+ const all = new Set;
1398
1401
  for (const entry of spec.exports ?? []) {
1399
- registry.add(entry.name);
1400
- 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);
1401
1413
  }
1402
1414
  for (const type of spec.types ?? []) {
1403
- registry.add(type.name);
1404
- 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);
1405
1421
  }
1406
- return registry;
1422
+ return { exports, types, all };
1407
1423
  }
1408
1424
  function computeDrift(spec) {
1409
- const exportRegistry = buildExportRegistry(spec);
1425
+ const registry = buildExportRegistry(spec);
1410
1426
  const exports = new Map;
1411
1427
  for (const entry of spec.exports ?? []) {
1412
- const drift = computeExportDrift(entry, exportRegistry);
1428
+ const drift = computeExportDrift(entry, registry);
1413
1429
  exports.set(entry.id ?? entry.name, drift);
1414
1430
  }
1415
1431
  return { exports };
1416
1432
  }
1417
- function computeExportDrift(entry, exportRegistry) {
1433
+ function computeExportDrift(entry, registry) {
1418
1434
  return [
1419
1435
  ...detectParamDrift(entry),
1420
1436
  ...detectOptionalityDrift(entry),
@@ -1423,8 +1439,8 @@ function computeExportDrift(entry, exportRegistry) {
1423
1439
  ...detectGenericConstraintDrift(entry),
1424
1440
  ...detectDeprecatedDrift(entry),
1425
1441
  ...detectVisibilityDrift(entry),
1426
- ...detectExampleDrift(entry, exportRegistry),
1427
- ...detectBrokenLinks(entry, exportRegistry),
1442
+ ...detectExampleDrift(entry, registry),
1443
+ ...detectBrokenLinks(entry, registry),
1428
1444
  ...detectExampleSyntaxErrors(entry),
1429
1445
  ...detectAsyncMismatch(entry),
1430
1446
  ...detectPropertyTypeDrift(entry)
@@ -1471,24 +1487,39 @@ function detectParamDrift(entry) {
1471
1487
  if (properties.has(firstProperty)) {
1472
1488
  continue;
1473
1489
  }
1474
- 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
+ }
1475
1499
  drifts.push({
1476
1500
  type: "param-mismatch",
1477
1501
  target: documentedName,
1478
1502
  issue: `JSDoc documents property "${propertyPath}" on parameter "${prefix}" which does not exist.`,
1479
- suggestion: suggestion2?.distance !== undefined && suggestion2.distance <= 3 ? `${prefix}.${suggestion2.value}` : undefined
1503
+ suggestion: suggestionText2
1480
1504
  });
1481
1505
  continue;
1482
1506
  }
1483
1507
  continue;
1484
1508
  }
1485
1509
  }
1486
- 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
+ }
1487
1518
  drifts.push({
1488
1519
  type: "param-mismatch",
1489
1520
  target: documentedName,
1490
1521
  issue: `JSDoc documents parameter "${documentedName}" which is not present in the signature.`,
1491
- suggestion: suggestion?.distance !== undefined && suggestion.distance <= 3 ? suggestion.value : undefined
1522
+ suggestion: suggestionText
1492
1523
  });
1493
1524
  }
1494
1525
  return drifts;
@@ -1596,7 +1627,7 @@ function detectReturnTypeDrift(entry) {
1596
1627
  if (!signatureReturn) {
1597
1628
  return [];
1598
1629
  }
1599
- const declaredRaw = signatureReturn.tsType ?? extractTypeFromSchema(signatureReturn.schema);
1630
+ const declaredRaw = extractTypeFromSchema(signatureReturn.schema);
1600
1631
  const declaredType = normalizeType(declaredRaw) ?? undefined;
1601
1632
  if (!declaredType) {
1602
1633
  return [];
@@ -1959,26 +1990,53 @@ function buildGenericConstraintSuggestion(templateName, actualConstraint) {
1959
1990
  }
1960
1991
  return `Remove the constraint from @template ${templateName} to match the declaration.`;
1961
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
+ }
1962
1996
  function findClosestMatch(source, candidates) {
1963
1997
  if (candidates.length === 0) {
1964
1998
  return;
1965
1999
  }
1966
- const normalizedSource = source.toLowerCase();
1967
- const substringCandidate = candidates.find((candidate) => {
1968
- const normalizedCandidate = candidate.toLowerCase();
1969
- return normalizedCandidate.includes(normalizedSource) || normalizedSource.includes(normalizedCandidate);
1970
- });
1971
- if (substringCandidate && substringCandidate !== source) {
1972
- return { value: substringCandidate, distance: 0 };
1973
- }
1974
- let best;
2000
+ const sourceWords = splitCamelCase(source);
2001
+ let bestMatch;
2002
+ let bestScore = 0;
1975
2003
  for (const candidate of candidates) {
1976
- const distance = levenshtein(source, candidate);
1977
- if (!best || distance < best.distance) {
1978
- 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;
1979
2033
  }
1980
2034
  }
1981
- return best;
2035
+ if (!bestMatch) {
2036
+ return;
2037
+ }
2038
+ const distance = Math.round((1 - bestScore) * 10);
2039
+ return { value: bestMatch, distance };
1982
2040
  }
1983
2041
  function levenshtein(a, b) {
1984
2042
  if (a === b) {
@@ -2008,8 +2066,22 @@ function levenshtein(a, b) {
2008
2066
  }
2009
2067
  return matrix[b.length][a.length];
2010
2068
  }
2011
- function detectExampleDrift(entry, exportRegistry) {
2012
- 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)
2013
2085
  return [];
2014
2086
  const drifts = [];
2015
2087
  for (const example of entry.examples) {
@@ -2023,7 +2095,11 @@ function detectExampleDrift(entry, exportRegistry) {
2023
2095
  if (isLocalDeclaration(node)) {
2024
2096
  localDeclarations.add(text);
2025
2097
  } else if (isIdentifierReference(node) && !isBuiltInIdentifier(text)) {
2026
- 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
+ }
2027
2103
  }
2028
2104
  }
2029
2105
  ts2.forEachChild(node, visit);
@@ -2035,16 +2111,27 @@ function detectExampleDrift(entry, exportRegistry) {
2035
2111
  continue;
2036
2112
  const sourceFile = ts2.createSourceFile("example.ts", codeContent, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
2037
2113
  const localDeclarations = new Set;
2038
- const referencedIdentifiers = new Set;
2114
+ const referencedIdentifiers = new Map;
2039
2115
  visit(sourceFile);
2040
2116
  for (const local of localDeclarations) {
2041
2117
  referencedIdentifiers.delete(local);
2042
2118
  }
2043
- for (const identifier of referencedIdentifiers) {
2044
- if (!exportRegistry.has(identifier)) {
2045
- 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);
2046
2133
  const isPascal = /^[A-Z]/.test(identifier);
2047
- const hasCloseMatch = suggestion && suggestion.distance <= 3;
2134
+ const hasCloseMatch = suggestion && suggestion.distance <= 5;
2048
2135
  if (hasCloseMatch || isPascal) {
2049
2136
  drifts.push({
2050
2137
  type: "example-drift",
@@ -2086,8 +2173,8 @@ function isIdentifierReference(node) {
2086
2173
  return false;
2087
2174
  return true;
2088
2175
  }
2089
- function detectBrokenLinks(entry, exportRegistry) {
2090
- if (!exportRegistry) {
2176
+ function detectBrokenLinks(entry, registry) {
2177
+ if (!registry) {
2091
2178
  return [];
2092
2179
  }
2093
2180
  const drifts = [];
@@ -2114,13 +2201,13 @@ function detectBrokenLinks(entry, exportRegistry) {
2114
2201
  if (target.includes("/") || target.includes("@")) {
2115
2202
  continue;
2116
2203
  }
2117
- if (!exportRegistry.has(rootName) && !exportRegistry.has(target)) {
2118
- 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));
2119
2206
  drifts.push({
2120
2207
  type: "broken-link",
2121
2208
  target,
2122
2209
  issue: `{${type} ${target}} references a symbol that does not exist.`,
2123
- suggestion: suggestion && suggestion.distance <= 3 ? `Did you mean "${suggestion.value}"?` : undefined
2210
+ suggestion: suggestion ? `Did you mean "${suggestion.value}"?` : undefined
2124
2211
  });
2125
2212
  }
2126
2213
  }
@@ -2198,7 +2285,7 @@ function detectAsyncMismatch(entry) {
2198
2285
  }
2199
2286
  const drifts = [];
2200
2287
  const returnsPromise = signatures.some((sig) => {
2201
- const returnType = sig.returns?.tsType ?? extractTypeFromSchema(sig.returns?.schema) ?? "";
2288
+ const returnType = extractTypeFromSchema(sig.returns?.schema) ?? "";
2202
2289
  return returnType.startsWith("Promise<") || returnType === "Promise";
2203
2290
  });
2204
2291
  const returnsTag = entry.tags?.find((tag) => tag.name === "returns" || tag.name === "return");
@@ -5161,11 +5248,11 @@ function formatTypeReference(type, typeChecker, typeRefs, referencedTypes, visit
5161
5248
  if (type.getFlags() & ts.TypeFlags.Object) {
5162
5249
  const objectType = type;
5163
5250
  if (objectType.objectFlags & ts.ObjectFlags.Mapped) {
5164
- return { type: "object", tsType: typeString };
5251
+ return { type: "object" };
5165
5252
  }
5166
5253
  }
5167
5254
  if (type.flags & ts.TypeFlags.Conditional) {
5168
- return { type: "object", tsType: typeString };
5255
+ return { type: "object" };
5169
5256
  }
5170
5257
  if (type.isUnion()) {
5171
5258
  const unionType = type;
@@ -6419,7 +6506,6 @@ function serializeCallSignatures(signatures, symbol, context, parsedDoc) {
6419
6506
  };
6420
6507
  });
6421
6508
  const returnType = signature.getReturnType();
6422
- const returnTypeText = returnType ? checker.typeToString(returnType) : undefined;
6423
6509
  if (returnType) {
6424
6510
  collectReferencedTypes(returnType, checker, referencedTypes);
6425
6511
  }
@@ -6445,7 +6531,6 @@ function serializeCallSignatures(signatures, symbol, context, parsedDoc) {
6445
6531
  returns: {
6446
6532
  schema: returnType ? formatTypeReference(returnType, checker, typeRefs, referencedTypes) : { type: "void" },
6447
6533
  description: functionDoc?.returns || "",
6448
- tsType: returnTypeText,
6449
6534
  ...typePredicateInfo ? { typePredicate: typePredicateInfo } : {}
6450
6535
  },
6451
6536
  description: functionDoc?.description || undefined,
@@ -8490,13 +8575,6 @@ function extractTypeName(schema) {
8490
8575
  if (typeof s.type === "string") {
8491
8576
  return s.type;
8492
8577
  }
8493
- if (typeof s.tsType === "string") {
8494
- const tsType = s.tsType;
8495
- if (tsType.length > 30) {
8496
- return `${tsType.slice(0, 27)}...`;
8497
- }
8498
- return tsType;
8499
- }
8500
8578
  return;
8501
8579
  }
8502
8580
  function hasSignatureChanged(oldMember, newMember) {
@@ -8540,8 +8618,8 @@ function findSimilarMember(removedName, newMembers, addedMembers) {
8540
8618
  for (const name of candidates) {
8541
8619
  if (name === removedName)
8542
8620
  continue;
8543
- const removedWords = splitCamelCase(removedName);
8544
- const newWords = splitCamelCase(name);
8621
+ const removedWords = splitCamelCase2(removedName);
8622
+ const newWords = splitCamelCase2(name);
8545
8623
  let matchingWords = 0;
8546
8624
  let suffixMatch = false;
8547
8625
  if (removedWords.length > 0 && newWords.length > 0) {
@@ -8588,7 +8666,7 @@ function levenshteinDistance(a, b) {
8588
8666
  }
8589
8667
  return matrix[b.length][a.length];
8590
8668
  }
8591
- function splitCamelCase(str) {
8669
+ function splitCamelCase2(str) {
8592
8670
  return str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
8593
8671
  }
8594
8672
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/sdk",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
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.10.0",
42
+ "@openpkg-ts/spec": "^0.11.0",
43
43
  "@vercel/sandbox": "^1.0.3",
44
44
  "mdast": "^3.0.0",
45
45
  "minimatch": "^10.1.1",