@qulib/core 0.7.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__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/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/playwright-adapter.d.ts.map +1 -1
- package/dist/adapters/playwright-adapter.js +71 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- 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/gap-analysis.schema.d.ts +8 -8
- package/dist/schemas/gap-analysis.schema.js +1 -1
- package/dist/schemas/public-surface.schema.d.ts +5 -5
- package/dist/schemas/repo-analysis.schema.d.ts +7 -7
- 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/package.json +4 -2
|
@@ -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"}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { AutomationMaturitySchema } from '../../schemas/automation-maturity.schema.js';
|
|
4
|
+
import { REBALANCED_WEIGHTS } from './api-coverage.js';
|
|
4
5
|
/**
|
|
5
|
-
* Dimension weights (sum = 1).
|
|
6
|
-
*
|
|
6
|
+
* Dimension weights (sum = 1).
|
|
7
|
+
*
|
|
8
|
+
* When apiSurface is NOT provided (backward-compat path), the original 6 dimensions
|
|
9
|
+
* continue to use the original weights (sum = 1.0). When apiSurface IS provided, the
|
|
10
|
+
* rebalanced weights from api-coverage.ts are used and the 7th dimension is added.
|
|
11
|
+
*
|
|
12
|
+
* Original weights (no API surface):
|
|
13
|
+
* test-coverage-breadth 0.28, framework-adoption 0.22, test-id-hygiene 0.18,
|
|
14
|
+
* ci-integration 0.14, auth-test-coverage 0.10, component-test-ratio 0.08
|
|
15
|
+
*
|
|
16
|
+
* Rebalanced weights (with API surface, sum still = 1.0 across all 7):
|
|
17
|
+
* test-coverage-breadth 0.24, framework-adoption 0.19, test-id-hygiene 0.15,
|
|
18
|
+
* ci-integration 0.12, auth-test-coverage 0.09, component-test-ratio 0.06,
|
|
19
|
+
* api-test-coverage 0.15
|
|
7
20
|
*/
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
21
|
+
const W_TEST_BREADTH_ORIGINAL = 0.28;
|
|
22
|
+
const W_FRAMEWORK_ORIGINAL = 0.22;
|
|
23
|
+
const W_TEST_ID_ORIGINAL = 0.18;
|
|
24
|
+
const W_CI_ORIGINAL = 0.14;
|
|
25
|
+
const W_AUTH_TESTS_ORIGINAL = 0.1;
|
|
26
|
+
const W_COMPONENT_RATIO_ORIGINAL = 0.08;
|
|
14
27
|
function hasCiAtRoot(repoPath) {
|
|
15
28
|
const ev = [];
|
|
16
29
|
const gh = join(repoPath, '.github', 'workflows');
|
|
@@ -49,7 +62,24 @@ function scoreLevel(overall) {
|
|
|
49
62
|
return { level: 4, label: 'L4 — strong automation' };
|
|
50
63
|
return { level: 5, label: 'L5 — advanced QA automation' };
|
|
51
64
|
}
|
|
52
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Compute automation maturity for a repo.
|
|
67
|
+
*
|
|
68
|
+
* @param repo - The scanned repo analysis.
|
|
69
|
+
* @param apiCoverageResult - Optional pre-computed API coverage result. When absent the
|
|
70
|
+
* 6 original dimensions are scored with the original weights (backward-compatible).
|
|
71
|
+
* When provided, a 7th dimension (`api-test-coverage`) is added and weights are
|
|
72
|
+
* rebalanced so the total still sums to 1.0 across applicable dimensions.
|
|
73
|
+
*/
|
|
74
|
+
export function computeAutomationMaturity(repo, apiCoverageResult) {
|
|
75
|
+
// Choose weights based on whether we have an API surface result
|
|
76
|
+
const hasApiDim = apiCoverageResult !== undefined;
|
|
77
|
+
const W_TEST_BREADTH = hasApiDim ? REBALANCED_WEIGHTS.TEST_BREADTH : W_TEST_BREADTH_ORIGINAL;
|
|
78
|
+
const W_FRAMEWORK = hasApiDim ? REBALANCED_WEIGHTS.FRAMEWORK : W_FRAMEWORK_ORIGINAL;
|
|
79
|
+
const W_TEST_ID = hasApiDim ? REBALANCED_WEIGHTS.TEST_ID : W_TEST_ID_ORIGINAL;
|
|
80
|
+
const W_CI = hasApiDim ? REBALANCED_WEIGHTS.CI : W_CI_ORIGINAL;
|
|
81
|
+
const W_AUTH_TESTS = hasApiDim ? REBALANCED_WEIGHTS.AUTH_TESTS : W_AUTH_TESTS_ORIGINAL;
|
|
82
|
+
const W_COMPONENT_RATIO = hasApiDim ? REBALANCED_WEIGHTS.COMPONENT_RATIO : W_COMPONENT_RATIO_ORIGINAL;
|
|
53
83
|
const routePaths = [...new Set(repo.routes.map((r) => r.path))];
|
|
54
84
|
let coveredRoutes = 0;
|
|
55
85
|
for (const p of routePaths) {
|
|
@@ -204,6 +234,10 @@ export function computeAutomationMaturity(repo) {
|
|
|
204
234
|
...(compGuidance && { guidance: compGuidance }),
|
|
205
235
|
};
|
|
206
236
|
const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
|
|
237
|
+
// Append api-test-coverage dimension when an API surface result was provided
|
|
238
|
+
if (apiCoverageResult) {
|
|
239
|
+
dimensions.push(apiCoverageResult.dimension);
|
|
240
|
+
}
|
|
207
241
|
// Overall score normalizes over applicable dimensions only.
|
|
208
242
|
// overallScore = round( Σ score_i * weight_i / Σ weight_i ) for i ∈ applicable.
|
|
209
243
|
// If no dimension is applicable (degenerate repo), overall = 0 and level = L1.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qulib/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tapesh Nagarwal",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"analyze": "tsx src/cli/index.ts analyze",
|
|
49
49
|
"clean": "tsx src/cli/index.ts clean",
|
|
50
50
|
"build": "tsc",
|
|
51
|
-
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/agent-summary.test.ts src/__tests__/cli-agent-summary.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
|
|
51
|
+
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/tools/scoring/__tests__/api-coverage.test.ts src/tools/scoring/__tests__/automation-maturity-with-api.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/agent-summary.test.ts src/__tests__/cli-agent-summary.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts src/adapters/__tests__/playwright-adapter.test.ts src/adapters/__tests__/api-adapter.test.ts src/tools/repo/__tests__/api-surface.test.ts",
|
|
52
52
|
"test:integration": "node --import tsx/esm --test src/__tests__/analyze.integration.test.ts",
|
|
53
53
|
"smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
|
|
54
54
|
"cost-doctor": "tsx src/cli/index.ts cost doctor"
|
|
@@ -58,9 +58,11 @@
|
|
|
58
58
|
"@playwright/test": "^1.44.0",
|
|
59
59
|
"commander": "^12.1.0",
|
|
60
60
|
"fast-glob": "^3.3.2",
|
|
61
|
+
"js-yaml": "^4.2.0",
|
|
61
62
|
"zod": "^3.23.0"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
65
|
+
"@types/js-yaml": "^4.0.9",
|
|
64
66
|
"@types/node": "^20.0.0",
|
|
65
67
|
"tsx": "^4.11.0",
|
|
66
68
|
"typescript": "^5.4.0"
|