@doccov/sdk 0.30.7 → 0.31.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.
@@ -1452,6 +1452,8 @@ function detectAllExampleIssues(entry, registry) {
1452
1452
  type: "example-syntax-error",
1453
1453
  target: `example[${i}]`,
1454
1454
  issue: `@example contains invalid syntax: ${message}`,
1455
+ expected: "valid syntax",
1456
+ actual: message,
1455
1457
  suggestion: "Check for missing brackets, semicolons, or typos."
1456
1458
  });
1457
1459
  continue;
@@ -1482,6 +1484,8 @@ function detectAllExampleIssues(entry, registry) {
1482
1484
  type: "example-drift",
1483
1485
  target: identifier,
1484
1486
  issue: `@example references "${identifier}" which does not exist in this package.`,
1487
+ expected: identifier,
1488
+ actual: hasCloseMatch ? suggestion.value : undefined,
1485
1489
  suggestion: hasCloseMatch ? `Did you mean "${suggestion.value}"?` : undefined
1486
1490
  });
1487
1491
  }
@@ -1506,6 +1510,8 @@ function detectExampleRuntimeErrors(entry, runtimeResults) {
1506
1510
  type: "example-runtime-error",
1507
1511
  target: `example[${i}]`,
1508
1512
  issue: isTimeout ? `@example timed out after ${result.duration}ms.` : `@example throws at runtime: ${errorMessage}`,
1513
+ expected: "successful execution",
1514
+ actual: errorMessage,
1509
1515
  suggestion: isTimeout ? "Check for infinite loops or long-running operations." : "Fix the example code or update it to match the current API."
1510
1516
  });
1511
1517
  }
@@ -1571,6 +1577,8 @@ function detectExampleAssertionFailures(entry, runtimeResults) {
1571
1577
  type: "example-assertion-failed",
1572
1578
  target: `example[${i}]:line${assertion.lineNumber}`,
1573
1579
  issue: `Assertion expected "${assertion.expected}" but no output was produced`,
1580
+ expected: assertion.expected,
1581
+ actual: "(no output)",
1574
1582
  suggestion: "Ensure the example produces output for each assertion"
1575
1583
  });
1576
1584
  continue;
@@ -1580,6 +1588,8 @@ function detectExampleAssertionFailures(entry, runtimeResults) {
1580
1588
  type: "example-assertion-failed",
1581
1589
  target: `example[${i}]:line${assertion.lineNumber}`,
1582
1590
  issue: `Assertion failed: expected "${assertion.expected}" but got "${actual}"`,
1591
+ expected: assertion.expected,
1592
+ actual,
1583
1593
  suggestion: `Update assertion to: // => ${actual}`
1584
1594
  });
1585
1595
  }
@@ -1643,6 +1653,8 @@ function detectParamDrift(entry) {
1643
1653
  type: "param-mismatch",
1644
1654
  target: documentedName,
1645
1655
  issue: `JSDoc documents property "${propertyPath}" on parameter "${prefix}" which does not exist.`,
1656
+ expected: documentedName,
1657
+ actual: propsArray.length > 0 ? propsArray.map((p) => `${prefix}.${p}`).join(", ") : undefined,
1646
1658
  suggestion: suggestionText2
1647
1659
  });
1648
1660
  continue;
@@ -1662,6 +1674,8 @@ function detectParamDrift(entry) {
1662
1674
  type: "param-mismatch",
1663
1675
  target: documentedName,
1664
1676
  issue: `JSDoc documents parameter "${documentedName}" which is not present in the signature.`,
1677
+ expected: documentedName,
1678
+ actual: paramsArray.join(", ") || undefined,
1665
1679
  suggestion: suggestionText
1666
1680
  });
1667
1681
  }
@@ -1704,6 +1718,8 @@ function detectOptionalityDrift(entry) {
1704
1718
  type: "optionality-mismatch",
1705
1719
  target: docParam.name,
1706
1720
  issue,
1721
+ expected: documentedOptional ? `[${docParam.name}]` : docParam.name,
1722
+ actual: actualOptional ? "optional" : "required",
1707
1723
  suggestion
1708
1724
  });
1709
1725
  }
@@ -1751,6 +1767,8 @@ function detectParamTypeDrift(entry) {
1751
1767
  type: "param-type-mismatch",
1752
1768
  target: documentedParam.name,
1753
1769
  issue: buildParamTypeMismatchIssue(documentedParam.name, documentedParam.type, declaredType),
1770
+ expected: documentedParam.type,
1771
+ actual: declaredType,
1754
1772
  suggestion: `Update @param {${declaredType}} ${documentedParam.name} to match the signature.`
1755
1773
  });
1756
1774
  }
@@ -1771,6 +1789,8 @@ function detectDeprecatedDrift(entry) {
1771
1789
  type: "deprecated-mismatch",
1772
1790
  target,
1773
1791
  issue: `Declaration for "${target}" is marked deprecated but @deprecated is missing from the docs.`,
1792
+ expected: "not deprecated",
1793
+ actual: "deprecated",
1774
1794
  suggestion: "Add an @deprecated tag explaining the replacement or removal timeline."
1775
1795
  }
1776
1796
  ];
@@ -1780,6 +1800,8 @@ function detectDeprecatedDrift(entry) {
1780
1800
  type: "deprecated-mismatch",
1781
1801
  target,
1782
1802
  issue: `JSDoc marks "${target}" as deprecated but the TypeScript declaration is not.`,
1803
+ expected: "@deprecated",
1804
+ actual: "not deprecated",
1783
1805
  suggestion: "Remove the @deprecated tag or deprecate the declaration."
1784
1806
  }
1785
1807
  ];
