@qulib/core 0.5.1 → 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 (97) hide show
  1. package/README.md +9 -9
  2. package/dist/cli/index.js +3 -3
  3. package/dist/llm/context-builder.js +2 -2
  4. package/dist/tools/auth/detect.d.ts +8 -0
  5. package/dist/tools/auth/detect.d.ts.map +1 -1
  6. package/dist/tools/auth/detect.js +46 -9
  7. package/package.json +2 -2
  8. package/dist/tools/apply-auth.d.ts +0 -4
  9. package/dist/tools/apply-auth.d.ts.map +0 -1
  10. package/dist/tools/apply-auth.js +0 -35
  11. package/dist/tools/auth/block-gap.d.ts +0 -9
  12. package/dist/tools/auth/block-gap.d.ts.map +0 -1
  13. package/dist/tools/auth/block-gap.js +0 -52
  14. package/dist/tools/auth/detector.d.ts +0 -23
  15. package/dist/tools/auth/detector.d.ts.map +0 -1
  16. package/dist/tools/auth/detector.js +0 -526
  17. package/dist/tools/auth/explorer.d.ts +0 -4
  18. package/dist/tools/auth/explorer.d.ts.map +0 -1
  19. package/dist/tools/auth/explorer.js +0 -346
  20. package/dist/tools/auth/oauth-providers.d.ts +0 -7
  21. package/dist/tools/auth/oauth-providers.d.ts.map +0 -1
  22. package/dist/tools/auth/oauth-providers.js +0 -21
  23. package/dist/tools/auth/surface-analyzer.d.ts +0 -4
  24. package/dist/tools/auth/surface-analyzer.d.ts.map +0 -1
  25. package/dist/tools/auth/surface-analyzer.js +0 -170
  26. package/dist/tools/auth/user-providers.d.ts +0 -15
  27. package/dist/tools/auth/user-providers.d.ts.map +0 -1
  28. package/dist/tools/auth/user-providers.js +0 -62
  29. package/dist/tools/auth-block-gap.d.ts +0 -9
  30. package/dist/tools/auth-block-gap.d.ts.map +0 -1
  31. package/dist/tools/auth-block-gap.js +0 -52
  32. package/dist/tools/auth-detector.d.ts +0 -23
  33. package/dist/tools/auth-detector.d.ts.map +0 -1
  34. package/dist/tools/auth-detector.js +0 -526
  35. package/dist/tools/auth-explorer.d.ts +0 -4
  36. package/dist/tools/auth-explorer.d.ts.map +0 -1
  37. package/dist/tools/auth-explorer.js +0 -346
  38. package/dist/tools/auth-surface-analyzer.d.ts +0 -4
  39. package/dist/tools/auth-surface-analyzer.d.ts.map +0 -1
  40. package/dist/tools/auth-surface-analyzer.js +0 -170
  41. package/dist/tools/auth.d.ts +0 -4
  42. package/dist/tools/auth.d.ts.map +0 -1
  43. package/dist/tools/auth.js +0 -35
  44. package/dist/tools/automation-maturity.d.ts +0 -4
  45. package/dist/tools/automation-maturity.d.ts.map +0 -1
  46. package/dist/tools/automation-maturity.js +0 -219
  47. package/dist/tools/browser.d.ts +0 -3
  48. package/dist/tools/browser.d.ts.map +0 -1
  49. package/dist/tools/browser.js +0 -13
  50. package/dist/tools/cypress-explorer.d.ts +0 -8
  51. package/dist/tools/cypress-explorer.d.ts.map +0 -1
  52. package/dist/tools/cypress-explorer.js +0 -5
  53. package/dist/tools/explorer-factory.d.ts +0 -4
  54. package/dist/tools/explorer-factory.d.ts.map +0 -1
  55. package/dist/tools/explorer-factory.js +0 -12
  56. package/dist/tools/explorer.interface.d.ts +0 -7
  57. package/dist/tools/explorer.interface.d.ts.map +0 -1
  58. package/dist/tools/explorer.interface.js +0 -1
  59. package/dist/tools/explorers/cypress-explorer.d.ts +0 -8
  60. package/dist/tools/explorers/cypress-explorer.d.ts.map +0 -1
  61. package/dist/tools/explorers/cypress-explorer.js +0 -5
  62. package/dist/tools/explorers/explorer.interface.d.ts +0 -7
  63. package/dist/tools/explorers/explorer.interface.d.ts.map +0 -1
  64. package/dist/tools/explorers/explorer.interface.js +0 -1
  65. package/dist/tools/explorers/playwright-explorer.d.ts +0 -8
  66. package/dist/tools/explorers/playwright-explorer.d.ts.map +0 -1
  67. package/dist/tools/explorers/playwright-explorer.js +0 -172
  68. package/dist/tools/framework-detector.d.ts +0 -15
  69. package/dist/tools/framework-detector.d.ts.map +0 -1
  70. package/dist/tools/framework-detector.js +0 -153
  71. package/dist/tools/gap-engine.d.ts +0 -8
  72. package/dist/tools/gap-engine.d.ts.map +0 -1
  73. package/dist/tools/gap-engine.js +0 -138
  74. package/dist/tools/oauth-providers.d.ts +0 -7
  75. package/dist/tools/oauth-providers.d.ts.map +0 -1
  76. package/dist/tools/oauth-providers.js +0 -21
  77. package/dist/tools/playwright-explorer.d.ts +0 -8
  78. package/dist/tools/playwright-explorer.d.ts.map +0 -1
  79. package/dist/tools/playwright-explorer.js +0 -172
  80. package/dist/tools/public-surface.d.ts +0 -5
  81. package/dist/tools/public-surface.d.ts.map +0 -1
  82. package/dist/tools/public-surface.js +0 -13
  83. package/dist/tools/repo/framework-detector.d.ts +0 -15
  84. package/dist/tools/repo/framework-detector.d.ts.map +0 -1
  85. package/dist/tools/repo/framework-detector.js +0 -153
  86. package/dist/tools/repo/scanner.d.ts +0 -19
  87. package/dist/tools/repo/scanner.d.ts.map +0 -1
  88. package/dist/tools/repo/scanner.js +0 -181
  89. package/dist/tools/repo-scanner.d.ts +0 -19
  90. package/dist/tools/repo-scanner.d.ts.map +0 -1
  91. package/dist/tools/repo-scanner.js +0 -181
  92. package/dist/tools/scoring/gap-engine.d.ts +0 -8
  93. package/dist/tools/scoring/gap-engine.d.ts.map +0 -1
  94. package/dist/tools/scoring/gap-engine.js +0 -138
  95. package/dist/tools/user-providers.d.ts +0 -15
  96. package/dist/tools/user-providers.d.ts.map +0 -1
  97. package/dist/tools/user-providers.js +0 -62
