@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.
- package/CHANGELOG.md +13 -0
- package/api/coverage/projects/[projectId]/history.ts +174 -0
- package/api/coverage/projects/[projectId]/snapshots.ts +65 -0
- package/api/index.ts +105 -26
- package/migrations/005_coverage_sdk_field_names.ts +41 -0
- package/package.json +3 -3
- package/src/index.ts +16 -54
- package/src/routes/badge.ts +109 -5
- package/src/routes/coverage.ts +19 -32
- package/src/routes/demo.ts +323 -14
- package/src/routes/github-app.ts +6 -6
- package/src/routes/spec-v1.ts +165 -0
- package/src/routes/spec.ts +186 -0
- package/src/utils/github-checks.ts +231 -11
- package/src/utils/remote-analyzer.ts +12 -12
- package/src/utils/spec-cache.ts +131 -0
- package/src/utils/spec-diff-core.ts +406 -0
package/src/routes/badge.ts
CHANGED
|
@@ -205,10 +205,114 @@ badgeRoute.get('/:owner/:repo', async (c) => {
|
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
// GET /badge/:owner/:repo.
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
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
|
+
}
|
package/src/routes/coverage.ts
CHANGED
|
@@ -92,13 +92,9 @@ coverageRoute.get('/projects/:projectId/history', async (c) => {
|
|
|
92
92
|
'version',
|
|
93
93
|
'branch',
|
|
94
94
|
'commitSha',
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
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.
|
|
232
|
-
const remaining = 100 - last.
|
|
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.
|
|
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.
|
|
269
|
+
exportsLost: prev.documentedExports - curr.documentedExports,
|
|
283
270
|
};
|
|
284
271
|
}
|
|
285
272
|
}
|
package/src/routes/demo.ts
CHANGED
|
@@ -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
|
|
90
|
+
* Analysis result summary (matches SDK CoverageSnapshot naming)
|
|
90
91
|
*/
|
|
91
92
|
interface AnalysisSummary {
|
|
92
93
|
packageName: string;
|
|
93
94
|
version: string;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
571
|
+
message: `Found ${summary.totalExports} exports, ${summary.documentedExports} documented`,
|
|
263
572
|
});
|
|
264
573
|
|
|
265
574
|
await sendEvent('status', {
|
package/src/routes/github-app.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
coverageScore: project.coverageScore,
|
|
353
|
+
documentedExports: 0,
|
|
354
|
+
totalExports: 0,
|
|
355
355
|
driftCount: project.driftCount ?? 0,
|
|
356
356
|
qualityErrors: 0,
|
|
357
357
|
qualityWarnings: 0,
|