@@ -1801,6 +1823,8 @@ function detectVisibilityDrift(entry) {
1801
1823
  type: "visibility-mismatch",
1802
1824
  target,
1803
1825
  issue: buildVisibilityIssue(target, exportDocVisibility, exportActualVisibility),
1826
+ expected: formatDocVisibilityTag(exportDocVisibility.tagName),
1827
+ actual: exportActualVisibility,
1804
1828
  suggestion: buildVisibilitySuggestion(exportDocVisibility, exportActualVisibility)
1805
1829
  });
1806
1830
  }
@@ -1821,6 +1845,8 @@ function detectVisibilityDrift(entry) {
1821
1845
  type: "visibility-mismatch",
1822
1846
  target: qualifiedTarget,
1823
1847
  issue: buildVisibilityIssue(qualifiedTarget, memberDocVisibility, memberActualVisibility),
1848
+ expected: formatDocVisibilityTag(memberDocVisibility.tagName),
1849
+ actual: memberActualVisibility,
1824
1850
  suggestion: buildVisibilitySuggestion(memberDocVisibility, memberActualVisibility)
1825
1851
  });
1826
1852
  }
@@ -1886,11 +1912,12 @@ function formatDocVisibilityTag(tagName) {
1886
1912
  }
1887
1913
  return trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
1888
1914
  }
1889
- function detectBrokenLinks(entry, registry) {
1915
+ function detectBrokenLinks(entry, registry, options) {
1890
1916
  if (!registry) {
1891
1917
  return [];
1892
1918
  }
1893
1919
  const drifts = [];
1920
+ const { moduleGraph } = options ?? {};
1894
1921
  const patterns = [
1895
1922
  { pattern: /\{@link\s+([^}\s|]+)(?:\s*\|[^}]*)?\}/g, type: "@link" },
1896
1923
  { pattern: /\{@see\s+([^}\s]+)\}/g, type: "@see" },
@@ -1914,15 +1941,21 @@ function detectBrokenLinks(entry, registry) {
1914
1941
  if (target.includes("/") || target.includes("@")) {
1915
1942
  continue;
1916
1943
  }
1917
- if (!registry.all.has(rootName) && !registry.all.has(target)) {
1918
- const suggestion = findClosestMatch(rootName, registry.allNames);
1919
- drifts.push({
1920
- type: "broken-link",
1921
- target,
1922
- issue: `{${type} ${target}} references a symbol that does not exist.`,
1923
- suggestion: suggestion ? `Did you mean "${suggestion.value}"?` : undefined
1924
- });
1944
+ if (registry.all.has(rootName) || registry.all.has(target)) {
1945
+ continue;
1946
+ }
1947
+ if (moduleGraph?.all.has(rootName) || moduleGraph?.all.has(target)) {
1948
+ continue;
1925
1949
  }
1950
+ const suggestion = findClosestMatch(rootName, registry.allNames);
1951
+ drifts.push({
1952
+ type: "broken-link",
1953
+ target,
1954
+ issue: `{${type} ${target}} references a symbol that does not exist.`,
1955
+ expected: target,
1956
+ actual: suggestion?.value,
1957
+ suggestion: suggestion ? `Did you mean "${suggestion.value}"?` : undefined
1958
+ });
1926
1959
  }
1927
1960
  }
1928
1961
  return drifts;
@@ -1946,6 +1979,8 @@ function detectAsyncMismatch(entry) {
1946
1979
  type: "async-mismatch",
1947
1980
  target: "returns",
1948
1981
  issue: "Function returns Promise but documentation does not indicate async behavior.",
1982
+ expected: "sync",
1983
+ actual: "Promise",
1949
1984
  suggestion: "Add @async tag or document @returns {Promise<...>}."
1950
1985
  });
1951
1986
  }
@@ -1954,6 +1989,8 @@ function detectAsyncMismatch(entry) {
1954
1989
  type: "async-mismatch",
1955
1990
  target: "returns",
1956
1991
  issue: "Documentation indicates async but function does not return Promise.",
1992
+ expected: "@async / Promise",
1993
+ actual: "sync",
1957
1994
  suggestion: "Remove @async tag or update @returns type."
1958
1995
  });
1959
1996
  }
@@ -1992,6 +2029,8 @@ function detectReturnTypeDrift(entry) {
1992
2029
  type: "return-type-mismatch",
1993
2030
  target: "returns",
1994
2031
  issue: buildReturnTypeMismatchIssue(documentedType, documentedNormalized, declaredType),
2032
+ expected: documentedType,
2033
+ actual: declaredType,
1995
2034
  suggestion: `Update @returns to ${declaredType}.`
1996
2035
  }
1997
2036
  ];
@@ -2027,6 +2066,8 @@ function detectGenericConstraintDrift(entry) {
2027
2066
  type: "generic-constraint-mismatch",
2028
2067
  target: doc.name,
2029
2068
  issue: buildGenericConstraintMismatchIssue(doc.name, doc.constraint, actualConstraint),
2069
+ expected: doc.constraint ?? "none",
2070
+ actual: actualConstraint ?? "none",
2030
2071
  suggestion: buildGenericConstraintSuggestion(doc.name, actualConstraint)
2031
2072
  });
2032
2073
  }
@@ -2061,6 +2102,8 @@ function detectPropertyTypeDrift(entry) {
2061
2102
  type: "property-type-drift",
2062
2103
  target: memberName,
2063
2104
  issue: `Property "${memberName}" documented as {${documentedType}} but actual type is ${actualType}.`,
2105
+ expected: documentedType,
2106
+ actual: actualType,
2064
2107
  suggestion: `Update @type {${actualType}} to match the declaration.`
2065
2108
  });
2066
2109
  }
@@ -2117,16 +2160,16 @@ function buildExportRegistry(spec) {
2117
2160
  const allNames = Array.from(all);
2118
2161
  return { exports, types, all, callableNames, typeNames, allExportNames, allNames };
2119
2162
  }
