@doccov/api 0.5.0 → 0.6.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.
@@ -205,10 +205,114 @@ badgeRoute.get('/:owner/:repo', async (c) => {
205
205
  }
206
206
  });
207
207
 
208
- // GET /badge/:owner/:repo.svg (alias)
209
- badgeRoute.get('/:owner/:repo.svg', async (c) => {
208
+ // GET /badge/:owner/:repo/json - Shields.io endpoint format
209
+ // https://shields.io/badges/endpoint-badge
210
+ badgeRoute.get('/:owner/:repo/json', async (c) => {
210
211
  const { owner, repo } = c.req.param();
211
- const repoName = repo.replace(/\.svg$/, '');
212
- const query = new URL(c.req.url).search;
213
- return c.redirect(`/badge/${owner}/${repoName}${query}`);
212
+
213
+ const ref = c.req.query('ref') ?? c.req.query('branch') ?? 'main';
214
+ const specPath = c.req.query('path') ?? c.req.query('package') ?? 'openpkg.json';
215
+
216
+ try {
217
+ const spec = await fetchSpec(owner, repo, { ref, path: specPath });
218
+
219
+ if (!spec) {
220
+ return c.json(
221
+ { schemaVersion: 1, label: 'docs', message: 'not found', color: 'lightgrey' },
222
+ 404,
223
+ { 'Cache-Control': 'no-cache' },
224
+ );
225
+ }
226
+
227
+ const validation = validateSpec(spec);
228
+ if (!validation.ok) {
229
+ return c.json(
230
+ { schemaVersion: 1, label: 'docs', message: 'invalid', color: 'lightgrey' },
231
+ 422,
232
+ { 'Cache-Control': 'no-cache' },
233
+ );
234
+ }
235
+
236
+ const coverageScore =
237
+ (spec as { docs?: { coverageScore?: number } }).docs?.coverageScore ??
238
+ computeCoverageScore(spec);
239
+
240
+ return c.json(
241
+ {
242
+ schemaVersion: 1,
243
+ label: 'docs',
244
+ message: `${coverageScore}%`,
245
+ color: getColorForScore(coverageScore),
246
+ },
247
+ 200,
248
+ { 'Cache-Control': 'public, max-age=300, stale-if-error=3600' },
249
+ );
250
+ } catch {
251
+ return c.json(
252
+ { schemaVersion: 1, label: 'docs', message: 'error', color: 'red' },
253
+ 500,
254
+ { 'Cache-Control': 'no-cache' },
255
+ );
256
+ }
257
+ });
258
+
259
+ // GET /badge/:owner/:repo/drift - Drift badge variant
260
+ badgeRoute.get('/:owner/:repo/drift', async (c) => {
261
+ const { owner, repo } = c.req.param();
262
+
263
+ const ref = c.req.query('ref') ?? c.req.query('branch') ?? 'main';
264
+ const specPath = c.req.query('path') ?? c.req.query('package') ?? 'openpkg.json';
265
+ const style = (c.req.query('style') ?? 'flat') as BadgeStyle;
266
+
267
+ try {
268
+ const spec = await fetchSpec(owner, repo, { ref, path: specPath });
269
+
270
+ if (!spec) {
271
+ const svg = generateBadgeSvg({
272
+ label: 'drift',
273
+ message: 'not found',
274
+ color: 'lightgrey',
275
+ style,
276
+ });
277
+ return c.body(svg, 404, CACHE_HEADERS_ERROR);
278
+ }
279
+
280
+ // Compute drift score from exports with drift issues
281
+ const exports = spec.exports ?? [];
282
+ const exportsWithDrift = exports.filter((e) => {
283
+ const docs = (e as { docs?: { drift?: unknown[] } }).docs;
284
+ return docs?.drift && Array.isArray(docs.drift) && docs.drift.length > 0;
285
+ });
286
+ const driftScore = exports.length === 0 ? 0 : Math.round((exportsWithDrift.length / exports.length) * 100);
287
+
288
+ // Lower drift is better
289
+ const color = getDriftColor(driftScore);
290
+
291
+ const svg = generateBadgeSvg({
292
+ label: 'drift',
293
+ message: `${driftScore}%`,
294
+ color,
295
+ style,
296
+ });
297
+
298
+ return c.body(svg, 200, CACHE_HEADERS_SUCCESS);
299
+ } catch {
300
+ const svg = generateBadgeSvg({
301
+ label: 'drift',
302
+ message: 'error',
303
+ color: 'red',
304
+ style,
305
+ });
306
+ return c.body(svg, 500, CACHE_HEADERS_ERROR);
307
+ }
214
308
  });
309
+
310
+ function getDriftColor(score: number): BadgeColor {
311
+ // Inverse of coverage - lower is better
312
+ if (score <= 5) return 'brightgreen';
313
+ if (score <= 10) return 'green';
314
+ if (score <= 20) return 'yellowgreen';
315
+ if (score <= 30) return 'yellow';
316
+ if (score <= 50) return 'orange';
317
+ return 'red';
318
+ }
@@ -92,13 +92,9 @@ coverageRoute.get('/projects/:projectId/history', async (c) => {
92
92
  'version',
93
93
  'branch',
94
94
  'commitSha',
95
- 'coveragePercent',
96
- 'documentedCount',
97
- 'totalCount',
98
- 'descriptionCount',
99
- 'paramsCount',
100
- 'returnsCount',
101
- 'examplesCount',
95
+ 'coverageScore',
96
+ 'documentedExports',
97
+ 'totalExports',
102
98
  'driftCount',
103
99
  'source',
104
100
  'createdAt',
@@ -129,13 +125,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
129
125
  version?: string;
130
126
  branch?: string;
131
127
  commitSha?: string;
132
- coveragePercent: number;
133
- documentedCount: number;
134
- totalCount: number;
135
- descriptionCount?: number;
136
- paramsCount?: number;
137
- returnsCount?: number;
138
- examplesCount?: number;
128
+ coverageScore: number;
129
+ documentedExports: number;
130
+ totalExports: number;
139
131
  driftCount?: number;
140
132
  source?: 'ci' | 'manual' | 'scheduled';
141
133
  }>();
@@ -162,13 +154,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
162
154
  version: body.version || null,
163
155
  branch: body.branch || null,
164
156
  commitSha: body.commitSha || null,
165
- coveragePercent: body.coveragePercent,
166
- documentedCount: body.documentedCount,
167
- totalCount: body.totalCount,
168
- descriptionCount: body.descriptionCount || null,
169
- paramsCount: body.paramsCount || null,
170
- returnsCount: body.returnsCount || null,
171
- examplesCount: body.examplesCount || null,
157
+ coverageScore: body.coverageScore,
158
+ documentedExports: body.documentedExports,
159
+ totalExports: body.totalExports,
172
160
  driftCount: body.driftCount || 0,
173
161
  source: body.source || 'manual',
174
162
  })
@@ -179,7 +167,7 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
179
167
  await db
180
168
  .updateTable('projects')
181
169
  .set({
182
- coverageScore: body.coveragePercent,
170
+ coverageScore: body.coverageScore,
183
171
  driftCount: body.driftCount || 0,
184
172
  lastAnalyzedAt: new Date(),
185
173
  })
@@ -192,9 +180,9 @@ coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
192
180
  // Helper: Generate insights from coverage data
193
181
  interface Snapshot {
194
182
  version: string | null;
195
- coveragePercent: number;
196
- documentedCount: number;
197
- totalCount: number;
183
+ coverageScore: number;
184
+ documentedExports: number;
185
+ totalExports: number;
198
186
  driftCount: number;
199
187
  }
200
188
 
@@ -210,7 +198,7 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
210
198
 
211
199
  const first = snapshots[0];
212
200
  const last = snapshots[snapshots.length - 1];
213
- const diff = last.coveragePercent - first.coveragePercent;
201
+ const diff = last.coverageScore - first.coverageScore;
214
202
 
215
203
  // Overall improvement/regression
216
204
  if (diff > 0) {
@@ -228,8 +216,8 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
228
216
  }
229
217
 
230
218
  // Predict time to 100%
231
- if (diff > 0 && last.coveragePercent < 100) {
232
- const remaining = 100 - last.coveragePercent;
219
+ if (diff > 0 && last.coverageScore < 100) {
220
+ const remaining = 100 - last.coverageScore;
233
221
  const avgGainPerSnapshot = diff / (snapshots.length - 1);
234
222
  if (avgGainPerSnapshot > 0) {
235
223
  const snapshotsToComplete = Math.ceil(remaining / avgGainPerSnapshot);
@@ -245,8 +233,7 @@ function generateInsights(snapshots: Snapshot[]): Insight[] {
245
233
  const milestones = [50, 75, 90, 100];
246
234
  for (const milestone of milestones) {
247
235
  const crossedAt = snapshots.findIndex(
248
- (s, i) =>
249
- i > 0 && s.coveragePercent >= milestone && snapshots[i - 1].coveragePercent < milestone,
236
+ (s, i) => i > 0 && s.coverageScore >= milestone && snapshots[i - 1].coverageScore < milestone,
250
237
  );
251
238
  if (crossedAt > 0) {
252
239
  insights.push({
@@ -271,7 +258,7 @@ function detectRegression(
271
258
  for (let i = 1; i < recent.length; i++) {
272
259
  const prev = recent[i - 1];
273
260
  const curr = recent[i];
274
- const drop = prev.coveragePercent - curr.coveragePercent;
261
+ const drop = prev.coverageScore - curr.coverageScore;
275
262
 
276
263
  if (drop >= 3) {
277
264
  // 3% or more drop
@@ -279,7 +266,7 @@ function detectRegression(
279
266
  fromVersion: prev.version || `v${i}`,
280
267
  toVersion: curr.version || `v${i + 1}`,
281
268
  coverageDrop: Math.round(drop),
282
- exportsLost: prev.documentedCount - curr.documentedCount,
269
+ exportsLost: prev.documentedExports - curr.documentedExports,
283
270
  };
284
271
  }
285
272
  }
@@ -3,6 +3,7 @@
3
3
  * Uses the /plan and /execute-stream endpoints from the Vercel API
4
4
  */
5
5
 
6
+ import { fetchGitHubContext, parseScanGitHubUrl } from '@doccov/sdk';
6
7
  import { Hono } from 'hono';
7
8
  import { streamSSE } from 'hono/streaming';
8
9
  import { anonymousRateLimit } from '../middleware/anonymous-rate-limit';
@@ -86,20 +87,85 @@ async function fetchNpmPackage(packageName: string): Promise<NpmPackageInfo> {
86
87
  }
87
88
 
88
89
  /**
89
- * Analysis result summary (matches SpecSummary from Vercel API)
90
+ * Analysis result summary (matches SDK CoverageSnapshot naming)
90
91
  */
91
92
  interface AnalysisSummary {
92
93
  packageName: string;
93
94
  version: string;
94
- coverage: number;
95
- exportCount: number;
96
- documentedCount: number;
97
- undocumentedCount: number;
95
+ coverageScore: number;
96
+ totalExports: number;
97
+ documentedExports: number;
98
98
  driftCount: number;
99
99
  topUndocumented: string[];
100
100
  topDrift: Array<{ name: string; issue: string }>;
101
101
  }
102
102
 
103
+ /**
104
+ * Workspace package info for monorepo detection
105
+ */
106
+ interface WorkspacePackageInfo {
107
+ name: string;
108
+ path: string;
109
+ private: boolean;
110
+ }
111
+
112
+ /**
113
+ * Resolve workspace patterns to actual package names via GitHub API.
114
+ * Fetches package.json from each directory to get real package names.
115
+ */
116
+ async function resolveGitHubPackages(
117
+ owner: string,
118
+ repo: string,
119
+ ref: string,
120
+ patterns: string[],
121
+ ): Promise<WorkspacePackageInfo[]> {
122
+ const packages: WorkspacePackageInfo[] = [];
123
+ const seen = new Set<string>();
124
+
125
+ for (const pattern of patterns) {
126
+ // Extract base directory from pattern: "packages/*" -> "packages"
127
+ const baseDir = pattern.replace(/\/?\*\*?$/, '');
128
+ if (!baseDir || baseDir.includes('*')) continue;
129
+
130
+ // List directories via GitHub API
131
+ const contentsUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${baseDir}?ref=${ref}`;
132
+ const contentsRes = await fetch(contentsUrl, {
133
+ headers: { 'User-Agent': 'DocCov', Accept: 'application/vnd.github.v3+json' },
134
+ });
135
+
136
+ if (!contentsRes.ok) continue;
137
+
138
+ const contents = (await contentsRes.json()) as Array<{ name: string; type: string }>;
139
+
140
+ // Fetch package.json from each subdirectory
141
+ for (const item of contents) {
142
+ if (item.type !== 'dir') continue;
143
+
144
+ const pkgPath = `${baseDir}/${item.name}`;
145
+ const pkgJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${pkgPath}/package.json`;
146
+
147
+ try {
148
+ const pkgRes = await fetch(pkgJsonUrl);
149
+ if (!pkgRes.ok) continue;
150
+
151
+ const pkg = (await pkgRes.json()) as { name?: string; private?: boolean };
152
+ if (pkg.name && !seen.has(pkg.name)) {
153
+ seen.add(pkg.name);
154
+ packages.push({
155
+ name: pkg.name,
156
+ path: pkgPath,
157
+ private: pkg.private ?? false,
158
+ });
159
+ }
160
+ } catch {
161
+ // Skip invalid package.json
162
+ }
163
+ }
164
+ }
165
+
166
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
167
+ }
168
+
103
169
  // GET /demo/analyze?package=lodash
104
170
  demoRoute.get('/analyze', async (c) => {
105
171
  const packageName = c.req.query('package');
@@ -228,6 +294,250 @@ demoRoute.get('/analyze', async (c) => {
228
294
  exports: number;
229
295
  documented: number;
230
296
  undocumented: number;
297
+ driftCount: number;
298
+ topUndocumented: string[];
299
+ topDrift: Array<{ name: string; issue: string }>;
300
+ };
301
+ error?: string;
302
+ };
303
+
304
+ // Forward progress events
305
+ if (eventType === 'progress') {
306
+ await sendEvent('log', { message: eventData.message || eventData.stage });
307
+ } else if (eventType === 'step:start') {
308
+ await sendEvent('status', {
309
+ step: eventData.stepId === 'analyze' ? 'analyze' : 'build',
310
+ message: eventData.name || `Running ${eventData.stepId}...`,
311
+ });
312
+ } else if (eventType === 'step:complete' && eventData.stepId) {
313
+ await sendEvent('log', {
314
+ message: `${eventData.stepId} completed`,
315
+ });
316
+ } else if (eventType === 'complete' && eventData.summary) {
317
+ // Transform summary to our format (SDK-aligned field names)
318
+ const summary: AnalysisSummary = {
319
+ packageName: eventData.summary.name,
320
+ version: eventData.summary.version,
321
+ coverageScore: eventData.summary.coverage,
322
+ totalExports: eventData.summary.exports,
323
+ documentedExports: eventData.summary.documented,
324
+ driftCount: eventData.summary.driftCount ?? 0,
325
+ topUndocumented: eventData.summary.topUndocumented ?? [],
326
+ topDrift: eventData.summary.topDrift ?? [],
327
+ };
328
+
329
+ await sendEvent('log', {
330
+ message: `Found ${summary.totalExports} exports, ${summary.documentedExports} documented`,
331
+ });
332
+
333
+ await sendEvent('status', {
334
+ step: 'complete',
335
+ message: 'Analysis complete!',
336
+ });
337
+
338
+ await sendEvent('result', { data: summary });
339
+ return;
340
+ } else if (eventType === 'error') {
341
+ throw new Error(eventData.error || 'Execution failed');
342
+ }
343
+ } catch (parseError) {
344
+ // Ignore JSON parse errors for incomplete data
345
+ if (parseError instanceof SyntaxError) continue;
346
+ throw parseError;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ // If we get here without a complete event, something went wrong
354
+ throw new Error('Execution completed without results');
355
+ } catch (err) {
356
+ const message = err instanceof Error ? err.message : 'Analysis failed';
357
+ await sendEvent('error', { message });
358
+ }
359
+ });
360
+ });
361
+
362
+ // POST /demo/detect - detect monorepo packages from GitHub URL
363
+ demoRoute.post('/detect', async (c) => {
364
+ const body = (await c.req.json()) as { url?: string };
365
+
366
+ if (!body.url) {
367
+ return c.json({ error: 'GitHub URL required' }, 400);
368
+ }
369
+
370
+ // Validate and parse GitHub URL
371
+ const parsed = parseScanGitHubUrl(body.url);
372
+ if (!parsed) {
373
+ return c.json({ error: 'Invalid GitHub URL' }, 400);
374
+ }
375
+
376
+ try {
377
+ // Fetch context from GitHub
378
+ const context = await fetchGitHubContext(body.url);
379
+
380
+ // If not a monorepo, return simple response
381
+ if (!context.workspace.isMonorepo) {
382
+ return c.json({
383
+ isMonorepo: false,
384
+ packageManager: context.packageManager,
385
+ owner: context.metadata.owner,
386
+ repo: context.metadata.repo,
387
+ ref: context.ref,
388
+ packages: [],
389
+ });
390
+ }
391
+
392
+ // Resolve actual package names from workspace patterns
393
+ const patterns = context.workspace.packages || ['packages/*'];
394
+ const packages = await resolveGitHubPackages(
395
+ context.metadata.owner,
396
+ context.metadata.repo,
397
+ context.ref,
398
+ patterns,
399
+ );
400
+
401
+ return c.json({
402
+ isMonorepo: true,
403
+ packageManager: context.packageManager,
404
+ owner: context.metadata.owner,
405
+ repo: context.metadata.repo,
406
+ ref: context.ref,
407
+ packages,
408
+ });
409
+ } catch (err) {
410
+ const message = err instanceof Error ? err.message : 'Detection failed';
411
+ return c.json({ error: message }, 500);
412
+ }
413
+ });
414
+
415
+ // GET /demo/analyze-repo?url=...&package=... - analyze GitHub repo directly
416
+ demoRoute.get('/analyze-repo', async (c) => {
417
+ const repoUrl = c.req.query('url');
418
+ const packageName = c.req.query('package');
419
+
420
+ if (!repoUrl) {
421
+ return c.json({ error: 'GitHub URL required' }, 400);
422
+ }
423
+
424
+ // Validate GitHub URL
425
+ const parsed = parseScanGitHubUrl(repoUrl);
426
+ if (!parsed) {
427
+ return c.json({ error: 'Invalid GitHub URL' }, 400);
428
+ }
429
+
430
+ return streamSSE(c, async (stream) => {
431
+ const sendEvent = async (
432
+ type: 'status' | 'log' | 'result' | 'error',
433
+ data: { step?: string; message?: string; data?: unknown },
434
+ ) => {
435
+ await stream.writeSSE({
436
+ data: JSON.stringify({ type, ...data }),
437
+ event: type === 'error' ? 'error' : type === 'result' ? 'complete' : 'progress',
438
+ });
439
+ };
440
+
441
+ try {
442
+ // Step 1: Log repo info
443
+ await sendEvent('status', {
444
+ step: 'repo',
445
+ message: `Analyzing ${parsed.owner}/${parsed.repo}...`,
446
+ });
447
+
448
+ await sendEvent('log', {
449
+ message: `Repository: ${repoUrl}`,
450
+ });
451
+
452
+ if (packageName) {
453
+ await sendEvent('log', {
454
+ message: `Package: ${packageName}`,
455
+ });
456
+ }
457
+
458
+ // Step 2: Generate build plan via /plan endpoint
459
+ await sendEvent('status', {
460
+ step: 'plan',
461
+ message: 'Generating build plan...',
462
+ });
463
+
464
+ const planResponse = await fetch(`${VERCEL_API_URL}/plan`, {
465
+ method: 'POST',
466
+ headers: { 'Content-Type': 'application/json' },
467
+ body: JSON.stringify({
468
+ url: repoUrl,
469
+ package: packageName,
470
+ }),
471
+ });
472
+
473
+ if (!planResponse.ok) {
474
+ const errorData = (await planResponse.json()) as { error?: string };
475
+ throw new Error(errorData.error || `Plan generation failed: ${planResponse.status}`);
476
+ }
477
+
478
+ const planData = (await planResponse.json()) as {
479
+ plan: unknown;
480
+ context: { isMonorepo: boolean; packageManager: string };
481
+ };
482
+
483
+ await sendEvent('log', {
484
+ message: `Build plan ready (${planData.context.packageManager}${planData.context.isMonorepo ? ', monorepo' : ''})`,
485
+ });
486
+
487
+ // Step 3: Execute build plan via /execute-stream endpoint
488
+ await sendEvent('status', {
489
+ step: 'build',
490
+ message: 'Building and analyzing...',
491
+ });
492
+
493
+ const executeResponse = await fetch(`${VERCEL_API_URL}/execute-stream`, {
494
+ method: 'POST',
495
+ headers: { 'Content-Type': 'application/json' },
496
+ body: JSON.stringify({ plan: planData.plan }),
497
+ });
498
+
499
+ if (!executeResponse.ok || !executeResponse.body) {
500
+ throw new Error(`Execution failed: ${executeResponse.status}`);
501
+ }
502
+
503
+ // Stream the execute-stream SSE events and forward relevant ones
504
+ const reader = executeResponse.body.getReader();
505
+ const decoder = new TextDecoder();
506
+ let buffer = '';
507
+
508
+ while (true) {
509
+ const { done, value } = await reader.read();
510
+ if (done) break;
511
+
512
+ buffer += decoder.decode(value, { stream: true });
513
+ const lines = buffer.split('\n');
514
+ buffer = lines.pop() || '';
515
+
516
+ for (const line of lines) {
517
+ if (line.startsWith('event:')) {
518
+ const eventType = line.slice(7).trim();
519
+
520
+ // Get the next data line
521
+ const dataLineIndex = lines.indexOf(line) + 1;
522
+ if (dataLineIndex < lines.length && lines[dataLineIndex].startsWith('data:')) {
523
+ const dataStr = lines[dataLineIndex].slice(5).trim();
524
+ try {
525
+ const eventData = JSON.parse(dataStr) as {
526
+ stage?: string;
527
+ message?: string;
528
+ stepId?: string;
529
+ name?: string;
530
+ success?: boolean;
531
+ summary?: {
532
+ name: string;
533
+ version: string;
534
+ coverage: number;
535
+ exports: number;
536
+ documented: number;
537
+ undocumented: number;
538
+ driftCount: number;
539
+ topUndocumented: string[];
540
+ topDrift: Array<{ name: string; issue: string }>;
231
541
  };
232
542
  error?: string;
233
543
  };
@@ -245,21 +555,20 @@ demoRoute.get('/analyze', async (c) => {
245
555
  message: `${eventData.stepId} completed`,
246
556
  });
247
557
  } else if (eventType === 'complete' && eventData.summary) {
248
- // Transform summary to our format
558
+ // Transform summary to our format (SDK-aligned field names)
249
559
  const summary: AnalysisSummary = {
250
560
  packageName: eventData.summary.name,
251
561
  version: eventData.summary.version,
252
- coverage: eventData.summary.coverage,
253
- exportCount: eventData.summary.exports,
254
- documentedCount: eventData.summary.documented,
255
- undocumentedCount: eventData.summary.undocumented,
256
- driftCount: 0,
257
- topUndocumented: [],
258
- topDrift: [],
562
+ coverageScore: eventData.summary.coverage,
563
+ totalExports: eventData.summary.exports,
564
+ documentedExports: eventData.summary.documented,
565
+ driftCount: eventData.summary.driftCount ?? 0,
566
+ topUndocumented: eventData.summary.topUndocumented ?? [],
567
+ topDrift: eventData.summary.topDrift ?? [],
259
568
  };
260
569
 
261
570
  await sendEvent('log', {
262
- message: `Found ${summary.exportCount} exports, ${summary.documentedCount} documented`,
571
+ message: `Found ${summary.totalExports} exports, ${summary.documentedExports} documented`,
263
572
  });
264
573
 
265
574
  await sendEvent('status', {
@@ -280,7 +280,7 @@ async function handlePushEvent(payload: {
280
280
  const result = await analyzeRemoteRepo(installationId, owner.login, repo, sha);
281
281
 
282
282
  if (result) {
283
- console.log(`[webhook] Analysis complete: ${result.coveragePercent}% coverage`);
283
+ console.log(`[webhook] Analysis complete: ${result.coverageScore}% coverage`);
284
284
 
285
285
  // Create check run with analysis results
286
286
  await createCheckRun(installationId, owner.login, repo, sha, result);
@@ -289,7 +289,7 @@ async function handlePushEvent(payload: {
289
289
  await db
290
290
  .updateTable('projects')
291
291
  .set({
292
- coverageScore: result.coveragePercent,
292
+ coverageScore: result.coverageScore,
293
293
  driftCount: result.driftCount,
294
294
  updatedAt: new Date(),
295
295
  })
@@ -333,7 +333,7 @@ async function handlePullRequestEvent(payload: {
333
333
  return;
334
334
  }
335
335
 
336
- console.log(`[webhook] PR analysis complete: ${headResult.coveragePercent}% coverage`);
336
+ console.log(`[webhook] PR analysis complete: ${headResult.coverageScore}% coverage`);
337
337
 
338
338
  // Try to get baseline from database or analyze base
339
339
  let diff: ReturnType<typeof computeAnalysisDiff> | null = null;
@@ -349,9 +349,9 @@ async function handlePullRequestEvent(payload: {
349
349
  // Use cached baseline for speed
350
350
  diff = computeAnalysisDiff(
351
351
  {
352
- coveragePercent: project.coverageScore,
353
- documentedCount: 0,
354
- totalCount: 0,
352
+ coverageScore: project.coverageScore,
353
+ documentedExports: 0,
354
+ totalExports: 0,
355
355
  driftCount: project.driftCount ?? 0,
356
356
  qualityErrors: 0,
357
357
  qualityWarnings: 0,