@@ -1,153 +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 { access, readFile } from 'node:fs/promises';
14
- import { constants } from 'node:fs';
15
- import { join } from 'node:path';
16
- import { FrameworkDetectionSchema } from '../schemas/repo-analysis.schema.js';
17
- async function fileExists(repoPath, rel) {
18
- try {
19
- await access(join(repoPath, rel), constants.F_OK);
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
25
- }
26
- function depNames(pkg) {
27
- return new Set([
28
- ...Object.keys(pkg.dependencies ?? {}),
29
- ...Object.keys(pkg.devDependencies ?? {}),
30
- ]);
31
- }
32
- export async function detectFramework(repoPath) {
33
- const evidence = [];
34
- const testFrameworks = new Set();
35
- let pkg = {};
36
- try {
37
- const raw = await readFile(join(repoPath, 'package.json'), 'utf8');
38
- pkg = JSON.parse(raw);
39
- evidence.push('read package.json');
40
- }
41
- catch {
42
- evidence.push('package.json missing or unreadable');
43
- }
44
- const deps = depNames(pkg);
45
- const has = (n) => deps.has(n);
46
- if (has('@playwright/test') || has('playwright')) {
47
- testFrameworks.add('playwright');
48
- evidence.push('dependency: @playwright/test or playwright');
49
- }
50
- if (has('cypress')) {
51
- testFrameworks.add('cypress-e2e');
52
- evidence.push('dependency: cypress');
53
- }
54
- if (has('jest')) {
55
- testFrameworks.add('jest');
56
- evidence.push('dependency: jest');
57
- }
58
- if (has('vitest')) {
59
- testFrameworks.add('vitest');
60
- evidence.push('dependency: vitest');
61
- }
62
- if (testFrameworks.size === 0) {
63
- testFrameworks.add('other');
64
- }
65
- const nextCfg = (await fileExists(repoPath, 'next.config.js')) ||
66
- (await fileExists(repoPath, 'next.config.mjs')) ||
67
- (await fileExists(repoPath, 'next.config.ts'));
68
- const nuxtCfg = await fileExists(repoPath, 'nuxt.config.ts');
69
- const svelteCfg = await fileExists(repoPath, 'svelte.config.js');
70
- const astroCfg = await fileExists(repoPath, 'astro.config.mjs');
71
- const remixCfg = await fileExists(repoPath, 'remix.config.js');
72
- const viteCfg = await fileExists(repoPath, 'vite.config.ts');
73
- if (nextCfg)
74
- evidence.push('found next.config.*');
75
- if (nuxtCfg)
76
- evidence.push('found nuxt.config.ts');
77
- if (svelteCfg)
78
- evidence.push('found svelte.config.js');
79
- if (astroCfg)
80
- evidence.push('found astro.config.mjs');
81
- if (remixCfg)
82
- evidence.push('found remix.config.js');
83
- if (viteCfg)
84
- evidence.push('found vite.config.ts');
85
- const hasAppDir = await fileExists(repoPath, 'app');
86
- const hasPagesDir = await fileExists(repoPath, 'pages');
87
- if (has('next') && hasAppDir)
88
- evidence.push('Next.js app/ directory present');
89
- if (has('next') && hasPagesDir)
90
- evidence.push('Next.js pages/ directory present');
91
- if (has('@remix-run/react') || has('@remix-run/node'))
92
- evidence.push('Remix packages in package.json');
93
- if (has('nuxt') || has('nuxt3'))
94
- evidence.push('Nuxt in package.json');
95
- if (has('@sveltejs/kit'))
96
- evidence.push('@sveltejs/kit in package.json');
97
- if (has('astro'))
98
- evidence.push('astro in package.json');
99
- if (has('vite') && !has('next'))
100
- evidence.push('vite in package.json (non-Next)');
101
- let primary = 'unknown';
102
- let confidence = 'low';
103
- if (has('next')) {
104
- if (hasAppDir && (await fileExists(repoPath, join('app', 'layout.tsx')))) {
105
- primary = 'nextjs-app-router';
106
- confidence = nextCfg || hasAppDir ? 'high' : 'medium';
107
- }
108
- else if (hasPagesDir) {
109
- primary = 'nextjs-pages-router';
110
- confidence = nextCfg || hasPagesDir ? 'high' : 'medium';
111
- }
112
- else {
113
- primary = 'nextjs-app-router';
114
- confidence = 'medium';
115
- evidence.push('next detected without clear app/ vs pages/ layout');
116
- }
117
- }
118
- else if (has('@remix-run/react') || remixCfg) {
119
- primary = 'remix';
120
- confidence = remixCfg ? 'high' : 'medium';
121
- }
122
- else if (has('nuxt') || nuxtCfg) {
123
- primary = 'nuxt';
124
- confidence = nuxtCfg ? 'high' : 'medium';
125
- }
126
- else if (has('@sveltejs/kit') || svelteCfg) {
127
- primary = 'sveltekit';
128
- confidence = svelteCfg ? 'high' : 'medium';
129
- }
130
- else if (has('astro') || astroCfg) {
131
- primary = 'astro';
132
- confidence = astroCfg ? 'high' : 'medium';
133
- }
134
- else if (viteCfg && !has('next')) {
135
- primary = 'vite';
136
- confidence = 'medium';
137
- }
138
- else if (has('express')) {
139
- primary = 'express';
140
- confidence = 'medium';
141
- evidence.push('express listed in dependencies');
142
- }
143
- else {
144
- /* keep unknown */
145
- }
146
- const raw = {
147
- primary,
148
- confidence,
149
- evidence,
150
- testFrameworks: [...testFrameworks],
151
- };
152
- return FrameworkDetectionSchema.parse(raw);
153
- }
@@ -1,8 +0,0 @@
1
- import { type GapAnalysis, type Gap } from '../schemas/gap-analysis.schema.js';
2
- import type { RouteInventory } from '../schemas/route-inventory.schema.js';
3
- import type { RepoAnalysis } from '../schemas/repo-analysis.schema.js';
4
- import type { HarnessConfig } from '../schemas/config.schema.js';
5
- export declare function computeQualityScoreFromGaps(gaps: Gap[], scoringWeights?: HarnessConfig['scoringWeights']): number;
6
- export declare function computeCoverageScore(routes: RouteInventory): number | null;
7
- export declare function analyzeGaps(routes: RouteInventory, repo: RepoAnalysis | null, mode: 'url-only' | 'url-repo', config: HarnessConfig): Omit<GapAnalysis, 'scenarios' | 'generatedTests'>;
8
- //# sourceMappingURL=gap-engine.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gap-engine.d.ts","sourceRoot":"","sources":["../../src/tools/gap-engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAQjE,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,GAAG,EAAE,EACX,cAAc,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAC/C,MAAM,CAkBR;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAa1E;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,GAAG,IAAI,EACzB,IAAI,EAAE,UAAU,GAAG,UAAU,EAC7B,MAAM,EAAE,aAAa,GACpB,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAqGnD"}
@@ -1,138 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { GapSchema } from '../schemas/gap-analysis.schema.js';
3
- // TODO: Add category-specific weight overrides (e.g., auth-surface gaps should cost more than untested-route gaps by default).
4
- // Requires a 2D weight matrix: { [severity]: { [category]: number } }.
5
- // Deferred until there is empirical data from real scan runs to calibrate.
6
- const DEFAULT_SCORING_WEIGHTS = { critical: 25, high: 20, medium: 8, low: 3 };
7
- export function computeQualityScoreFromGaps(gaps, scoringWeights) {
8
- let critical = 0;
9
- let high = 0;
10
- let medium = 0;
11
- let low = 0;
12
- for (const g of gaps) {
13
- if (g.severity === 'critical')
14
- critical++;
15
- else if (g.severity === 'high')
16
- high++;
17
- else if (g.severity === 'medium')
18
- medium++;
19
- else
20
- low++;
21
- }
22
- const w = {
23
- critical: scoringWeights?.critical ?? DEFAULT_SCORING_WEIGHTS.critical,
24
- high: scoringWeights?.high ?? DEFAULT_SCORING_WEIGHTS.high,
25
- medium: scoringWeights?.medium ?? DEFAULT_SCORING_WEIGHTS.medium,
26
- low: scoringWeights?.low ?? DEFAULT_SCORING_WEIGHTS.low,
27
- };
28
- return Math.max(0, 100 - critical * w.critical - high * w.high - medium * w.medium - low * w.low);
29
- }
30
- export function computeCoverageScore(routes) {
31
- const scanned = routes.routes.length;
32
- const skipped = routes.pagesSkipped;
33
- const denom = scanned + skipped;
34
- // TODO: return null here once the explorer exposes an explicit "discovered-but-unknown" signal
35
- // (i.e. routes were found but the full set couldn't be confirmed — a low score is misleading)
36
- if (denom === 0) {
37
- if (routes.budgetExceeded) {
38
- return 0;
39
- }
40
- return scanned === 0 ? 0 : 100;
41
- }
42
- return Math.round((100 * scanned) / denom);
43
- }
44
- export function analyzeGaps(routes, repo, mode, config) {
45
- const coveredPaths = new Set();
46
- if (repo) {
47
- for (const testFile of repo.testFiles) {
48
- for (const path of testFile.coveredPaths) {
49
- coveredPaths.add(path);
50
- }
51
- }
52
- }
53
- const gaps = [];
54
- const addGap = (gap) => {
55
- const validated = GapSchema.parse(gap);
56
- gaps.push(validated);
57
- };
58
- let hasNavigationFailures = false;
59
- for (const route of routes.routes) {
60
- if (repo && !coveredPaths.has(route.path)) {
61
- const highRisk = /checkout|payment|auth|login|order/i.test(route.path);
62
- addGap({
63
- id: randomUUID(),
64
- path: route.path,
65
- severity: highRisk ? 'high' : 'medium',
66
- reason: `Route is not covered by existing tests: ${route.path}`,
67
- category: 'untested-route',
68
- });
69
- }
70
- const navErrors = route.consoleErrors.filter((e) => e.startsWith('Navigation error:'));
71
- if (navErrors.length > 0) {
72
- hasNavigationFailures = true;
73
- addGap({
74
- id: randomUUID(),
75
- path: route.path,
76
- severity: 'high',
77
- reason: `Navigation failed: ${navErrors.join('; ')}`,
78
- category: 'console-error',
79
- });
80
- }
81
- else if (route.consoleErrors.length > 0) {
82
- addGap({
83
- id: randomUUID(),
84
- path: route.path,
85
- severity: 'high',
86
- reason: `Console errors detected (${route.consoleErrors.length})`,
87
- category: 'console-error',
88
- });
89
- }
90
- if (route.brokenLinks.length > 0) {
91
- addGap({
92
- id: randomUUID(),
93
- path: route.path,
94
- severity: 'medium',
95
- reason: `Broken or invalid links detected (${route.brokenLinks.length})`,
96
- category: 'broken-link',
97
- });
98
- }
99
- for (const violation of route.a11yViolations) {
100
- const impact = violation.impact.toLowerCase();
101
- const severity = impact === 'critical'
102
- ? 'critical'
103
- : impact === 'serious'
104
- ? 'high'
105
- : impact === 'moderate'
106
- ? 'medium'
107
- : 'low';
108
- addGap({
109
- id: randomUUID(),
110
- path: route.path,
111
- severity,
112
- reason: `A11y violation ${violation.id} (${violation.impact}): ${violation.helpUrl}`,
113
- category: 'a11y',
114
- });
115
- }
116
- }
117
- const releaseConfidence = computeQualityScoreFromGaps(gaps, config.scoringWeights);
118
- const pagesScanned = routes.routes.length;
119
- let coverageWarning;
120
- if (routes.budgetExceeded) {
121
- coverageWarning = 'budget-exceeded';
122
- }
123
- else if (hasNavigationFailures) {
124
- coverageWarning = 'navigation-failures';
125
- }
126
- else if (pagesScanned < config.minPagesForConfidence) {
127
- coverageWarning = 'low-coverage';
128
- }
129
- return {
130
- analyzedAt: new Date().toISOString(),
131
- mode,
132
- releaseConfidence,
133
- coveragePagesScanned: pagesScanned,
134
- coverageBudgetExceeded: routes.budgetExceeded,
135
- coverageWarning,
136
- gaps,
137
- };
138
- }
@@ -1,7 +0,0 @@
1
- export interface OAuthProvider {
2
- id: string;
3
- label: string;
4
- patterns: RegExp[];
5
- }
6
- export declare const BUILT_IN_OAUTH_PROVIDERS: OAuthProvider[];
7
- //# sourceMappingURL=oauth-providers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oauth-providers.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-providers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,wBAAwB,EAAE,aAAa,EAsBnD,CAAC"}
@@ -1,21 +0,0 @@
1
- export const BUILT_IN_OAUTH_PROVIDERS = [
2
- { id: 'github', label: 'GitHub', patterns: [/\bgithub\b/i] },
3
- { id: 'google', label: 'Google', patterns: [/\bgoogle\b/i, /accounts\.google\.com/i] },
4
- { id: 'microsoft', label: 'Microsoft', patterns: [/microsoft/i, /login\.microsoftonline\.com/i] },
5
- { id: 'apple', label: 'Apple', patterns: [/sign in with apple/i, /\bapple id\b/i] },
6
- { id: 'facebook', label: 'Facebook', patterns: [/facebook/i] },
7
- { id: 'twitter', label: 'Twitter/X', patterns: [/twitter\.com/i, /\bsign in with x\b/i] },
8
- { id: 'linkedin', label: 'LinkedIn', patterns: [/linkedin/i] },
9
- { id: 'auth0', label: 'Auth0', patterns: [/auth0/i] },
10
- { id: 'okta', label: 'Okta', patterns: [/\bokta\b/i] },
11
- { id: 'onelogin', label: 'OneLogin', patterns: [/onelogin/i] },
12
- { id: 'duo', label: 'Duo Security', patterns: [/duo security/i] },
13
- { id: 'ping', label: 'Ping Identity', patterns: [/pingidentity/i, /pingone/i] },
14
- { id: 'workday', label: 'Workday', patterns: [/workday/i] },
15
- { id: 'saml', label: 'SAML SSO', patterns: [/\bsaml\b/i] },
16
- { id: 'clever', label: 'Clever', patterns: [/\bclever\b/i, /clever\.com/i] },
17
- { id: 'classlink', label: 'ClassLink', patterns: [/classlink/i] },
18
- { id: 'schoology', label: 'Schoology', patterns: [/schoology/i] },
19
- { id: 'canvas', label: 'Canvas (Instructure)', patterns: [/\bcanvas lms\b/i, /instructure/i] },
20
- { id: 'blackboard', label: 'Blackboard', patterns: [/blackboard/i] },
21
- ];
@@ -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/playwright-explorer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAwB,KAAK,cAAc,EAAc,MAAM,sCAAsC,CAAC;AAC7G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAoBrE,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 './apply-auth.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,5 +0,0 @@
1
- import type { RouteInventory } from '../schemas/route-inventory.schema.js';
2
- import type { Gap } from '../schemas/gap-analysis.schema.js';
3
- import type { PublicSurface } from '../schemas/public-surface.schema.js';
4
- export declare function buildPublicSurface(pages: RouteInventory['routes'], gaps: Gap[]): PublicSurface;
5
- //# sourceMappingURL=public-surface.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"public-surface.d.ts","sourceRoot":"","sources":["../../src/tools/public-surface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAmD,MAAM,qCAAqC,CAAC;AAE1H,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,aAAa,CAY9F"}
@@ -1,13 +0,0 @@
1
- export function buildPublicSurface(pages, gaps) {
2
- const accessibilityViolations = [];
3
- const brokenLinks = [];
4
- for (const r of pages) {
5
- for (const v of r.a11yViolations) {
6
- accessibilityViolations.push({ ...v, path: r.path });
7
- }
8
- for (const b of r.brokenLinks) {
9
- brokenLinks.push({ ...b, path: r.path });
10
- }
11
- }
12
- return { pages, gaps, accessibilityViolations, brokenLinks };
13
- }
@@ -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/repo/framework-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAA4B,KAAK,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAuBhH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA4GzF"}