2120
- function computeDrift(spec) {
2163
+ function computeDrift(spec, options) {
2121
2164
  const registry = buildExportRegistry(spec);
2122
2165
  const exports = new Map;
2123
2166
  for (const entry of spec.exports ?? []) {
2124
- const drift = computeExportDrift(entry, registry);
2167
+ const drift = computeExportDrift(entry, registry, options);
2125
2168
  exports.set(entry.id ?? entry.name, drift);
2126
2169
  }
2127
2170
  return { exports };
2128
2171
  }
2129
- function computeExportDrift(entry, registry) {
2172
+ function computeExportDrift(entry, registry, options) {
2130
2173
  const hasDescription = Boolean(entry.description);
2131
2174
  const hasTags = (entry.tags?.length ?? 0) > 0;
2132
2175
  const hasExamples = (entry.examples?.length ?? 0) > 0;
@@ -2141,12 +2184,20 @@ function computeExportDrift(entry, registry) {
2141
2184
  drifts.push(...detectAllExampleIssues(entry, registry));
2142
2185
  }
2143
2186
  if (hasDescription || hasTags) {
2144
- drifts.push(...detectBrokenLinks(entry, registry));
2187
+ drifts.push(...detectBrokenLinks(entry, registry, { moduleGraph: options?.moduleGraph }));
2145
2188
  }
2146
2189
  return drifts;
2147
2190
  }
2148
2191
 
2149
2192
  // src/analysis/health.ts
2193
+ function isExportDocumented(exp) {
2194
+ if (exp.description && exp.description.trim().length > 0)
2195
+ return true;
2196
+ const meaningfulTags = exp.tags?.filter((t) => t.name !== "internal") ?? [];
2197
+ if (meaningfulTags.length > 0)
2198
+ return true;
2199
+ return false;
2200
+ }
2150
2201
  function computeHealth(input) {
2151
2202
  const {
2152
2203
  coverageScore,
@@ -2197,11 +2248,92 @@ function computeHealth(input) {
2197
2248
  return result;
2198
2249
  }
2199
2250
 
2251
+ // src/analysis/presets.ts
2252
+ var DEFAULT_REQUIREMENTS = {
2253
+ description: true,
2254
+ params: false,
2255
+ returns: false,
2256
+ examples: false,
2257
+ since: false
2258
+ };
2259
+ var PRESETS = {
2260
+ minimal: {
2261
+ description: true,
2262
+ params: false,
2263
+ returns: false,
2264
+ examples: false,
2265
+ since: false
2266
+ },
2267
+ verbose: {
2268
+ description: true,
2269
+ params: true,
2270
+ returns: true,
2271
+ examples: false,
2272
+ since: false
2273
+ },
2274
+ "types-only": {
2275
+ description: false,
2276
+ params: false,
2277
+ returns: false,
2278
+ examples: false,
2279
+ since: false
2280
+ }
2281
+ };
2282
+ function resolveRequirements(style, require2) {
2283
+ const base = style ? PRESETS[style] : DEFAULT_REQUIREMENTS;
2284
+ if (require2) {
2285
+ return {
2286
+ description: require2.description ?? base.description,
2287
+ params: require2.params ?? base.params,
2288
+ returns: require2.returns ?? base.returns,
2289
+ examples: require2.examples ?? base.examples,
2290
+ since: require2.since ?? base.since
2291
+ };
2292
+ }
2293
+ return { ...base };
2294
+ }
2295
+
2200
2296
  // src/analysis/doccov-builder.ts
2201
2297
  import { DRIFT_CATEGORIES } from "@doccov/spec";
2202
- function buildDocCovSpec(options) {
2203
- const { openpkg, openpkgPath, forgottenExports, apiSurfaceIgnore } = options;
2298
+ function hasInternalTag(exp) {
2299
+ return exp.tags?.some((t) => t.name === "internal") ?? false;
2300
+ }
2301
+ var YIELD_BATCH_SIZE = 5;
2302
+ async function buildDocCovSpec(options) {
2303
+ const {
2304
+ openpkg,
2305
+ openpkgPath,
2306
+ forgottenExports,
2307
+ apiSurfaceIgnore,
2308
+ entryExportNames,
2309
+ onProgress,
2310
+ onExportAnalyzed,
2311
+ style,
2312
+ require: require2
2313
+ } = options;
2204
2314
  const registry = buildExportRegistry(openpkg);
2315
+ const requirements = resolveRequirements(style, require2);
2316
+ const allExports = (openpkg.exports ?? []).filter((exp) => !hasInternalTag(exp));
2317
+ const total = allExports.length;
2318
+ const analysisResults = [];
2319
+ for (let i = 0;i < total; i++) {
2320
+ const exp = allExports[i];
2321
+ onProgress?.(i + 1, total, exp.name);
2322
+ if (i % YIELD_BATCH_SIZE === 0 && i > 0) {
2323
+ await new Promise((r) => setImmediate(r));
2324
+ }
2325
+ const coverage = computeExportCoverage(exp, requirements);
2326
+ const rawDrifts = computeExportDrift(exp, registry);
2327
+ const categorizedDrifts = rawDrifts.map((d) => toCategorizedDrift(d));
2328
+ analysisResults.push({ coverage, drifts: categorizedDrifts, exp });
2329
+ }
2330
+ const byName = new Map;
2331
+ for (const result of analysisResults) {
2332
+ const name = result.exp.name;
2333
+ const existing = byName.get(name) ?? [];
2334
+ existing.push(result);
2335
+ byName.set(name, existing);
2336
+ }
2205
2337
  const exports = {};
2206
2338
  let totalScore = 0;
2207
2339
  let documentedCount = 0;
@@ -2219,30 +2351,46 @@ function buildDocCovSpec(options) {
2219
2351
  };
2220
2352
  let totalDrift = 0;
2221
2353
  let fixableDrift = 0;
2222
- for (const exp of openpkg.exports ?? []) {
2223
- const coverage = computeExportCoverage(exp);
2224
- const rawDrifts = computeExportDrift(exp, registry);
2225
- const categorizedDrifts = rawDrifts.map((d) => toCategorizedDrift(d));
2226
- const exportId = exp.id ?? exp.name;
2227
- exports[exportId] = {
2228
- coverageScore: coverage.score,
2229
- missing: coverage.missing.length > 0 ? coverage.missing : undefined,
2230
- drift: categorizedDrifts.length > 0 ? categorizedDrifts : undefined
2354
+ for (const [name, overloads] of byName) {
2355
+ const bestResult = overloads.reduce((best, curr) => curr.coverage.score > best.coverage.score ? curr : best);
2356
+ const exportId = bestResult.exp.id ?? name;
2357
+ const allMissing = new Set;
2358
+ for (const r of overloads) {
2359
+ for (const m of r.coverage.missing) {
2360
+ allMissing.add(m);
2361
+ }
2362
+ }
2363
+ const allDrifts = [];
2364
+ for (const r of overloads) {
2365
+ allDrifts.push(...r.drifts);
2366
+ }
2367
+ const analysis = {
2368
+ coverageScore: bestResult.coverage.score,
2369
+ missing: allMissing.size > 0 ? Array.from(allMissing) : undefined,
2370
+ drift: allDrifts.length > 0 ? allDrifts : undefined
2231
2371
  };
2232
- totalScore += coverage.score;
2233
- if (coverage.score === 100)
2372
+ if (overloads.length > 1) {
2373
+ analysis.overloadCount = overloads.length;
2374
+ }
2375
+ exports[exportId] = analysis;
2376
+ if (onExportAnalyzed) {
2377
+ const idx = Object.keys(exports).length;
2378
+ await onExportAnalyzed(exportId, name, analysis, idx, byName.size);
2379
+ }
2380
+ totalScore += bestResult.coverage.score;
2381
+ if (isExportDocumented(bestResult.exp))
2234
2382
  documentedCount++;
2235
- for (const rule of coverage.missing) {
2383
+ for (const rule of bestResult.coverage.missing) {
2236
2384
  missingByRule[rule]++;
2237
2385
  }
2238
- for (const d of categorizedDrifts) {
2386
+ for (const d of allDrifts) {
2239
2387
  driftByCategory[d.category]++;
2240
2388
  totalDrift++;
2241
2389
  if (d.fixable)
2242
2390
  fixableDrift++;
2243
2391
  }
2244
2392
  }
2245
- const exportCount = openpkg.exports?.length ?? 0;
2393
+ const exportCount = byName.size;
2246
2394
  const coverageScore = exportCount > 0 ? Math.round(totalScore / exportCount) : 100;
2247
2395
  const health = computeHealth({
2248
2396
  coverageScore,
@@ -2265,7 +2413,7 @@ function buildDocCovSpec(options) {
2265
2413
  },
2266
2414
  health
2267
2415
  };
2268
- const apiSurface = computeApiSurface(forgottenExports, openpkg.types?.length ?? 0, apiSurfaceIgnore);
2416
+ const apiSurface = computeApiSurface(forgottenExports, openpkg.types?.length ?? 0, apiSurfaceIgnore, entryExportNames);
2269
2417
  return {
2270
2418
  doccov: "1.0.0",
2271
2419
  source: {
@@ -2280,11 +2428,12 @@ function buildDocCovSpec(options) {
2280
2428
  ...apiSurface ? { apiSurface } : {}
2281
2429
  };
2282
2430
  }
2283
- function computeApiSurface(forgottenExports, exportedTypesCount, ignoreList) {
2431
+ function computeApiSurface(forgottenExports, exportedTypesCount, ignoreList, entryExportNames) {
2284
2432
  if (!forgottenExports)
2285
2433
  return;
2286
2434
  const ignoreSet = new Set(ignoreList ?? []);
2287
- const filteredExports = forgottenExports.filter((f) => !ignoreSet.has(f.name));
2435
+ const entryExportSet = new Set(entryExportNames ?? []);
2436
+ const filteredExports = forgottenExports.filter((f) => !ignoreSet.has(f.name) && !entryExportSet.has(f.name));
2288
2437
  const forgotten = filteredExports.map((f) => ({
2289
2438
  name: f.name,
2290
2439
  definedIn: f.definedIn ? { file: f.definedIn } : undefined,
@@ -2305,36 +2454,52 @@ function computeApiSurface(forgottenExports, exportedTypesCount, ignoreList) {
2305
2454
  completeness
2306
2455
  };
2307
2456
  }
2308
- function computeExportCoverage(exp) {
2457
+ function computeExportCoverage(exp, requirements) {
2309
2458
  const missing = [];
2310
2459
  let points = 0;
2311
2460
  let maxPoints = 0;
2312
- maxPoints += 30;
2313
- if (exp.description && exp.description.trim().length > 0) {
2314
- points += 30;
2461
+ const descRequired = requirements.description;
2462
+ if (descRequired) {
2463
+ maxPoints += 30;
2464
+ if (exp.description && exp.description.trim().length > 0) {
2465
+ points += 30;
2466
+ } else {
2467
+ missing.push("description");
2468
+ }
2315
2469
  } else {
2316
- missing.push("description");
2470
+ if (!exp.description || exp.description.trim().length === 0) {
2471
+ missing.push("description");
2472
+ }
2317
2473
  }
2318
2474
  const isCallable = exp.kind === "function" || exp.kind === "class";
2319
2475
  if (isCallable && exp.signatures?.length) {
2320
2476
  const sig = exp.signatures[0];
2321
2477
  const params = sig.parameters ?? [];
2322
2478
  if (params.length > 0) {
2323
- maxPoints += 25;
2479
+ const paramsRequired = requirements.params;
2480
+ if (paramsRequired) {
2481
+ maxPoints += 25;
2482
+ }
2324
2483
  const documentedParams = params.filter((p) => p.description && p.description.trim().length > 0);
2325
2484
  if (documentedParams.length === params.length) {
2326
- points += 25;
2485
+ if (paramsRequired)
2486
+ points += 25;
2327
2487
  } else if (documentedParams.length > 0) {
2328
- points += Math.round(documentedParams.length / params.length * 25);
2488
+ if (paramsRequired)
2489
+ points += Math.round(documentedParams.length / params.length * 25);
2329
2490
  missing.push("params");
2330
2491
  } else {
2331
2492
  missing.push("params");
2332
2493
  }
2333
2494
  }
2334
2495
  if (exp.kind === "function" && sig.returns) {
2335
- maxPoints += 20;
2496
+ const returnsRequired = requirements.returns;
2497
+ if (returnsRequired) {
2498
+ maxPoints += 20;
2499
+ }
2336
2500
  if (sig.returns.description && sig.returns.description.trim().length > 0) {
2337
- points += 20;
2501
+ if (returnsRequired)
2502
+ points += 20;
2338
2503
  } else {
2339
2504
  missing.push("returns");
2340
2505
  }
@@ -2349,9 +2514,13 @@ function computeExportCoverage(exp) {
2349
2514
  }
2350
2515
  }
2351
2516
  }
2352
- maxPoints += 15;
2517
+ const examplesRequired = requirements.examples;
2518
+ if (examplesRequired) {
2519
+ maxPoints += 15;
2520
+ }
2353
2521
  if (exp.examples && exp.examples.length > 0) {
2354
- points += 15;
2522
+ if (examplesRequired)
2523
+ points += 15;
2355
2524
  } else {
2356
2525
  missing.push("examples");
2357
2526
  }
@@ -2507,35 +2676,45 @@ function isExportFullyDocumented(exp, doccov) {
2507
2676
  // src/analysis/report.ts
2508
2677
  import * as fs3 from "node:fs";
2509
2678
  import * as path3 from "node:path";
2510
- function generateReport(spec, openpkgPath = "openpkg.json") {
2511
- const doccov = buildDocCovSpec({ openpkg: spec, openpkgPath });
2679
+ async function generateReport(spec, openpkgPath = "openpkg.json") {
2680
+ const doccov = await buildDocCovSpec({ openpkg: spec, openpkgPath });
2512
2681
  return generateReportFromDocCov(spec, doccov);
2513
2682
  }
2514
2683
  function generateReportFromDocCov(openpkg, doccov) {
2515
2684
  const exportsData = {};
2516
2685
  const missingByRule = {};
2686
+ const openpkgExportsById = new Map;
2687
+ for (const exp of openpkg.exports ?? []) {
2688
+ const id = exp.id ?? exp.name;
2689
+ if (!openpkgExportsById.has(id)) {
2690
+ openpkgExportsById.set(id, exp);
2691
+ }
2692
+ }
2517
2693
  let documentedExports = 0;
2518
2694
  let totalDrift = 0;
2519
- for (const exp of openpkg.exports ?? []) {
2520
- const analysis = getExportAnalysis(exp, doccov);
2695
+ for (const [exportId, analysis] of Object.entries(doccov.exports)) {
2696
+ const openpkgExp = openpkgExportsById.get(exportId);
2521
2697
  const data = {
2522
- name: exp.name,
2523
- kind: exp.kind,
2524
- coverageScore: analysis?.coverageScore ?? 100
2698
+ name: openpkgExp?.name ?? exportId,
2699
+ kind: openpkgExp?.kind ?? "unknown",
2700
+ coverageScore: analysis.coverageScore
2525
2701
  };
2526
- if (analysis?.missing && analysis.missing.length > 0) {
2702
+ if (analysis.missing && analysis.missing.length > 0) {
2527
2703
  data.missing = analysis.missing;
2528
2704
  for (const ruleId of analysis.missing) {
2529
2705
  missingByRule[ruleId] = (missingByRule[ruleId] ?? 0) + 1;
2530
2706
  }
2531
- } else {
2707
+ }
2708
+ if (openpkgExp && isExportDocumented(openpkgExp)) {
2532
2709
  documentedExports++;
2533
2710
  }
2534
- if (analysis?.drift && analysis.drift.length > 0) {
2711
+ if (analysis.drift && analysis.drift.length > 0) {
2535
2712
  data.drift = analysis.drift;
2536
2713
  totalDrift += analysis.drift.length;
2537
2714
  }
2538
- const exportId = exp.id ?? exp.name;
2715
+ if (analysis.overloadCount && analysis.overloadCount > 1) {
2716
+ data.overloadCount = analysis.overloadCount;
2717
+ }
2539
2718
  exportsData[exportId] = data;
2540
2719
  }
2541
2720
  const coverage = {
@@ -2778,6 +2957,226 @@ function renderApiSurface(spec) {
2778
2957
  `);
2779
2958
  }
2780
2959
 
2960
+ // src/analysis/module-graph.ts
2961
+ function buildModuleGraph(specs) {
2962
+ const modules = new Map;
2963
+ const exports = new Map;
2964
+ const types = new Map;
2965
+ const all = new Set;
2966
+ for (const spec of specs) {
2967
+ const moduleName = spec.meta.name;
2968
+ const moduleExports = new Set;
2969
+ const moduleTypes = new Set;
2970
+ for (const exp of spec.exports ?? []) {
2971
+ moduleExports.add(exp.name);
2972
+ all.add(exp.name);
2973
+ if (exp.id) {
2974
+ moduleExports.add(exp.id);
2975
+ all.add(exp.id);
2976
+ }
2977
+ if (!exports.has(exp.name)) {
2978
+ exports.set(exp.name, moduleName);
2979
+ }
2980
+ if (exp.kind === "namespace" && exp.members) {
2981
+ for (const member of exp.members) {
2982
+ if (!member.name)
2983
+ continue;
2984
+ moduleExports.add(member.name);
2985
+ all.add(member.name);
2986
+ if (!exports.has(member.name)) {
2987
+ exports.set(member.name, moduleName);
2988
+ }
2989
+ }
2990
+ }
2991
+ }
2992
+ for (const type of spec.types ?? []) {
2993
+ moduleTypes.add(type.name);
2994
+ all.add(type.name);
2995
+ if (type.id) {
2996
+ moduleTypes.add(type.id);
2997
+ all.add(type.id);
2998
+ }
2999
+ if (!types.has(type.name)) {
3000
+ types.set(type.name, moduleName);
3001
+ }
3002
+ }
3003
+ modules.set(moduleName, {
3004
+ name: moduleName,
3005
+ exports: moduleExports,
3006
+ types: moduleTypes
3007
+ });
3008
+ }
3009
+ return { modules, exports, types, all };
3010
+ }
3011
+ function findSymbolModule(graph, symbol) {
3012
+ return graph.exports.get(symbol) ?? graph.types.get(symbol);
3013
+ }
3014
+ function symbolExistsInGraph(graph, symbol) {
3015
+ return graph.all.has(symbol);
3016
+ }
3017
+
3018
+ // src/analysis/incremental.ts
3019
+ import * as fs4 from "node:fs";
3020
+ import * as os from "node:os";
3021
+ import * as path4 from "node:path";
3022
+
3023
+ class IncrementalAnalyzer {
3024
+ tempPath;
3025
+ totalExpected;
3026
+ resultCount = 0;
3027
+ fileHandle;
3028
+ constructor(options = {}) {
3029
+ const tempDir = options.tempDir ?? os.tmpdir();
3030
+ const prefix = options.prefix ?? "doccov";
3031
+ this.tempPath = path4.join(tempDir, `${prefix}-${Date.now()}-${process.pid}.ndjson`);
3032
+ }
3033
+ get path() {
3034
+ return this.tempPath;
3035
+ }
3036
+ get count() {
3037
+ return this.resultCount;
3038
+ }
3039
+ setTotal(total) {
3040
+ this.totalExpected = total;
3041
+ }
3042
+ async init() {
3043
+ if (!this.fileHandle) {
3044
+ this.fileHandle = await fs4.promises.open(this.tempPath, "a");
3045
+ const header = {
3046
+ type: "header",
3047
+ startedAt: new Date().toISOString(),
3048
+ totalExpected: this.totalExpected,
3049
+ pid: process.pid
3050
+ };
3051
+ await this.fileHandle.appendFile(JSON.stringify(header) + `
3052
+ `);
3053
+ }
3054
+ }
3055
+ async writeResult(result) {
3056
+ await this.init();
3057
+ const line = JSON.stringify({ type: "result", ...result }) + `
3058
+ `;
3059
+ await this.fileHandle.appendFile(line);
3060
+ this.resultCount++;
3061
+ }
3062
+ async writeExportAnalysis(id, name, analysis) {
3063
+ const result = {
3064
+ id,
3065
+ name,
3066
+ coverageScore: analysis.coverageScore,
3067
+ missing: analysis.missing,
3068
+ drift: analysis.drift,
3069
+ overloadCount: analysis.overloadCount,
3070
+ timestamp: Date.now()
3071
+ };
3072
+ await this.writeResult(result);
3073
+ }
3074
+ async getPartialResults() {
3075
+ try {
3076
+ if (this.fileHandle) {
3077
+ await this.fileHandle.close();
3078
+ this.fileHandle = undefined;
3079
+ }
3080
+ const content = await fs4.promises.readFile(this.tempPath, "utf-8");
3081
+ const lines = content.split(`
3082
+ `).filter(Boolean);
3083
+ const results = [];
3084
+ let totalExpected;
3085
+ for (const line of lines) {
3086
+ try {
3087
+ const parsed = JSON.parse(line);
3088
+ if (parsed.type === "header") {
3089
+ totalExpected = parsed.totalExpected;
3090
+ } else if (parsed.type === "result") {
3091
+ const { type, ...result } = parsed;
3092
+ results.push(result);
3093
+ }
3094
+ } catch {}
3095
+ }
3096
+ return {
3097
+ results,
3098
+ totalExpected,
3099
+ interrupted: totalExpected !== undefined && results.length < totalExpected
3100
+ };
3101
+ } catch (err) {
3102
+ if (err.code === "ENOENT") {
3103
+ return { results: [], interrupted: false };
3104
+ }
3105
+ throw err;
3106
+ }
3107
+ }
3108
+ async cleanup() {
3109
+ try {
3110
+ if (this.fileHandle) {
3111
+ await this.fileHandle.close();
3112
+ this.fileHandle = undefined;
3113
+ }
3114
+ await fs4.promises.unlink(this.tempPath);
3115
+ } catch {}
3116
+ }
3117
+ cleanupSync() {
3118
+ try {
3119
+ if (this.fileHandle) {
3120
+ this.fileHandle = undefined;
3121
+ }
3122
+ fs4.unlinkSync(this.tempPath);
3123
+ } catch {}
3124
+ }
3125
+ exists() {
3126
+ return fs4.existsSync(this.tempPath);
3127
+ }
3128
+ getPartialResultsSync() {
3129
+ try {
3130
+ const content = fs4.readFileSync(this.tempPath, "utf-8");
3131
+ const lines = content.split(`
3132
+ `).filter(Boolean);
3133
+ const results = [];
3134
+ let totalExpected;
3135
+ for (const line of lines) {
3136
+ try {
3137
+ const parsed = JSON.parse(line);
3138
+ if (parsed.type === "header") {
3139
+ totalExpected = parsed.totalExpected;
3140
+ } else if (parsed.type === "result") {
3141
+ const { type, ...result } = parsed;
3142
+ results.push(result);
3143
+ }
3144
+ } catch {}
3145
+ }
3146
+ return {
3147
+ results,
3148
+ totalExpected,
3149
+ interrupted: totalExpected !== undefined && results.length < totalExpected
3150
+ };
3151
+ } catch {
3152
+ return { results: [], interrupted: false };
3153
+ }
3154
+ }
3155
+ }
3156
+ function findOrphanedTempFiles(tempDir = os.tmpdir(), prefix = "doccov") {
3157
+ try {
3158
+ const files = fs4.readdirSync(tempDir);
3159
+ return files.filter((f) => f.startsWith(prefix) && f.endsWith(".ndjson")).map((f) => path4.join(tempDir, f));
3160
+ } catch {
3161
+ return [];
3162
+ }
3163
+ }
3164
+ function cleanupOrphanedTempFiles(tempDir = os.tmpdir(), prefix = "doccov", maxAge = 60 * 60 * 1000) {
3165
+ const files = findOrphanedTempFiles(tempDir, prefix);
3166
+ const now = Date.now();
3167
+ let cleaned = 0;
3168
+ for (const file of files) {
3169
+ try {
3170
+ const stat = fs4.statSync(file);
3171
+ if (now - stat.mtimeMs > maxAge) {
3172
+ fs4.unlinkSync(file);
3173
+ cleaned++;
3174
+ }
3175
+ } catch {}
3176
+ }
3177
+ return cleaned;
3178
+ }
3179
+
2781
3180
  // src/extract/schema/index.ts
2782
3181
  import {
2783
3182
  arktypeAdapter,
@@ -2847,8 +3246,8 @@ async function detectRuntimeSchemas(context) {
2847
3246
  }
2848
3247
 
2849
3248
  // src/utils/project-root.ts
2850
- import * as fs4 from "node:fs";
2851
- import * as path4 from "node:path";
3249
+ import * as fs5 from "node:fs";
3250
+ import * as path5 from "node:path";
2852
3251
  var PROJECT_ROOT_MARKERS = [
2853
3252
  ".git",
2854
3253
  "pnpm-workspace.yaml",
@@ -2858,39 +3257,39 @@ var PROJECT_ROOT_MARKERS = [
2858
3257
  "rush.json"
2859
3258
  ];
2860
3259
  function findProjectRoot(startDir) {
2861
- let dir = path4.resolve(startDir);
2862
- const root = path4.parse(dir).root;
3260
+ let dir = path5.resolve(startDir);
3261
+ const root = path5.parse(dir).root;
2863
3262
  while (dir !== root) {
2864
3263
  for (const marker of PROJECT_ROOT_MARKERS) {
2865
- const markerPath = path4.join(dir, marker);
2866
- if (fs4.existsSync(markerPath)) {
3264
+ const markerPath = path5.join(dir, marker);
3265
+ if (fs5.existsSync(markerPath)) {
2867
3266
  return dir;
2868
3267
  }
2869
3268
  }
2870
- const pkgPath = path4.join(dir, "package.json");
2871
- if (fs4.existsSync(pkgPath)) {
3269
+ const pkgPath = path5.join(dir, "package.json");
3270
+ if (fs5.existsSync(pkgPath)) {
2872
3271
  try {
2873
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
3272
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2874
3273
  if (pkg.workspaces) {
2875
3274
  return dir;
2876
3275
  }
2877
3276
  } catch {}
2878
3277
  }
2879
- dir = path4.dirname(dir);
3278
+ dir = path5.dirname(dir);
2880
3279
  }
2881
- return path4.resolve(startDir);
3280
+ return path5.resolve(startDir);
2882
3281
  }
2883
3282
  function getDoccovDir(cwd) {
2884
3283
  const projectRoot = findProjectRoot(cwd);
2885
- return path4.join(projectRoot, ".doccov");
3284
+ return path5.join(projectRoot, ".doccov");
2886
3285
  }
2887
3286
 
2888
3287
  // src/analysis/history.ts
2889
- import * as fs5 from "node:fs";
2890
- import * as path5 from "node:path";
3288
+ import * as fs6 from "node:fs";
3289
+ import * as path6 from "node:path";
2891
3290
  var HISTORY_DIR = ".doccov/history";
2892
3291
  function getHistoryDir(cwd) {
2893
- return path5.join(getDoccovDir(cwd), "history");
3292
+ return path6.join(getDoccovDir(cwd), "history");
2894
3293
  }
2895
3294
  var RETENTION_DAYS = 90;
2896
3295
  function getSnapshotFilename(timestamp) {
@@ -2905,7 +3304,7 @@ function getSnapshotFilename(timestamp) {
2905
3304
  }
2906
3305
  function computeSnapshot(spec, options) {
2907
3306
  const exports = spec.exports ?? [];
2908
- const documented = exports.filter((e) => e.description && e.description.trim().length > 0);
3307
+ const documented = exports.filter(isExportDocumented);
2909
3308
  const driftCount = exports.reduce((sum, e) => {
2910
3309
  const docs = e.docs;
2911
3310
  return sum + (docs?.drift?.length ?? 0);
@@ -2925,23 +3324,23 @@ function computeSnapshot(spec, options) {
2925
3324
  }
2926
3325
  function saveSnapshot(snapshot, cwd) {
2927
3326
  const historyDir = getHistoryDir(cwd);
2928
- if (!fs5.existsSync(historyDir)) {
2929
- fs5.mkdirSync(historyDir, { recursive: true });
3327
+ if (!fs6.existsSync(historyDir)) {
3328
+ fs6.mkdirSync(historyDir, { recursive: true });
2930
3329
  }
2931
3330
  const filename = getSnapshotFilename(new Date(snapshot.timestamp));
2932
- const filepath = path5.join(historyDir, filename);
2933
- fs5.writeFileSync(filepath, JSON.stringify(snapshot, null, 2));
3331
+ const filepath = path6.join(historyDir, filename);
3332
+ fs6.writeFileSync(filepath, JSON.stringify(snapshot, null, 2));
2934
3333
  }
2935
3334
  function loadSnapshots(cwd) {
2936
3335
  const historyDir = getHistoryDir(cwd);
2937
- if (!fs5.existsSync(historyDir)) {
3336
+ if (!fs6.existsSync(historyDir)) {
2938
3337
  return [];
2939
3338
  }
2940
- const files = fs5.readdirSync(historyDir).filter((f) => f.endsWith(".json")).sort().reverse();
3339
+ const files = fs6.readdirSync(historyDir).filter((f) => f.endsWith(".json")).sort().reverse();
2941
3340
  const snapshots = [];
2942
3341
  for (const file of files) {
2943
3342
  try {
2944
- const content = fs5.readFileSync(path5.join(historyDir, file), "utf-8");
3343
+ const content = fs6.readFileSync(path6.join(historyDir, file), "utf-8");
2945
3344
  snapshots.push(JSON.parse(content));
2946
3345
  } catch {}
2947
3346
  }
@@ -2982,14 +3381,14 @@ function formatDelta(delta) {
2982
3381
  }
2983
3382
  function pruneHistory(cwd, keepCount = 100) {
2984
3383
  const historyDir = getHistoryDir(cwd);
2985
- if (!fs5.existsSync(historyDir)) {
3384
+ if (!fs6.existsSync(historyDir)) {
2986
3385
  return 0;
2987
3386
  }
2988
- const files = fs5.readdirSync(historyDir).filter((f) => f.endsWith(".json")).sort().reverse();
3387
+ const files = fs6.readdirSync(historyDir).filter((f) => f.endsWith(".json")).sort().reverse();
2989
3388
  const toDelete = files.slice(keepCount);
2990
3389
  for (const file of toDelete) {
2991
3390
  try {
2992
- fs5.unlinkSync(path5.join(historyDir, file));
3391
+ fs6.unlinkSync(path6.join(historyDir, file));
2993
3392
  } catch {}
2994
3393
  }
2995
3394
  return toDelete.length;
@@ -3085,4 +3484,4 @@ function getExtendedTrend(spec, cwd, options) {
3085
3484
  };
3086
3485
  }
3087
3486
 
3088
- export { isFixableDrift, generateFix, generateFixesForExport, mergeFixes, categorizeDrifts, generateForgottenExportFixes, groupFixesByFile, applyForgottenExportFixes, previewForgottenExportFixes, ts, parseJSDocToPatch, applyPatchToJSDoc, serializeJSDoc, findJSDocLocation, applyEdits, createSourceFile, isBuiltInIdentifier, detectExampleRuntimeErrors, parseAssertions, hasNonAssertionComments, detectExampleAssertionFailures, buildExportRegistry, computeDrift, computeExportDrift, computeHealth, buildDocCovSpec, DRIFT_CATEGORIES2 as DRIFT_CATEGORIES, DRIFT_CATEGORY_LABELS, DRIFT_CATEGORY_DESCRIPTIONS, categorizeDrift, groupDriftsByCategory, getDriftSummary, formatDriftSummaryLine, calculateAggregateCoverage, ensureSpecCoverage, getExportAnalysis, getExportScore, getExportDrift, getExportMissing, isExportFullyDocumented, generateReport, generateReportFromDocCov, loadCachedReport, saveReport, isCachedReportValid, renderApiSurface, extractSchemaOutputType, getRegisteredAdapters, getSupportedLibraries, extractSchemaType, extractStandardSchemas, extractStandardSchemasFromProject, findAdapter, isSchemaType, isStandardJSONSchema, resolveCompiledPath, detectRuntimeSchemas, findProjectRoot, getDoccovDir, HISTORY_DIR, computeSnapshot, saveSnapshot, loadSnapshots, getTrend, renderSparkline, formatDelta, pruneHistory, loadSnapshotsForDays, generateWeeklySummaries, getExtendedTrend };
3487
+ export { isFixableDrift, generateFix, generateFixesForExport, mergeFixes, categorizeDrifts, generateForgottenExportFixes, groupFixesByFile, applyForgottenExportFixes, previewForgottenExportFixes, ts, parseJSDocToPatch, applyPatchToJSDoc, serializeJSDoc, findJSDocLocation, applyEdits, createSourceFile, isBuiltInIdentifier, detectExampleRuntimeErrors, parseAssertions, hasNonAssertionComments, detectExampleAssertionFailures, buildExportRegistry, computeDrift, computeExportDrift, isExportDocumented, computeHealth, DEFAULT_REQUIREMENTS, PRESETS, resolveRequirements, buildDocCovSpec, DRIFT_CATEGORIES2 as DRIFT_CATEGORIES, DRIFT_CATEGORY_LABELS, DRIFT_CATEGORY_DESCRIPTIONS, categorizeDrift, groupDriftsByCategory, getDriftSummary, formatDriftSummaryLine, calculateAggregateCoverage, ensureSpecCoverage, getExportAnalysis, getExportScore, getExportDrift, getExportMissing, isExportFullyDocumented, generateReport, generateReportFromDocCov, loadCachedReport, saveReport, isCachedReportValid, renderApiSurface, buildModuleGraph, findSymbolModule, symbolExistsInGraph, IncrementalAnalyzer, findOrphanedTempFiles, cleanupOrphanedTempFiles, extractSchemaOutputType, getRegisteredAdapters, getSupportedLibraries, extractSchemaType, extractStandardSchemas, extractStandardSchemasFromProject, findAdapter, isSchemaType, isStandardJSONSchema, resolveCompiledPath, detectRuntimeSchemas, findProjectRoot, getDoccovDir, HISTORY_DIR, computeSnapshot, saveSnapshot, loadSnapshots, getTrend, renderSparkline, formatDelta, pruneHistory, loadSnapshotsForDays, generateWeeklySummaries, getExtendedTrend };