@qulib/core 0.5.2 → 0.5.3

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 (96) hide show
  1. package/README.md +9 -9
  2. package/dist/cli/index.js +3 -3
  3. package/dist/tools/auth/detect.d.ts +8 -0
  4. package/dist/tools/auth/detect.d.ts.map +1 -1
  5. package/dist/tools/auth/detect.js +33 -9
  6. package/package.json +1 -1
  7. package/dist/tools/apply-auth.d.ts +0 -4
  8. package/dist/tools/apply-auth.d.ts.map +0 -1
  9. package/dist/tools/apply-auth.js +0 -35
  10. package/dist/tools/auth/block-gap.d.ts +0 -9
  11. package/dist/tools/auth/block-gap.d.ts.map +0 -1
  12. package/dist/tools/auth/block-gap.js +0 -52
  13. package/dist/tools/auth/detector.d.ts +0 -23
  14. package/dist/tools/auth/detector.d.ts.map +0 -1
  15. package/dist/tools/auth/detector.js +0 -526
  16. package/dist/tools/auth/explorer.d.ts +0 -4
  17. package/dist/tools/auth/explorer.d.ts.map +0 -1
  18. package/dist/tools/auth/explorer.js +0 -346
  19. package/dist/tools/auth/oauth-providers.d.ts +0 -7
  20. package/dist/tools/auth/oauth-providers.d.ts.map +0 -1
  21. package/dist/tools/auth/oauth-providers.js +0 -21
  22. package/dist/tools/auth/surface-analyzer.d.ts +0 -4
  23. package/dist/tools/auth/surface-analyzer.d.ts.map +0 -1
  24. package/dist/tools/auth/surface-analyzer.js +0 -170
  25. package/dist/tools/auth/user-providers.d.ts +0 -15
  26. package/dist/tools/auth/user-providers.d.ts.map +0 -1
  27. package/dist/tools/auth/user-providers.js +0 -62
  28. package/dist/tools/auth-block-gap.d.ts +0 -9
  29. package/dist/tools/auth-block-gap.d.ts.map +0 -1
  30. package/dist/tools/auth-block-gap.js +0 -52
  31. package/dist/tools/auth-detector.d.ts +0 -23
  32. package/dist/tools/auth-detector.d.ts.map +0 -1
  33. package/dist/tools/auth-detector.js +0 -526
  34. package/dist/tools/auth-explorer.d.ts +0 -4
  35. package/dist/tools/auth-explorer.d.ts.map +0 -1
  36. package/dist/tools/auth-explorer.js +0 -346
  37. package/dist/tools/auth-surface-analyzer.d.ts +0 -4
  38. package/dist/tools/auth-surface-analyzer.d.ts.map +0 -1
  39. package/dist/tools/auth-surface-analyzer.js +0 -170
  40. package/dist/tools/auth.d.ts +0 -4
  41. package/dist/tools/auth.d.ts.map +0 -1
  42. package/dist/tools/auth.js +0 -35
  43. package/dist/tools/automation-maturity.d.ts +0 -4
  44. package/dist/tools/automation-maturity.d.ts.map +0 -1
  45. package/dist/tools/automation-maturity.js +0 -219
  46. package/dist/tools/browser.d.ts +0 -3
  47. package/dist/tools/browser.d.ts.map +0 -1
  48. package/dist/tools/browser.js +0 -13
  49. package/dist/tools/cypress-explorer.d.ts +0 -8
  50. package/dist/tools/cypress-explorer.d.ts.map +0 -1
  51. package/dist/tools/cypress-explorer.js +0 -5
  52. package/dist/tools/explorer-factory.d.ts +0 -4
  53. package/dist/tools/explorer-factory.d.ts.map +0 -1
  54. package/dist/tools/explorer-factory.js +0 -12
  55. package/dist/tools/explorer.interface.d.ts +0 -7
  56. package/dist/tools/explorer.interface.d.ts.map +0 -1
  57. package/dist/tools/explorer.interface.js +0 -1
  58. package/dist/tools/explorers/cypress-explorer.d.ts +0 -8
  59. package/dist/tools/explorers/cypress-explorer.d.ts.map +0 -1
  60. package/dist/tools/explorers/cypress-explorer.js +0 -5
  61. package/dist/tools/explorers/explorer.interface.d.ts +0 -7
  62. package/dist/tools/explorers/explorer.interface.d.ts.map +0 -1
  63. package/dist/tools/explorers/explorer.interface.js +0 -1
  64. package/dist/tools/explorers/playwright-explorer.d.ts +0 -8
  65. package/dist/tools/explorers/playwright-explorer.d.ts.map +0 -1
  66. package/dist/tools/explorers/playwright-explorer.js +0 -172
  67. package/dist/tools/framework-detector.d.ts +0 -15
  68. package/dist/tools/framework-detector.d.ts.map +0 -1
  69. package/dist/tools/framework-detector.js +0 -153
  70. package/dist/tools/gap-engine.d.ts +0 -8
  71. package/dist/tools/gap-engine.d.ts.map +0 -1
  72. package/dist/tools/gap-engine.js +0 -138
  73. package/dist/tools/oauth-providers.d.ts +0 -7
  74. package/dist/tools/oauth-providers.d.ts.map +0 -1
  75. package/dist/tools/oauth-providers.js +0 -21
  76. package/dist/tools/playwright-explorer.d.ts +0 -8
  77. package/dist/tools/playwright-explorer.d.ts.map +0 -1
  78. package/dist/tools/playwright-explorer.js +0 -172
  79. package/dist/tools/public-surface.d.ts +0 -5
  80. package/dist/tools/public-surface.d.ts.map +0 -1
  81. package/dist/tools/public-surface.js +0 -13
  82. package/dist/tools/repo/framework-detector.d.ts +0 -15
  83. package/dist/tools/repo/framework-detector.d.ts.map +0 -1
  84. package/dist/tools/repo/framework-detector.js +0 -153
  85. package/dist/tools/repo/scanner.d.ts +0 -19
  86. package/dist/tools/repo/scanner.d.ts.map +0 -1
  87. package/dist/tools/repo/scanner.js +0 -181
  88. package/dist/tools/repo-scanner.d.ts +0 -19
  89. package/dist/tools/repo-scanner.d.ts.map +0 -1
  90. package/dist/tools/repo-scanner.js +0 -181
  91. package/dist/tools/scoring/gap-engine.d.ts +0 -8
  92. package/dist/tools/scoring/gap-engine.d.ts.map +0 -1
  93. package/dist/tools/scoring/gap-engine.js +0 -138
  94. package/dist/tools/user-providers.d.ts +0 -15
  95. package/dist/tools/user-providers.d.ts.map +0 -1
  96. package/dist/tools/user-providers.js +0 -62
