@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.
Files changed (34) hide show
  1. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts +7 -0
  2. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.js +7 -0
  4. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts +10 -0
  5. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.js +9 -0
  7. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts +9 -0
  8. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts.map +1 -0
  9. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.js +10 -0
  10. package/dist/adapters/api-adapter.d.ts +26 -0
  11. package/dist/adapters/api-adapter.d.ts.map +1 -1
  12. package/dist/adapters/api-adapter.js +156 -2
  13. package/dist/adapters/playwright-adapter.d.ts.map +1 -1
  14. package/dist/adapters/playwright-adapter.js +71 -2
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/schemas/automation-maturity.schema.d.ts +8 -8
  19. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  20. package/dist/schemas/automation-maturity.schema.js +1 -0
  21. package/dist/schemas/gap-analysis.schema.d.ts +8 -8
  22. package/dist/schemas/gap-analysis.schema.js +1 -1
  23. package/dist/schemas/public-surface.schema.d.ts +5 -5
  24. package/dist/schemas/repo-analysis.schema.d.ts +7 -7
  25. package/dist/tools/repo/api-surface.d.ts +59 -0
  26. package/dist/tools/repo/api-surface.d.ts.map +1 -0
  27. package/dist/tools/repo/api-surface.js +414 -0
  28. package/dist/tools/scoring/api-coverage.d.ts +74 -0
  29. package/dist/tools/scoring/api-coverage.d.ts.map +1 -0
  30. package/dist/tools/scoring/api-coverage.js +158 -0
  31. package/dist/tools/scoring/automation-maturity.d.ts +11 -1
  32. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -1
  33. package/dist/tools/scoring/automation-maturity.js +43 -9
  34. 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
- export declare function computeAutomationMaturity(repo: RepoAnalysis): AutomationMaturity;
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;AAiDrD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAmMhF"}
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). Breadth + harness adoption dominate: shipping risk is mostly
6
- * untested routes and missing Playwright/Cypress-level coverage.
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 W_TEST_BREADTH = 0.28;
9
- const W_FRAMEWORK = 0.22;
10
- const W_TEST_ID = 0.18;
11
- const W_CI = 0.14;
12
- const W_AUTH_TESTS = 0.1;
13
- const W_COMPONENT_RATIO = 0.08;
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
- export function computeAutomationMaturity(repo) {
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.7.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"