@qulib/core 0.7.0 → 0.9.0
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/README.md +30 -5
- package/bin/qulib.js +2 -3
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts +7 -0
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts.map +1 -0
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.js +7 -0
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts +10 -0
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts.map +1 -0
- package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.js +9 -0
- package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts +9 -0
- package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts.map +1 -0
- package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.js +10 -0
- package/dist/__tests__/playwright-available.d.ts +32 -0
- package/dist/__tests__/playwright-available.d.ts.map +1 -0
- package/dist/__tests__/playwright-available.js +35 -0
- package/dist/adapters/api-adapter.d.ts +26 -0
- package/dist/adapters/api-adapter.d.ts.map +1 -1
- package/dist/adapters/api-adapter.js +156 -2
- package/dist/adapters/ci-results-adapter.d.ts +67 -0
- package/dist/adapters/ci-results-adapter.d.ts.map +1 -0
- package/dist/adapters/ci-results-adapter.js +143 -0
- package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
- package/dist/adapters/cypress-e2e-adapter.js +25 -2
- package/dist/adapters/playwright-adapter.d.ts.map +1 -1
- package/dist/adapters/playwright-adapter.js +94 -2
- package/dist/adapters/pr-metadata-adapter.d.ts +75 -0
- package/dist/adapters/pr-metadata-adapter.d.ts.map +1 -0
- package/dist/adapters/pr-metadata-adapter.js +146 -0
- package/dist/adapters/validate-specs.d.ts +55 -0
- package/dist/adapters/validate-specs.d.ts.map +1 -0
- package/dist/adapters/validate-specs.js +67 -0
- package/dist/baseline/baseline.d.ts +54 -0
- package/dist/baseline/baseline.d.ts.map +1 -0
- package/dist/baseline/baseline.js +252 -0
- package/dist/baseline/baseline.schema.d.ts +233 -0
- package/dist/baseline/baseline.schema.d.ts.map +1 -0
- package/dist/baseline/baseline.schema.js +59 -0
- package/dist/cli/confidence-run.d.ts +16 -0
- package/dist/cli/confidence-run.d.ts.map +1 -0
- package/dist/cli/confidence-run.js +158 -0
- package/dist/cli/index.d.ts +11 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +80 -4
- package/dist/cli/scaffold-run.d.ts +86 -0
- package/dist/cli/scaffold-run.d.ts.map +1 -0
- package/dist/cli/scaffold-run.js +232 -0
- package/dist/cli/score-automation-run.d.ts +25 -0
- package/dist/cli/score-automation-run.d.ts.map +1 -0
- package/dist/cli/score-automation-run.js +123 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/fixture.js +174 -0
- package/dist/examples/notquality-dogfood/run.d.ts +34 -0
- package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/run.js +139 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/recipes/a11y.d.ts +36 -0
- package/dist/recipes/a11y.d.ts.map +1 -0
- package/dist/recipes/a11y.js +118 -0
- package/dist/recipes/auth.d.ts +38 -0
- package/dist/recipes/auth.d.ts.map +1 -0
- package/dist/recipes/auth.js +156 -0
- package/dist/recipes/index.d.ts +26 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +41 -0
- package/dist/recipes/nav.d.ts +34 -0
- package/dist/recipes/nav.d.ts.map +1 -0
- package/dist/recipes/nav.js +128 -0
- package/dist/recipes/seed.d.ts +34 -0
- package/dist/recipes/seed.d.ts.map +1 -0
- package/dist/recipes/seed.js +87 -0
- package/dist/scaffold-tests.d.ts +21 -0
- package/dist/scaffold-tests.d.ts.map +1 -1
- package/dist/scaffold-tests.js +12 -2
- package/dist/schemas/automation-maturity.schema.d.ts +8 -8
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
- package/dist/schemas/automation-maturity.schema.js +1 -0
- package/dist/schemas/confidence.schema.d.ts +526 -0
- package/dist/schemas/confidence.schema.d.ts.map +1 -0
- package/dist/schemas/confidence.schema.js +161 -0
- package/dist/schemas/gap-analysis.schema.d.ts +8 -8
- package/dist/schemas/gap-analysis.schema.js +1 -1
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/public-surface.schema.d.ts +5 -5
- package/dist/schemas/recipe.schema.d.ts +66 -0
- package/dist/schemas/recipe.schema.d.ts.map +1 -0
- package/dist/schemas/recipe.schema.js +45 -0
- package/dist/schemas/repo-analysis.schema.d.ts +7 -7
- package/dist/schemas/views.schema.d.ts +234 -0
- package/dist/schemas/views.schema.d.ts.map +1 -0
- package/dist/schemas/views.schema.js +82 -0
- package/dist/tools/repo/api-surface.d.ts +59 -0
- package/dist/tools/repo/api-surface.d.ts.map +1 -0
- package/dist/tools/repo/api-surface.js +414 -0
- package/dist/tools/scoring/api-coverage.d.ts +74 -0
- package/dist/tools/scoring/api-coverage.d.ts.map +1 -0
- package/dist/tools/scoring/api-coverage.js +158 -0
- package/dist/tools/scoring/automation-maturity.d.ts +11 -1
- package/dist/tools/scoring/automation-maturity.d.ts.map +1 -1
- package/dist/tools/scoring/automation-maturity.js +43 -9
- package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
- package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-from-qulib.js +206 -0
- package/dist/tools/scoring/confidence-views.d.ts +40 -0
- package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-views.js +163 -0
- package/dist/tools/scoring/confidence.d.ts +32 -0
- package/dist/tools/scoring/confidence.d.ts.map +1 -0
- package/dist/tools/scoring/confidence.js +180 -0
- package/dist/tools/scoring/levels.d.ts +15 -0
- package/dist/tools/scoring/levels.d.ts.map +1 -0
- package/dist/tools/scoring/levels.js +21 -0
- package/package.json +15 -7
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/repo/api-surface
|
|
3
|
+
* @packageBoundary @qulib/core
|
|
4
|
+
*
|
|
5
|
+
* Evidence-only API surface discovery. Three tiers of confidence:
|
|
6
|
+
* Tier1 — OpenAPI / Swagger spec files (YAML or JSON). Only reads `summary` and
|
|
7
|
+
* `parameters` from a real spec; never fabricates endpoints.
|
|
8
|
+
* Tier2 — Framework route files: Next.js App-Router `route.ts` exports,
|
|
9
|
+
* Next.js Pages `pages/api/`, Express router calls from RepoAnalysis.routes,
|
|
10
|
+
* Fastify `fastify.{method}`, Hono `app.{method}`, NestJS decorators.
|
|
11
|
+
* Tier3 — Opt-in heuristics. Currently: tRPC router definition files.
|
|
12
|
+
* Only activated when `options.enableTier3 === true`.
|
|
13
|
+
*
|
|
14
|
+
* Every endpoint carries:
|
|
15
|
+
* sourceFile — repo-relative file path that evidence was read from
|
|
16
|
+
* sourceTier — 'openapi' | 'framework' | 'heuristic'
|
|
17
|
+
* confidence — 'high' | 'medium' | 'low'
|
|
18
|
+
*
|
|
19
|
+
* NEVER invents endpoints or parameters. When a spec file cannot be parsed,
|
|
20
|
+
* or a file pattern does not clearly indicate a route, the file is skipped.
|
|
21
|
+
*/
|
|
22
|
+
import { readFile } from 'node:fs/promises';
|
|
23
|
+
import { relative, basename } from 'node:path';
|
|
24
|
+
import glob from 'fast-glob';
|
|
25
|
+
const IGNORE_PATTERNS = ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**'];
|
|
26
|
+
/**
|
|
27
|
+
* Shared fast-glob options for every discovery tier. `suppressErrors: true` makes the
|
|
28
|
+
* recursive walk skip subtrees it cannot read (EACCES/EPERM) or that disappear mid-scan
|
|
29
|
+
* (ENOENT) instead of throwing. Real repos — and shared dirs like /tmp on CI runners
|
|
30
|
+
* (e.g. the root-owned, unreadable /tmp/snap-private-tmp on Ubuntu) — routinely contain
|
|
31
|
+
* directories the scanner cannot enter; discovery must degrade gracefully, never crash.
|
|
32
|
+
* `cwd` is supplied per-call.
|
|
33
|
+
*/
|
|
34
|
+
const GLOB_OPTIONS = {
|
|
35
|
+
onlyFiles: true,
|
|
36
|
+
absolute: true,
|
|
37
|
+
ignore: IGNORE_PATTERNS,
|
|
38
|
+
suppressErrors: true,
|
|
39
|
+
};
|
|
40
|
+
function toPosix(p) {
|
|
41
|
+
return p.split('\\').join('/');
|
|
42
|
+
}
|
|
43
|
+
function normalizeMethod(raw) {
|
|
44
|
+
const upper = raw.toUpperCase();
|
|
45
|
+
if (upper === 'GET')
|
|
46
|
+
return 'GET';
|
|
47
|
+
if (upper === 'POST')
|
|
48
|
+
return 'POST';
|
|
49
|
+
if (upper === 'PUT')
|
|
50
|
+
return 'PUT';
|
|
51
|
+
if (upper === 'DELETE')
|
|
52
|
+
return 'DELETE';
|
|
53
|
+
if (upper === 'PATCH')
|
|
54
|
+
return 'PATCH';
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
function isOpenApiDocument(raw) {
|
|
58
|
+
if (typeof raw !== 'object' || raw === null)
|
|
59
|
+
return false;
|
|
60
|
+
const obj = raw;
|
|
61
|
+
return ((typeof obj['openapi'] === 'string' || typeof obj['swagger'] === 'string') &&
|
|
62
|
+
typeof obj['paths'] === 'object');
|
|
63
|
+
}
|
|
64
|
+
async function discoverFromOpenApi(repoPath) {
|
|
65
|
+
const endpoints = [];
|
|
66
|
+
const specFiles = await glob([
|
|
67
|
+
'**/openapi.yaml',
|
|
68
|
+
'**/openapi.yml',
|
|
69
|
+
'**/openapi.json',
|
|
70
|
+
'**/swagger.yaml',
|
|
71
|
+
'**/swagger.yml',
|
|
72
|
+
'**/swagger.json',
|
|
73
|
+
'**/api-docs.yaml',
|
|
74
|
+
'**/api-docs.json',
|
|
75
|
+
], { cwd: repoPath, ...GLOB_OPTIONS });
|
|
76
|
+
let specsFound = 0;
|
|
77
|
+
for (const file of specFiles) {
|
|
78
|
+
const rel = toPosix(relative(repoPath, file));
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile(file, 'utf8');
|
|
82
|
+
if (file.endsWith('.json')) {
|
|
83
|
+
raw = JSON.parse(content);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Dynamic import of js-yaml to avoid bundling issues
|
|
87
|
+
const yaml = (await import('js-yaml'));
|
|
88
|
+
raw = yaml.load(content);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Skip unparseable files — never fabricate
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!isOpenApiDocument(raw))
|
|
96
|
+
continue;
|
|
97
|
+
specsFound++;
|
|
98
|
+
const paths = raw.paths ?? {};
|
|
99
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
100
|
+
if (typeof pathItem !== 'object' || pathItem === null)
|
|
101
|
+
continue;
|
|
102
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch'];
|
|
103
|
+
for (const method of methods) {
|
|
104
|
+
const operation = pathItem[method];
|
|
105
|
+
if (typeof operation !== 'object' || operation === null)
|
|
106
|
+
continue;
|
|
107
|
+
const op = operation;
|
|
108
|
+
const parameterNames = (op.parameters ?? [])
|
|
109
|
+
.filter((p) => typeof p?.name === 'string')
|
|
110
|
+
.map((p) => p.name);
|
|
111
|
+
endpoints.push({
|
|
112
|
+
method: normalizeMethod(method),
|
|
113
|
+
path,
|
|
114
|
+
sourceFile: rel,
|
|
115
|
+
sourceTier: 'openapi',
|
|
116
|
+
confidence: 'high',
|
|
117
|
+
...(typeof op.summary === 'string' && op.summary ? { summary: op.summary } : {}),
|
|
118
|
+
...(parameterNames.length > 0 ? { parameterNames } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { endpoints, specsFound };
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Tier 2 — Framework route files
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
async function discoverNextAppRouterEndpoints(repoPath) {
|
|
129
|
+
const endpoints = [];
|
|
130
|
+
const routeFiles = await glob(['app/**/route.ts', 'app/**/route.tsx', 'src/app/**/route.ts', 'src/app/**/route.tsx'], {
|
|
131
|
+
cwd: repoPath,
|
|
132
|
+
...GLOB_OPTIONS,
|
|
133
|
+
});
|
|
134
|
+
for (const file of routeFiles) {
|
|
135
|
+
const rel = toPosix(relative(repoPath, file));
|
|
136
|
+
let content;
|
|
137
|
+
try {
|
|
138
|
+
content = await readFile(file, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Extract the route path from the file location
|
|
144
|
+
const routeSegment = rel
|
|
145
|
+
.replace(/^(src\/)?app\//, '')
|
|
146
|
+
.replace(/\/route\.tsx?$/, '');
|
|
147
|
+
// Normalize Next.js dynamic segments: [id] -> [id] (keep as-is, it's the real path)
|
|
148
|
+
const routePath = `/${routeSegment}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
149
|
+
// Find exported HTTP method functions: export async function GET/POST/PUT/DELETE/PATCH
|
|
150
|
+
const methodExportRe = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)\b/g;
|
|
151
|
+
let match;
|
|
152
|
+
let foundAny = false;
|
|
153
|
+
while ((match = methodExportRe.exec(content)) !== null) {
|
|
154
|
+
foundAny = true;
|
|
155
|
+
endpoints.push({
|
|
156
|
+
method: normalizeMethod(match[1] ?? 'unknown'),
|
|
157
|
+
path: routePath,
|
|
158
|
+
sourceFile: rel,
|
|
159
|
+
sourceTier: 'framework',
|
|
160
|
+
confidence: 'high',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// If the file exists but has no recognized export, it's still a route — emit as unknown method
|
|
164
|
+
if (!foundAny) {
|
|
165
|
+
endpoints.push({
|
|
166
|
+
method: 'unknown',
|
|
167
|
+
path: routePath,
|
|
168
|
+
sourceFile: rel,
|
|
169
|
+
sourceTier: 'framework',
|
|
170
|
+
confidence: 'medium',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return endpoints;
|
|
175
|
+
}
|
|
176
|
+
async function discoverNextPagesApiEndpoints(repoPath) {
|
|
177
|
+
const endpoints = [];
|
|
178
|
+
const apiFiles = await glob(['pages/api/**/*.ts', 'pages/api/**/*.tsx', 'src/pages/api/**/*.ts'], {
|
|
179
|
+
cwd: repoPath,
|
|
180
|
+
...GLOB_OPTIONS,
|
|
181
|
+
});
|
|
182
|
+
for (const file of apiFiles) {
|
|
183
|
+
const rel = toPosix(relative(repoPath, file));
|
|
184
|
+
const name = basename(rel);
|
|
185
|
+
if (name.startsWith('_'))
|
|
186
|
+
continue;
|
|
187
|
+
const routeSegment = rel
|
|
188
|
+
.replace(/^(src\/)?pages\/api\//, '')
|
|
189
|
+
.replace(/\.tsx?$/, '');
|
|
190
|
+
const routePath = routeSegment === 'index'
|
|
191
|
+
? '/api'
|
|
192
|
+
: `/api/${routeSegment.replace(/\/index$/, '')}`.replace(/\/+/g, '/');
|
|
193
|
+
let content;
|
|
194
|
+
try {
|
|
195
|
+
content = await readFile(file, 'utf8');
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Pages API routes export a default handler that typically checks req.method internally.
|
|
201
|
+
// We can't know the HTTP methods without executing the code, so we emit 'unknown'.
|
|
202
|
+
// Look for explicit method checks like: req.method === 'POST'
|
|
203
|
+
const methodCheckRe = /req\.method\s*(?:===|==|!==|!=)\s*['"`](GET|POST|PUT|DELETE|PATCH)['"`]/g;
|
|
204
|
+
const methods = new Set();
|
|
205
|
+
let match;
|
|
206
|
+
while ((match = methodCheckRe.exec(content)) !== null) {
|
|
207
|
+
if (match[1])
|
|
208
|
+
methods.add(match[1]);
|
|
209
|
+
}
|
|
210
|
+
if (methods.size > 0) {
|
|
211
|
+
for (const method of methods) {
|
|
212
|
+
endpoints.push({
|
|
213
|
+
method: normalizeMethod(method),
|
|
214
|
+
path: routePath,
|
|
215
|
+
sourceFile: rel,
|
|
216
|
+
sourceTier: 'framework',
|
|
217
|
+
confidence: 'medium',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
endpoints.push({
|
|
223
|
+
method: 'unknown',
|
|
224
|
+
path: routePath,
|
|
225
|
+
sourceFile: rel,
|
|
226
|
+
sourceTier: 'framework',
|
|
227
|
+
confidence: 'medium',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return endpoints;
|
|
232
|
+
}
|
|
233
|
+
function discoverExpressEndpoints(repo) {
|
|
234
|
+
// Re-use existing RepoAnalysis.routes which already extracted Express router calls.
|
|
235
|
+
// Filter to only include routes that came from src/ files (Express pattern).
|
|
236
|
+
return repo.routes
|
|
237
|
+
.filter((r) => r.method !== 'GET' || r.file.startsWith('src/'))
|
|
238
|
+
.map((r) => ({
|
|
239
|
+
method: normalizeMethod(r.method),
|
|
240
|
+
path: r.path,
|
|
241
|
+
sourceFile: r.file,
|
|
242
|
+
sourceTier: 'framework',
|
|
243
|
+
confidence: 'medium',
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
async function discoverFastifyEndpoints(repoPath) {
|
|
247
|
+
const endpoints = [];
|
|
248
|
+
const files = await glob(['src/**/*.ts', 'src/**/*.js', 'routes/**/*.ts', 'routes/**/*.js'], {
|
|
249
|
+
cwd: repoPath,
|
|
250
|
+
...GLOB_OPTIONS,
|
|
251
|
+
});
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
const rel = toPosix(relative(repoPath, file));
|
|
254
|
+
let content;
|
|
255
|
+
try {
|
|
256
|
+
content = await readFile(file, 'utf8');
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// Fastify: fastify.get('/path', ...) or fastify.route({ method: 'GET', url: '/path' })
|
|
262
|
+
const fastifyCallRe = /(?:fastify|app|server)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = fastifyCallRe.exec(content)) !== null) {
|
|
265
|
+
const method = match[1];
|
|
266
|
+
const path = match[2];
|
|
267
|
+
if (method && path) {
|
|
268
|
+
endpoints.push({
|
|
269
|
+
method: normalizeMethod(method),
|
|
270
|
+
path: path.startsWith('/') ? path : `/${path}`,
|
|
271
|
+
sourceFile: rel,
|
|
272
|
+
sourceTier: 'framework',
|
|
273
|
+
confidence: 'medium',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Hono: app.get('/path', ...) — same pattern, already covered above if variable is named app/server
|
|
278
|
+
// NestJS decorators: @Get('/path'), @Post('/path'), etc.
|
|
279
|
+
const nestDecoratorRe = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g;
|
|
280
|
+
while ((match = nestDecoratorRe.exec(content)) !== null) {
|
|
281
|
+
const method = match[1];
|
|
282
|
+
const path = match[2];
|
|
283
|
+
if (method !== undefined && path !== undefined) {
|
|
284
|
+
endpoints.push({
|
|
285
|
+
method: normalizeMethod(method),
|
|
286
|
+
path: path === '' ? '/' : (path.startsWith('/') ? path : `/${path}`),
|
|
287
|
+
sourceFile: rel,
|
|
288
|
+
sourceTier: 'framework',
|
|
289
|
+
confidence: 'medium',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return endpoints;
|
|
295
|
+
}
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Tier 3 — Heuristic (opt-in): tRPC
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
async function discoverTrpcEndpoints(repoPath) {
|
|
300
|
+
const endpoints = [];
|
|
301
|
+
const trpcFiles = await glob(['src/**/*.ts', 'server/**/*.ts', 'lib/**/*.ts', 'app/**/*.ts'], { cwd: repoPath, ...GLOB_OPTIONS });
|
|
302
|
+
for (const file of trpcFiles) {
|
|
303
|
+
const rel = toPosix(relative(repoPath, file));
|
|
304
|
+
let content;
|
|
305
|
+
try {
|
|
306
|
+
content = await readFile(file, 'utf8');
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// Only look at files that import from @trpc
|
|
312
|
+
if (!content.includes('@trpc/server') && !content.includes('@trpc/next'))
|
|
313
|
+
continue;
|
|
314
|
+
// tRPC procedure definitions: .query({ ... }) or .mutation({ ... })
|
|
315
|
+
// These aren't HTTP routes in the traditional sense, but procedure names are the "paths"
|
|
316
|
+
const queryRe = /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)\s*\.(?:input\([^)]*\)\s*\.)?query\s*\(/g;
|
|
317
|
+
const mutationRe = /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)\s*\.(?:input\([^)]*\)\s*\.)?mutation\s*\(/g;
|
|
318
|
+
let match;
|
|
319
|
+
while ((match = queryRe.exec(content)) !== null) {
|
|
320
|
+
const name = match[1];
|
|
321
|
+
if (name) {
|
|
322
|
+
endpoints.push({
|
|
323
|
+
method: 'GET',
|
|
324
|
+
path: `/trpc/${name}`,
|
|
325
|
+
sourceFile: rel,
|
|
326
|
+
sourceTier: 'heuristic',
|
|
327
|
+
confidence: 'low',
|
|
328
|
+
summary: `tRPC query: ${name}`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
while ((match = mutationRe.exec(content)) !== null) {
|
|
333
|
+
const name = match[1];
|
|
334
|
+
if (name) {
|
|
335
|
+
endpoints.push({
|
|
336
|
+
method: 'POST',
|
|
337
|
+
path: `/trpc/${name}`,
|
|
338
|
+
sourceFile: rel,
|
|
339
|
+
sourceTier: 'heuristic',
|
|
340
|
+
confidence: 'low',
|
|
341
|
+
summary: `tRPC mutation: ${name}`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return endpoints;
|
|
347
|
+
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// De-duplication
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
function deduplicateEndpoints(endpoints) {
|
|
352
|
+
// Higher tier = higher priority; within a tier, first seen wins
|
|
353
|
+
const tierRank = { openapi: 0, framework: 1, heuristic: 2 };
|
|
354
|
+
const seen = new Map();
|
|
355
|
+
for (const ep of endpoints) {
|
|
356
|
+
const key = `${ep.method}:${ep.path}`;
|
|
357
|
+
const existing = seen.get(key);
|
|
358
|
+
if (!existing) {
|
|
359
|
+
seen.set(key, ep);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Keep the one with higher tier rank (lower number = better)
|
|
363
|
+
const existingRank = tierRank[existing.sourceTier];
|
|
364
|
+
const newRank = tierRank[ep.sourceTier];
|
|
365
|
+
if (newRank < existingRank) {
|
|
366
|
+
seen.set(key, ep);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return [...seen.values()];
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Public entry point
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
export async function discoverApiSurface(repoPath, options = {}) {
|
|
376
|
+
const tier3Enabled = options.enableTier3 === true;
|
|
377
|
+
// Run tiers in parallel for speed
|
|
378
|
+
const [tier1Result, nextAppEndpoints, nextPagesEndpoints, fastifyEndpoints,] = await Promise.all([
|
|
379
|
+
discoverFromOpenApi(repoPath),
|
|
380
|
+
discoverNextAppRouterEndpoints(repoPath),
|
|
381
|
+
discoverNextPagesApiEndpoints(repoPath),
|
|
382
|
+
discoverFastifyEndpoints(repoPath),
|
|
383
|
+
]);
|
|
384
|
+
// Express uses existing RepoAnalysis — callers may pass a pre-scanned repo via a
|
|
385
|
+
// separate helper. Here we expose the raw discovery functions; the integration
|
|
386
|
+
// layer in computeApiCoverage passes the repo. For standalone usage we return
|
|
387
|
+
// only file-based discoveries.
|
|
388
|
+
const tier1 = tier1Result.endpoints;
|
|
389
|
+
const tier2 = [...nextAppEndpoints, ...nextPagesEndpoints, ...fastifyEndpoints];
|
|
390
|
+
const tier3 = tier3Enabled
|
|
391
|
+
? await discoverTrpcEndpoints(repoPath)
|
|
392
|
+
: [];
|
|
393
|
+
const allEndpoints = deduplicateEndpoints([...tier1, ...tier2, ...tier3]);
|
|
394
|
+
return {
|
|
395
|
+
discoveredAt: new Date().toISOString(),
|
|
396
|
+
repoPath,
|
|
397
|
+
endpoints: allEndpoints,
|
|
398
|
+
openApiSpecsFound: tier1Result.specsFound,
|
|
399
|
+
tier3Enabled,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Variant that also incorporates RepoAnalysis.routes (Express routes already
|
|
404
|
+
* extracted by scanRepo). Use this when you already have a RepoAnalysis to avoid
|
|
405
|
+
* double-reading files.
|
|
406
|
+
*/
|
|
407
|
+
export async function discoverApiSurfaceWithRepo(repoPath, repo, options = {}) {
|
|
408
|
+
const base = await discoverApiSurface(repoPath, options);
|
|
409
|
+
const expressEndpoints = discoverExpressEndpoints(repo);
|
|
410
|
+
return {
|
|
411
|
+
...base,
|
|
412
|
+
endpoints: deduplicateEndpoints([...base.endpoints, ...expressEndpoints]),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/scoring/api-coverage
|
|
3
|
+
*
|
|
4
|
+
* Computes the `api-test-coverage` dimension for the automation maturity score.
|
|
5
|
+
*
|
|
6
|
+
* Weight: 0.15. The six existing dimensions have been rebalanced to sum to 1.0:
|
|
7
|
+
*
|
|
8
|
+
* Before (sum = 1.0):
|
|
9
|
+
* test-coverage-breadth 0.28
|
|
10
|
+
* framework-adoption 0.22
|
|
11
|
+
* test-id-hygiene 0.18
|
|
12
|
+
* ci-integration 0.14
|
|
13
|
+
* auth-test-coverage 0.10
|
|
14
|
+
* component-test-ratio 0.08
|
|
15
|
+
* ────
|
|
16
|
+
* 1.00
|
|
17
|
+
*
|
|
18
|
+
* After (sum = 1.0, api-test-coverage added at 0.15):
|
|
19
|
+
* test-coverage-breadth 0.24 (-0.04)
|
|
20
|
+
* framework-adoption 0.19 (-0.03)
|
|
21
|
+
* test-id-hygiene 0.15 (-0.03)
|
|
22
|
+
* ci-integration 0.12 (-0.02)
|
|
23
|
+
* auth-test-coverage 0.09 (-0.01)
|
|
24
|
+
* component-test-ratio 0.06 (-0.02)
|
|
25
|
+
* api-test-coverage 0.15 (new)
|
|
26
|
+
* ────
|
|
27
|
+
* 1.00
|
|
28
|
+
*
|
|
29
|
+
* Scoring rules:
|
|
30
|
+
* - If 0 API endpoints discovered → applicability = 'not_applicable' (excluded from denominator)
|
|
31
|
+
* - A test file "covers" an endpoint if:
|
|
32
|
+
* (a) the endpoint path appears verbatim in the file's coveredPaths, OR
|
|
33
|
+
* (b) the file name contains a token from the endpoint path (heuristic fallback)
|
|
34
|
+
* - Each endpoint that is covered raises the score proportionally.
|
|
35
|
+
* - POST/PUT/DELETE endpoints are HIGH severity gaps when untested;
|
|
36
|
+
* GET endpoints are MEDIUM severity gaps when untested.
|
|
37
|
+
*
|
|
38
|
+
* Evidence is per-endpoint and contextual:
|
|
39
|
+
* "GET /api/users — found in app/api/users/route.ts, covered by tests/api/users.test.ts"
|
|
40
|
+
* "POST /api/orders — found in app/api/orders/route.ts, NOT covered"
|
|
41
|
+
*/
|
|
42
|
+
import type { RepoAnalysis } from '../../schemas/repo-analysis.schema.js';
|
|
43
|
+
import type { AutomationMaturityDimension } from '../../schemas/automation-maturity.schema.js';
|
|
44
|
+
import type { ApiSurface, DiscoveredEndpoint } from '../repo/api-surface.js';
|
|
45
|
+
export declare const W_API_COVERAGE = 0.15;
|
|
46
|
+
/**
|
|
47
|
+
* Rebalanced weights for the original 6 dimensions (sum = 0.85).
|
|
48
|
+
* Export these so automation-maturity.ts can import and use them.
|
|
49
|
+
*/
|
|
50
|
+
export declare const REBALANCED_WEIGHTS: {
|
|
51
|
+
readonly TEST_BREADTH: 0.24;
|
|
52
|
+
readonly FRAMEWORK: 0.19;
|
|
53
|
+
readonly TEST_ID: 0.15;
|
|
54
|
+
readonly CI: 0.12;
|
|
55
|
+
readonly AUTH_TESTS: 0.09;
|
|
56
|
+
readonly COMPONENT_RATIO: 0.06;
|
|
57
|
+
};
|
|
58
|
+
export interface ApiEndpointCoverage {
|
|
59
|
+
method: DiscoveredEndpoint['method'];
|
|
60
|
+
path: string;
|
|
61
|
+
sourceFile: string;
|
|
62
|
+
sourceTier: DiscoveredEndpoint['sourceTier'];
|
|
63
|
+
covered: boolean;
|
|
64
|
+
coveringTestFile?: string;
|
|
65
|
+
severity: 'high' | 'medium' | 'low';
|
|
66
|
+
}
|
|
67
|
+
export interface ApiCoverageResult {
|
|
68
|
+
dimension: AutomationMaturityDimension;
|
|
69
|
+
endpointCoverage: ApiEndpointCoverage[];
|
|
70
|
+
untestedHighSeverityCount: number;
|
|
71
|
+
untestedMediumSeverityCount: number;
|
|
72
|
+
}
|
|
73
|
+
export declare function computeApiCoverage(repo: RepoAnalysis, apiSurface: ApiSurface): ApiCoverageResult;
|
|
74
|
+
//# sourceMappingURL=api-coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-coverage.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/api-coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EACV,2BAA2B,EAE5B,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE7E,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;;;CAOrB,CAAC;AAEX,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,2BAA2B,CAAC;IACvC,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IACxC,yBAAyB,EAAE,MAAM,CAAC;IAClC,2BAA2B,EAAE,MAAM,CAAC;CACrC;AA+BD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,YAAY,EAClB,UAAU,EAAE,UAAU,GACrB,iBAAiB,CA8FnB"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/scoring/api-coverage
|
|
3
|
+
*
|
|
4
|
+
* Computes the `api-test-coverage` dimension for the automation maturity score.
|
|
5
|
+
*
|
|
6
|
+
* Weight: 0.15. The six existing dimensions have been rebalanced to sum to 1.0:
|
|
7
|
+
*
|
|
8
|
+
* Before (sum = 1.0):
|
|
9
|
+
* test-coverage-breadth 0.28
|
|
10
|
+
* framework-adoption 0.22
|
|
11
|
+
* test-id-hygiene 0.18
|
|
12
|
+
* ci-integration 0.14
|
|
13
|
+
* auth-test-coverage 0.10
|
|
14
|
+
* component-test-ratio 0.08
|
|
15
|
+
* ────
|
|
16
|
+
* 1.00
|
|
17
|
+
*
|
|
18
|
+
* After (sum = 1.0, api-test-coverage added at 0.15):
|
|
19
|
+
* test-coverage-breadth 0.24 (-0.04)
|
|
20
|
+
* framework-adoption 0.19 (-0.03)
|
|
21
|
+
* test-id-hygiene 0.15 (-0.03)
|
|
22
|
+
* ci-integration 0.12 (-0.02)
|
|
23
|
+
* auth-test-coverage 0.09 (-0.01)
|
|
24
|
+
* component-test-ratio 0.06 (-0.02)
|
|
25
|
+
* api-test-coverage 0.15 (new)
|
|
26
|
+
* ────
|
|
27
|
+
* 1.00
|
|
28
|
+
*
|
|
29
|
+
* Scoring rules:
|
|
30
|
+
* - If 0 API endpoints discovered → applicability = 'not_applicable' (excluded from denominator)
|
|
31
|
+
* - A test file "covers" an endpoint if:
|
|
32
|
+
* (a) the endpoint path appears verbatim in the file's coveredPaths, OR
|
|
33
|
+
* (b) the file name contains a token from the endpoint path (heuristic fallback)
|
|
34
|
+
* - Each endpoint that is covered raises the score proportionally.
|
|
35
|
+
* - POST/PUT/DELETE endpoints are HIGH severity gaps when untested;
|
|
36
|
+
* GET endpoints are MEDIUM severity gaps when untested.
|
|
37
|
+
*
|
|
38
|
+
* Evidence is per-endpoint and contextual:
|
|
39
|
+
* "GET /api/users — found in app/api/users/route.ts, covered by tests/api/users.test.ts"
|
|
40
|
+
* "POST /api/orders — found in app/api/orders/route.ts, NOT covered"
|
|
41
|
+
*/
|
|
42
|
+
export const W_API_COVERAGE = 0.15;
|
|
43
|
+
/**
|
|
44
|
+
* Rebalanced weights for the original 6 dimensions (sum = 0.85).
|
|
45
|
+
* Export these so automation-maturity.ts can import and use them.
|
|
46
|
+
*/
|
|
47
|
+
export const REBALANCED_WEIGHTS = {
|
|
48
|
+
TEST_BREADTH: 0.24,
|
|
49
|
+
FRAMEWORK: 0.19,
|
|
50
|
+
TEST_ID: 0.15,
|
|
51
|
+
CI: 0.12,
|
|
52
|
+
AUTH_TESTS: 0.09,
|
|
53
|
+
COMPONENT_RATIO: 0.06,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Returns true if testCoveredPaths or test filename hints that it covers endpointPath.
|
|
57
|
+
*/
|
|
58
|
+
function endpointIsCovered(endpoint, testFile) {
|
|
59
|
+
const ep = endpoint.path.toLowerCase();
|
|
60
|
+
// (a) Exact or prefix match in coveredPaths
|
|
61
|
+
const directCover = testFile.coveredPaths.some((cp) => {
|
|
62
|
+
const norm = cp.toLowerCase();
|
|
63
|
+
return norm === ep || (ep !== '/' && ep.startsWith(norm) && (norm === ep || ep[norm.length] === '/'));
|
|
64
|
+
});
|
|
65
|
+
if (directCover)
|
|
66
|
+
return true;
|
|
67
|
+
// (b) Heuristic: test filename tokens match endpoint path segments
|
|
68
|
+
const testFileName = testFile.file.toLowerCase();
|
|
69
|
+
const segments = ep.split('/').filter((s) => s.length > 2 && !/^\[/.test(s));
|
|
70
|
+
if (segments.length === 0)
|
|
71
|
+
return false;
|
|
72
|
+
return segments.some((seg) => testFileName.includes(seg));
|
|
73
|
+
}
|
|
74
|
+
function classifySeverity(method) {
|
|
75
|
+
if (method === 'POST' || method === 'PUT' || method === 'DELETE')
|
|
76
|
+
return 'high';
|
|
77
|
+
if (method === 'PATCH')
|
|
78
|
+
return 'high';
|
|
79
|
+
return 'medium';
|
|
80
|
+
}
|
|
81
|
+
export function computeApiCoverage(repo, apiSurface) {
|
|
82
|
+
const endpoints = apiSurface.endpoints;
|
|
83
|
+
// Not applicable when there are no API endpoints
|
|
84
|
+
if (endpoints.length === 0) {
|
|
85
|
+
const dim = {
|
|
86
|
+
dimension: 'api-test-coverage',
|
|
87
|
+
score: 0,
|
|
88
|
+
weight: W_API_COVERAGE,
|
|
89
|
+
evidence: ['No API endpoints discovered — api-test-coverage dimension does not apply.'],
|
|
90
|
+
recommendations: [],
|
|
91
|
+
applicability: 'not_applicable',
|
|
92
|
+
reason: 'No API endpoints discovered — api-test-coverage dimension does not apply.',
|
|
93
|
+
guidance: 'No API endpoints were found. If this repo has REST endpoints, ensure they are declared in a supported framework (Next.js route.ts, Express, Fastify, NestJS) or an OpenAPI spec file.',
|
|
94
|
+
};
|
|
95
|
+
return { dimension: dim, endpointCoverage: [], untestedHighSeverityCount: 0, untestedMediumSeverityCount: 0 };
|
|
96
|
+
}
|
|
97
|
+
const endpointCoverage = [];
|
|
98
|
+
let coveredCount = 0;
|
|
99
|
+
let untestedHighSeverityCount = 0;
|
|
100
|
+
let untestedMediumSeverityCount = 0;
|
|
101
|
+
const evidence = [];
|
|
102
|
+
for (const ep of endpoints) {
|
|
103
|
+
const severity = classifySeverity(ep.method);
|
|
104
|
+
// Find the first test file that covers this endpoint
|
|
105
|
+
let coveringTestFile;
|
|
106
|
+
for (const tf of repo.testFiles) {
|
|
107
|
+
if (endpointIsCovered(ep, tf)) {
|
|
108
|
+
coveringTestFile = tf.file;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const covered = coveringTestFile !== undefined;
|
|
113
|
+
if (covered) {
|
|
114
|
+
coveredCount++;
|
|
115
|
+
evidence.push(`${ep.method} ${ep.path} — found in ${ep.sourceFile}, covered by ${coveringTestFile}`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (severity === 'high')
|
|
119
|
+
untestedHighSeverityCount++;
|
|
120
|
+
else
|
|
121
|
+
untestedMediumSeverityCount++;
|
|
122
|
+
evidence.push(`${ep.method} ${ep.path} — found in ${ep.sourceFile}, NOT covered`);
|
|
123
|
+
}
|
|
124
|
+
endpointCoverage.push({
|
|
125
|
+
method: ep.method,
|
|
126
|
+
path: ep.path,
|
|
127
|
+
sourceFile: ep.sourceFile,
|
|
128
|
+
sourceTier: ep.sourceTier,
|
|
129
|
+
covered,
|
|
130
|
+
...(coveringTestFile !== undefined ? { coveringTestFile } : {}),
|
|
131
|
+
severity,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const score = Math.round((100 * coveredCount) / endpoints.length);
|
|
135
|
+
const recommendations = [];
|
|
136
|
+
if (untestedHighSeverityCount > 0) {
|
|
137
|
+
recommendations.push(`${untestedHighSeverityCount} high-severity API endpoint(s) (POST/PUT/DELETE/PATCH) have no test coverage — add supertest or API-level tests.`);
|
|
138
|
+
}
|
|
139
|
+
if (untestedMediumSeverityCount > 0) {
|
|
140
|
+
recommendations.push(`${untestedMediumSeverityCount} GET endpoint(s) have no test coverage — add route smoke tests.`);
|
|
141
|
+
}
|
|
142
|
+
const specNote = apiSurface.openApiSpecsFound > 0
|
|
143
|
+
? ` (${apiSurface.openApiSpecsFound} OpenAPI spec(s) parsed — Tier1 high-confidence)`
|
|
144
|
+
: '';
|
|
145
|
+
const dim = {
|
|
146
|
+
dimension: 'api-test-coverage',
|
|
147
|
+
score,
|
|
148
|
+
weight: W_API_COVERAGE,
|
|
149
|
+
evidence: [
|
|
150
|
+
`${coveredCount}/${endpoints.length} API endpoints appear covered by test files${specNote}.`,
|
|
151
|
+
...evidence.slice(0, 10),
|
|
152
|
+
...(evidence.length > 10 ? [`… and ${evidence.length - 10} more endpoint(s)`] : []),
|
|
153
|
+
],
|
|
154
|
+
recommendations,
|
|
155
|
+
applicability: 'applicable',
|
|
156
|
+
};
|
|
157
|
+
return { dimension: dim, endpointCoverage, untestedHighSeverityCount, untestedMediumSeverityCount };
|
|
158
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { RepoAnalysis } from '../../schemas/repo-analysis.schema.js';
|
|
2
2
|
import type { AutomationMaturity } from '../../schemas/automation-maturity.schema.js';
|
|
3
|
-
|
|
3
|
+
import { type ApiCoverageResult } from './api-coverage.js';
|
|
4
|
+
/**
|
|
5
|
+
* Compute automation maturity for a repo.
|
|
6
|
+
*
|
|
7
|
+
* @param repo - The scanned repo analysis.
|
|
8
|
+
* @param apiCoverageResult - Optional pre-computed API coverage result. When absent the
|
|
9
|
+
* 6 original dimensions are scored with the original weights (backward-compatible).
|
|
10
|
+
* When provided, a 7th dimension (`api-test-coverage`) is added and weights are
|
|
11
|
+
* rebalanced so the total still sums to 1.0 across applicable dimensions.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeAutomationMaturity(repo: RepoAnalysis, apiCoverageResult?: ApiCoverageResult): AutomationMaturity;
|
|
4
14
|
//# sourceMappingURL=automation-maturity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,6CAA6C,CAAC;AAErD,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AA4D/E;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,YAAY,EAClB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,kBAAkB,CAiNpB"}
|