@@ -1,219 +0,0 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { AutomationMaturitySchema } from '../schemas/automation-maturity.schema.js';
4
- /**
5
- * Dimension weights (sum = 1). Breadth + harness adoption dominate: shipping risk is mostly
6
- * untested routes and missing Playwright/Cypress-level coverage.
7
- */
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;
14
- function hasCiAtRoot(repoPath) {
15
- const ev = [];
16
- const gh = join(repoPath, '.github', 'workflows');
17
- if (existsSync(gh) && statSync(gh).isDirectory()) {
18
- try {
19
- const files = readdirSync(gh).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
20
- if (files.length > 0) {
21
- ev.push(`.github/workflows (${files.length} workflow file(s))`);
22
- return { ok: true, evidence: ev };
23
- }
24
- }
25
- catch {
26
- /* ignore */
27
- }
28
- }
29
- if (existsSync(join(repoPath, '.circleci'))) {
30
- ev.push('.circleci/ present');
31
- return { ok: true, evidence: ev };
32
- }
33
- for (const f of ['.gitlab-ci.yml', 'Jenkinsfile']) {
34
- if (existsSync(join(repoPath, f))) {
35
- ev.push(`${f} present`);
36
- return { ok: true, evidence: ev };
37
- }
38
- }
39
- return { ok: false, evidence: ['No GitHub Actions, CircleCI, GitLab CI, or Jenkinsfile detected at repo root'] };
40
- }
41
- function scoreLevel(overall) {
42
- if (overall < 20)
43
- return { level: 1, label: 'L1 — nascent automation' };
44
- if (overall < 40)
45
- return { level: 2, label: 'L2 — emerging coverage' };
46
- if (overall < 60)
47
- return { level: 3, label: 'L3 — building maturity' };
48
- if (overall < 80)
49
- return { level: 4, label: 'L4 — strong automation' };
50
- return { level: 5, label: 'L5 — advanced QA automation' };
51
- }
52
- export function computeAutomationMaturity(repo) {
53
- const routePaths = [...new Set(repo.routes.map((r) => r.path))];
54
- let coveredRoutes = 0;
55
- for (const p of routePaths) {
56
- const covered = repo.testFiles.some((tf) => tf.coveredPaths.some((c) => p === c || (c !== '/' && p.startsWith(c))));
57
- if (covered)
58
- coveredRoutes++;
59
- }
60
- const breadthScore = routePaths.length === 0 ? 100 : Math.round((100 * coveredRoutes) / routePaths.length);
61
- const breadthDim = {
62
- dimension: 'test-coverage-breadth',
63
- score: breadthScore,
64
- weight: W_TEST_BREADTH,
65
- evidence: routePaths.length === 0
66
- ? ['No static routes inferred from repo layout']
67
- : [
68
- `${coveredRoutes}/${routePaths.length} inferred routes appear in at least one test coveredPaths`,
69
- ],
70
- recommendations: breadthScore >= 80
71
- ? []
72
- : ['Add route-level smoke tests that assert critical paths referenced in production URLs.'],
73
- };
74
- const types = new Set(repo.testFiles.map((t) => t.type));
75
- let frameworkScore = 0;
76
- const fwEvidence = [`Test runners seen: ${[...types].join(', ') || 'none'}`];
77
- if (types.has('playwright') || types.has('cypress-e2e') || types.has('cypress-component')) {
78
- frameworkScore = 100;
79
- fwEvidence.push('Playwright or Cypress present — good browser harness signal.');
80
- }
81
- else if (types.has('jest') || types.has('vitest')) {
82
- frameworkScore = 55;
83
- fwEvidence.push('Jest/Vitest only — add Playwright or Cypress for deployment-facing checks.');
84
- }
85
- else if (repo.testFiles.length > 0) {
86
- frameworkScore = 30;
87
- fwEvidence.push('Tests exist but no recognized browser harness in scanned files.');
88
- }
89
- else {
90
- frameworkScore = 0;
91
- fwEvidence.push('No test files matched qulib scan globs.');
92
- }
93
- const frameworkDim = {
94
- dimension: 'framework-adoption',
95
- score: frameworkScore,
96
- weight: W_FRAMEWORK,
97
- evidence: fwEvidence,
98
- recommendations: frameworkScore >= 80 ? [] : ['Standardize on Playwright or Cypress for E2E against deployed URLs.'],
99
- };
100
- const missingIds = repo.missingTestIds.length;
101
- const interactiveTsxScanned = repo.interactiveTsxFilesScanned ?? missingIds;
102
- let hygieneScore = 0;
103
- let hygieneApplicability = 'applicable';
104
- let hygieneReason;
105
- const hygieneEvidence = [];
106
- if (interactiveTsxScanned === 0) {
107
- hygieneApplicability = 'unknown';
108
- hygieneReason = 'No interactive TSX files scanned — cannot compute a missing-id ratio honestly.';
109
- hygieneEvidence.push(hygieneReason);
110
- }
111
- else {
112
- const missingRatio = missingIds / interactiveTsxScanned;
113
- hygieneScore = Math.round(Math.max(0, 100 * (1 - missingRatio)));
114
- hygieneEvidence.push(`${missingIds}/${interactiveTsxScanned} interactive TSX file(s) lacked data-testid (heuristic scan).`);
115
- }
116
- const hygieneDim = {
117
- dimension: 'test-id-hygiene',
118
- score: hygieneScore,
119
- weight: W_TEST_ID,
120
- evidence: hygieneEvidence,
121
- recommendations: hygieneApplicability === 'applicable' && hygieneScore < 85
122
- ? ['Add stable data-testid (or role-based selectors) on interactive components used in tests.']
123
- : [],
124
- applicability: hygieneApplicability,
125
- ...(hygieneReason && { reason: hygieneReason }),
126
- };
127
- const ci = hasCiAtRoot(repo.repoPath);
128
- const ciDim = {
129
- dimension: 'ci-integration',
130
- score: ci.ok ? 100 : 0,
131
- weight: W_CI,
132
- evidence: ci.evidence,
133
- recommendations: ci.ok ? [] : ['Add a CI workflow that runs unit/E2E tests on every PR.'],
134
- };
135
- const authRe = /\/(login|auth|signin)(\/|$)/i;
136
- const authRouteFileRe = /(login|auth|signin)/i;
137
- const authCovered = repo.testFiles.some((tf) => tf.coveredPaths.some((c) => authRe.test(c)));
138
- const repoHasAuthRoute = repo.routes.some((r) => authRe.test(r.path));
139
- const repoHasAuthTestFile = repo.testFiles.some((tf) => authRouteFileRe.test(tf.file));
140
- const repoHasAnyAuthSignal = repoHasAuthRoute || repoHasAuthTestFile || authCovered;
141
- let authScore = 0;
142
- let authApplicability = 'applicable';
143
- let authReason;
144
- const authEvidence = [];
145
- if (!repoHasAnyAuthSignal) {
146
- authApplicability = 'not_applicable';
147
- authReason = 'No auth routes, auth-named test files, or auth path coverage detected — repo appears auth-free.';
148
- authEvidence.push(authReason);
149
- }
150
- else {
151
- authScore = authCovered ? 90 : 25;
152
- authEvidence.push(authCovered
153
- ? 'At least one test references /login, /auth, or /signin in coveredPaths.'
154
- : 'Repo has auth-shaped routes or test files but no auth-route coverage in extracted test path strings.');
155
- }
156
- const authDim = {
157
- dimension: 'auth-test-coverage',
158
- score: authScore,
159
- weight: W_AUTH_TESTS,
160
- evidence: authEvidence,
161
- recommendations: authApplicability === 'applicable' && !authCovered
162
- ? ['Add focused tests for sign-in and post-auth landing behavior.']
163
- : [],
164
- applicability: authApplicability,
165
- ...(authReason && { reason: authReason }),
166
- };
167
- const cypressE2e = repo.testFiles.filter((t) => t.type === 'cypress-e2e').length;
168
- const cypressComp = repo.testFiles.filter((t) => t.type === 'cypress-component').length;
169
- const cypressTotal = cypressE2e + cypressComp;
170
- let compRatioScore = 0;
171
- let compApplicability = 'applicable';
172
- let compReason;
173
- const compEvidence = [];
174
- if (cypressTotal === 0) {
175
- compApplicability = 'not_applicable';
176
- compReason = 'No Cypress (e2e or component) tests detected — component-test-ratio does not apply.';
177
- compEvidence.push(compReason);
178
- }
179
- else {
180
- compRatioScore = Math.round((100 * cypressComp) / cypressTotal);
181
- compEvidence.push(`Cypress e2e files (matched): ${cypressE2e}, component: ${cypressComp}.`);
182
- }
183
- const compDim = {
184
- dimension: 'component-test-ratio',
185
- score: compRatioScore,
186
- weight: W_COMPONENT_RATIO,
187
- evidence: compEvidence,
188
- recommendations: compApplicability === 'applicable' && cypressComp > 0
189
- ? ['Balance component vs E2E Cypress tests so critical flows stay fast in CI.']
190
- : [],
191
- applicability: compApplicability,
192
- ...(compReason && { reason: compReason }),
193
- };
194
- const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
195
- // Overall score normalizes over applicable dimensions only.
196
- // overallScore = round( Σ score_i * weight_i / Σ weight_i ) for i ∈ applicable.
197
- // If no dimension is applicable (degenerate repo), overall = 0 and level = L1.
198
- const applicableDims = dimensions.filter((d) => (d.applicability ?? 'applicable') === 'applicable');
199
- const weightSum = applicableDims.reduce((s, d) => s + d.weight, 0);
200
- const overallScore = weightSum > 0
201
- ? Math.round(applicableDims.reduce((s, d) => s + d.score * d.weight, 0) / weightSum)
202
- : 0;
203
- const { level, label } = scoreLevel(overallScore);
204
- const topRecommendations = [...applicableDims]
205
- .sort((a, b) => a.score - b.score)
206
- .flatMap((d) => d.recommendations)
207
- .filter(Boolean)
208
- .slice(0, 8);
209
- return AutomationMaturitySchema.parse({
210
- computedAt: new Date().toISOString(),
211
- repoPath: repo.repoPath,
212
- overallScore,
213
- level,
214
- label,
215
- dimensions,
216
- topRecommendations,
217
- scoreFormula: 'overallScore = round( Σ (score * weight) / Σ weight ) for applicable dimensions only. not_applicable and unknown dimensions are excluded from the denominator.',
218
- });
219
- }
@@ -1,3 +0,0 @@
1
- import { type Browser } from '@playwright/test';
2
- export declare function launchBrowser(): Promise<Browser>;
3
- //# sourceMappingURL=browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/tools/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE1D,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAYtD"}
@@ -1,13 +0,0 @@
1
- import { chromium } from '@playwright/test';
2
- export async function launchBrowser() {
3
- try {
4
- return await chromium.launch({ headless: true });
5
- }
6
- catch (err) {
7
- const message = err instanceof Error ? err.message : String(err);
8
- if (message.includes("Executable doesn't exist") || message.includes('chromium')) {
9
- throw new Error(`Playwright Chromium browser is not installed. Run:\n\n npx playwright install chromium\n\nThen retry your qulib command. This is a one-time setup step.`);
10
- }
11
- throw err;
12
- }
13
- }
@@ -1,8 +0,0 @@
1
- import type { AppExplorer } from './explorer.interface.js';
2
- import type { HarnessConfig } from '../schemas/config.schema.js';
3
- import type { RouteInventory } from '../schemas/route-inventory.schema.js';
4
- import type { RunArtifactsOptions } from '../harness/run-options.js';
5
- export declare class CypressExplorer implements AppExplorer {
6
- explore(_baseUrl: string, _config: HarnessConfig, _artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
7
- }
8
- //# sourceMappingURL=cypress-explorer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cypress-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/cypress-explorer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,qBAAa,eAAgB,YAAW,WAAW;IAC3C,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;CAGnH"}
@@ -1,5 +0,0 @@
1
- export class CypressExplorer {
2
- async explore(_baseUrl, _config, _artifacts) {
3
- throw new Error('Not implemented');
4
- }
5
- }
@@ -1,4 +0,0 @@
1
- import type { ExplorerType } from '../schemas/config.schema.js';
2
- import type { AppExplorer } from './explorer.interface.js';
3
- export declare function createExplorer(type: ExplorerType): AppExplorer;
4
- //# sourceMappingURL=explorer-factory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"explorer-factory.d.ts","sourceRoot":"","sources":["../../src/tools/explorer-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAS9D"}
@@ -1,12 +0,0 @@
1
- import { PlaywrightExplorer } from './playwright-explorer.js';
2
- import { CypressExplorer } from './cypress-explorer.js';
3
- export function createExplorer(type) {
4
- switch (type) {
5
- case 'playwright':
6
- return new PlaywrightExplorer();
7
- case 'cypress':
8
- return new CypressExplorer();
9
- default:
10
- throw new Error(`Unknown explorer type: ${type}`);
11
- }
12
- }
@@ -1,7 +0,0 @@
1
- import type { HarnessConfig } from '../schemas/config.schema.js';
2
- import type { RouteInventory } from '../schemas/route-inventory.schema.js';
3
- import type { RunArtifactsOptions } from '../harness/run-options.js';
4
- export interface AppExplorer {
5
- explore(baseUrl: string, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
6
- }
7
- //# sourceMappingURL=explorer.interface.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"explorer.interface.d.ts","sourceRoot":"","sources":["../../src/tools/explorer.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC3G"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,8 +0,0 @@
1
- import type { AppExplorer } from './explorer.interface.js';
2
- import type { HarnessConfig } from '../../schemas/config.schema.js';
3
- import type { RouteInventory } from '../../schemas/route-inventory.schema.js';
4
- import type { RunArtifactsOptions } from '../../harness/run-options.js';
5
- export declare class CypressExplorer implements AppExplorer {
6
- explore(_baseUrl: string, _config: HarnessConfig, _artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
7
- }
8
- //# sourceMappingURL=cypress-explorer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cypress-explorer.d.ts","sourceRoot":"","sources":["../../../src/tools/explorers/cypress-explorer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAExE,qBAAa,eAAgB,YAAW,WAAW;IAC3C,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;CAGnH"}
@@ -1,5 +0,0 @@
1
- export class CypressExplorer {
2
- async explore(_baseUrl, _config, _artifacts) {
3
- throw new Error('Not implemented');
4
- }
5
- }
@@ -1,7 +0,0 @@
1
- import type { HarnessConfig } from '../../schemas/config.schema.js';
2
- import type { RouteInventory } from '../../schemas/route-inventory.schema.js';
3
- import type { RunArtifactsOptions } from '../../harness/run-options.js';
4
- export interface AppExplorer {
5
- explore(baseUrl: string, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
6
- }
7
- //# sourceMappingURL=explorer.interface.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"explorer.interface.d.ts","sourceRoot":"","sources":["../../../src/tools/explorers/explorer.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC3G"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,8 +0,0 @@
1
- import type { AppExplorer } from './explorer.interface.js';
2
- import { type RouteInventory } from '../../schemas/route-inventory.schema.js';
3
- import type { HarnessConfig } from '../../schemas/config.schema.js';
4
- import type { RunArtifactsOptions } from '../../harness/run-options.js';
5
- export declare class PlaywrightExplorer implements AppExplorer {
6
- explore(baseUrl: string, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
7
- }
8
- //# sourceMappingURL=playwright-explorer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"playwright-explorer.d.ts","sourceRoot":"","sources":["../../../src/tools/explorers/playwright-explorer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAwB,KAAK,cAAc,EAAc,MAAM,yCAAyC,CAAC;AAChH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAoBxE,qBAAa,kBAAmB,YAAW,WAAW;IAC9C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;CAsKhH"}
@@ -1,172 +0,0 @@
1
- import { launchBrowser } from './browser.js';
2
- import { AxeBuilder } from '@axe-core/playwright';
3
- import { createAuthenticatedContext } from '../auth/apply.js';
4
- import { RouteInventorySchema } from '../../schemas/route-inventory.schema.js';
5
- function crawlHostKey(hostname) {
6
- return hostname.replace(/^www\./i, '').toLowerCase();
7
- }
8
- function isInternalHref(href, baseUrlStr) {
9
- try {
10
- const u = new URL(href);
11
- const base = new URL(baseUrlStr);
12
- return u.protocol === base.protocol && crawlHostKey(u.hostname) === crawlHostKey(base.hostname);
13
- }
14
- catch {
15
- return false;
16
- }
17
- }
18
- function debugMode() {
19
- return process.env.QULIB_DEBUG === '1';
20
- }
21
- export class PlaywrightExplorer {
22
- async explore(baseUrl, config, artifacts) {
23
- const progress = artifacts?.progressLog;
24
- const browser = await launchBrowser();
25
- let context;
26
- try {
27
- context = await createAuthenticatedContext(browser, config.auth, config.timeoutMs);
28
- }
29
- catch (err) {
30
- await browser.close();
31
- throw new Error(`Authentication failed: ${String(err)}. Check your auth config and credentials.`);
32
- }
33
- if (config.auth) {
34
- const label = config.auth.type === 'form-login' ? config.auth.credentials.username : 'storage-state';
35
- progress?.info(`Authenticated context: ${label}`);
36
- if (!progress) {
37
- process.stderr.write(`[qulib] authenticated as ${label}\n`);
38
- }
39
- }
40
- const visited = new Set();
41
- const queue = [baseUrl];
42
- const routes = [];
43
- let budgetExceeded = false;
44
- try {
45
- while (queue.length > 0) {
46
- if (visited.size >= config.maxPagesToScan) {
47
- budgetExceeded = queue.length > 0;
48
- break;
49
- }
50
- const url = queue.shift();
51
- if (!url) {
52
- continue;
53
- }
54
- const normalized = url.split('?')[0].split('#')[0];
55
- if (visited.has(normalized))
56
- continue;
57
- visited.add(normalized);
58
- const page = await context.newPage();
59
- const consoleErrors = [];
60
- page.on('console', (msg) => {
61
- if (msg.type() === 'error') {
62
- consoleErrors.push(msg.text());
63
- }
64
- });
65
- try {
66
- const navResponse = await page.goto(url, {
67
- timeout: config.timeoutMs,
68
- waitUntil: 'domcontentloaded',
69
- });
70
- const httpStatus = navResponse?.status() ?? 0;
71
- if (debugMode()) {
72
- const html = await page.content();
73
- progress?.debug(`page HTML byteLength=${Buffer.byteLength(html, 'utf8')} url=${normalized}`);
74
- }
75
- const pageTitle = await page.title();
76
- const formCount = await page.locator('form').count();
77
- const buttonLabels = await page.locator('button').allInnerTexts();
78
- const hrefs = await page.evaluate(() => Array.from(document.querySelectorAll('a[href]'))
79
- .map((a) => a.href)
80
- .filter(Boolean));
81
- const internalLinks = hrefs
82
- .filter((href) => isInternalHref(href, baseUrl))
83
- .map((href) => href.split('?')[0].split('#')[0]);
84
- const uniqueInternal = [...new Set(internalLinks)];
85
- for (const link of uniqueInternal) {
86
- if (!visited.has(link) && !queue.includes(link)) {
87
- queue.push(link);
88
- }
89
- }
90
- const brokenLinks = [];
91
- for (const link of uniqueInternal.slice(0, 20)) {
92
- try {
93
- const response = await page.request.head(link, { timeout: 5000 });
94
- if (response.status() >= 400) {
95
- brokenLinks.push({ url: link, status: response.status() });
96
- }
97
- }
98
- catch (err) {
99
- brokenLinks.push({ url: link, status: null, reason: String(err) });
100
- }
101
- }
102
- let a11yViolations = [];
103
- try {
104
- const axeResults = await new AxeBuilder({ page })
105
- .withTags(['wcag2a', 'wcag2aa'])
106
- .analyze();
107
- if (debugMode()) {
108
- progress?.debug(`raw axe violations (pre-map) count=${axeResults.violations.length} json=${JSON.stringify(axeResults.violations)}`);
109
- }
110
- a11yViolations = axeResults.violations.map((v) => ({
111
- id: v.id,
112
- impact: v.impact ?? 'unknown',
113
- helpUrl: v.helpUrl,
114
- nodeCount: v.nodes.length,
115
- }));
116
- }
117
- catch (err) {
118
- consoleErrors.push(`axe-core failure: ${String(err)}`);
119
- }
120
- const path = new URL(url).pathname || '/';
121
- progress?.info(`Crawled ${normalized} status=${httpStatus} a11yViolations=${a11yViolations.length}`);
122
- routes.push({
123
- path,
124
- pageTitle,
125
- links: uniqueInternal,
126
- formCount,
127
- buttonLabels: buttonLabels.map((b) => b.trim()).filter(Boolean),
128
- consoleErrors,
129
- brokenLinks,
130
- a11yViolations,
131
- statusCode: httpStatus,
132
- });
133
- }
134
- catch (err) {
135
- const path = (() => {
136
- try {
137
- return new URL(url).pathname || '/';
138
- }
139
- catch {
140
- return url;
141
- }
142
- })();
143
- progress?.info(`Crawled ${normalized} status=error a11yViolations=0 err=${String(err).slice(0, 120)}`);
144
- routes.push({
145
- path,
146
- pageTitle: '',
147
- links: [],
148
- formCount: 0,
149
- buttonLabels: [],
150
- consoleErrors: [`Navigation error: ${String(err)}`],
151
- brokenLinks: [],
152
- a11yViolations: [],
153
- });
154
- }
155
- finally {
156
- await page.close();
157
- }
158
- }
159
- }
160
- finally {
161
- await context.close();
162
- await browser.close();
163
- }
164
- return RouteInventorySchema.parse({
165
- scannedAt: new Date().toISOString(),
166
- baseUrl,
167
- routes,
168
- pagesSkipped: budgetExceeded ? queue.length : 0,
169
- budgetExceeded,
170
- });
171
- }
172
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * @module framework-detector
3
- * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
- *
5
- * Framework detection runs during the observe phase as part of repo scanning.
6
- * It is a pure static analysis operation with no browser or LLM dependency.
7
- * Move this to @qulib/analyzer when that package is created.
8
- *
9
- * // TODO(@qulib/analyzer): When @qulib/analyzer is extracted, this module should move there.
10
- * // It is currently embedded in @qulib/core because repo scanning is part of the observe phase.
11
- * // The package boundary decision: core = runtime QA analysis, analyzer = static repo intelligence.
12
- */
13
- import { type FrameworkDetectionResult } from '../schemas/repo-analysis.schema.js';
14
- export declare function detectFramework(repoPath: string): Promise<FrameworkDetectionResult>;
15
- //# sourceMappingURL=framework-detector.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"framework-detector.d.ts","sourceRoot":"","sources":["../../src/tools/framework-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAA4B,KAAK,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAuB7G,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA4GzF"}