@qulib/core 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +31 -3
  2. package/dist/analyze.d.ts +16 -4
  3. package/dist/analyze.d.ts.map +1 -1
  4. package/dist/analyze.js +98 -38
  5. package/dist/cli/cost-doctor.d.ts +2 -0
  6. package/dist/cli/cost-doctor.d.ts.map +1 -0
  7. package/dist/cli/cost-doctor.js +72 -0
  8. package/dist/cli/index.js +14 -0
  9. package/dist/harness/progress-log.d.ts +7 -0
  10. package/dist/harness/progress-log.d.ts.map +1 -0
  11. package/dist/harness/progress-log.js +1 -0
  12. package/dist/harness/run-options.d.ts +2 -0
  13. package/dist/harness/run-options.d.ts.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/llm/content-hash.d.ts +2 -0
  18. package/dist/llm/content-hash.d.ts.map +1 -0
  19. package/dist/llm/content-hash.js +4 -0
  20. package/dist/llm/context-builder.js +1 -1
  21. package/dist/llm/cost-intelligence.d.ts +29 -0
  22. package/dist/llm/cost-intelligence.d.ts.map +1 -0
  23. package/dist/llm/cost-intelligence.js +153 -0
  24. package/dist/llm/provider.d.ts +11 -1
  25. package/dist/llm/provider.d.ts.map +1 -1
  26. package/dist/llm/provider.js +43 -4
  27. package/dist/phases/act.d.ts.map +1 -1
  28. package/dist/phases/act.js +4 -1
  29. package/dist/phases/observe.js +1 -1
  30. package/dist/phases/think-finalize.d.ts +6 -0
  31. package/dist/phases/think-finalize.d.ts.map +1 -0
  32. package/dist/phases/think-finalize.js +164 -0
  33. package/dist/phases/think.d.ts +2 -0
  34. package/dist/phases/think.d.ts.map +1 -1
  35. package/dist/phases/think.js +16 -65
  36. package/dist/reporters/markdown-reporter.d.ts.map +1 -1
  37. package/dist/reporters/markdown-reporter.js +23 -3
  38. package/dist/schemas/config.schema.d.ts +7 -0
  39. package/dist/schemas/config.schema.d.ts.map +1 -1
  40. package/dist/schemas/config.schema.js +18 -1
  41. package/dist/schemas/cost-intelligence.schema.d.ts +229 -0
  42. package/dist/schemas/cost-intelligence.schema.d.ts.map +1 -0
  43. package/dist/schemas/cost-intelligence.schema.js +41 -0
  44. package/dist/schemas/decision-log.schema.d.ts +2 -2
  45. package/dist/schemas/gap-analysis.schema.d.ts +270 -31
  46. package/dist/schemas/gap-analysis.schema.d.ts.map +1 -1
  47. package/dist/schemas/gap-analysis.schema.js +7 -3
  48. package/dist/schemas/index.d.ts +3 -1
  49. package/dist/schemas/index.d.ts.map +1 -1
  50. package/dist/schemas/index.js +3 -1
  51. package/dist/schemas/public-surface.schema.d.ts +268 -0
  52. package/dist/schemas/public-surface.schema.d.ts.map +1 -0
  53. package/dist/schemas/public-surface.schema.js +15 -0
  54. package/dist/tools/auth-block-gap.d.ts +3 -0
  55. package/dist/tools/auth-block-gap.d.ts.map +1 -0
  56. package/dist/tools/auth-block-gap.js +19 -0
  57. package/dist/tools/auth-detector.d.ts +2 -1
  58. package/dist/tools/auth-detector.d.ts.map +1 -1
  59. package/dist/tools/auth-detector.js +28 -3
  60. package/dist/tools/auth-explorer.d.ts +2 -1
  61. package/dist/tools/auth-explorer.d.ts.map +1 -1
  62. package/dist/tools/auth-explorer.js +30 -8
  63. package/dist/tools/auth-surface-analyzer.d.ts +4 -0
  64. package/dist/tools/auth-surface-analyzer.d.ts.map +1 -0
  65. package/dist/tools/auth-surface-analyzer.js +154 -0
  66. package/dist/tools/cypress-explorer.d.ts +2 -1
  67. package/dist/tools/cypress-explorer.d.ts.map +1 -1
  68. package/dist/tools/cypress-explorer.js +1 -1
  69. package/dist/tools/explorer.interface.d.ts +2 -1
  70. package/dist/tools/explorer.interface.d.ts.map +1 -1
  71. package/dist/tools/gap-engine.d.ts +3 -1
  72. package/dist/tools/gap-engine.d.ts.map +1 -1
  73. package/dist/tools/gap-engine.js +39 -12
  74. package/dist/tools/playwright-explorer.d.ts +2 -1
  75. package/dist/tools/playwright-explorer.d.ts.map +1 -1
  76. package/dist/tools/playwright-explorer.js +21 -3
  77. package/dist/tools/public-surface.d.ts +5 -0
  78. package/dist/tools/public-surface.d.ts.map +1 -0
  79. package/dist/tools/public-surface.js +13 -0
  80. package/package.json +6 -2
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { launchBrowser } from './browser.js';
3
+ async function waitNetworkIdleBestEffort(page) {
4
+ try {
5
+ await page.waitForLoadState('networkidle', { timeout: 5000 });
6
+ }
7
+ catch {
8
+ /* best-effort */
9
+ }
10
+ }
11
+ export async function analyzeAuthSurfaceGaps(url, detection, timeoutMs) {
12
+ if (!detection.hasAuth) {
13
+ return [];
14
+ }
15
+ const gaps = [];
16
+ const browser = await launchBrowser();
17
+ try {
18
+ const context = await browser.newContext();
19
+ const page = await context.newPage();
20
+ const resp = await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' }).catch(() => null);
21
+ if (!resp || !resp.ok()) {
22
+ gaps.push({
23
+ id: randomUUID(),
24
+ path: new URL(url).pathname || '/',
25
+ severity: 'critical',
26
+ category: 'auth-surface',
27
+ reason: 'Sign-in surface did not load successfully for evaluation.',
28
+ description: 'The auth entry URL failed to load or returned a non-OK status before DOM checks could run.',
29
+ recommendation: 'Verify DNS, TLS, and that the URL is reachable from the scan environment.',
30
+ });
31
+ return gaps;
32
+ }
33
+ await waitNetworkIdleBestEffort(page);
34
+ const title = await page.title().catch(() => '');
35
+ if (!title || title.trim().length < 3) {
36
+ gaps.push({
37
+ id: randomUUID(),
38
+ path: '/',
39
+ severity: 'medium',
40
+ category: 'auth-surface',
41
+ reason: 'Missing or trivial document title on the sign-in surface.',
42
+ description: 'Users and assistive tech rely on a meaningful window title.',
43
+ recommendation: 'Set a concise, unique <title> for the login experience.',
44
+ });
45
+ }
46
+ const metaDesc = await page.locator('meta[name="description"]').getAttribute('content').catch(() => null);
47
+ if (!metaDesc || metaDesc.trim().length < 8) {
48
+ gaps.push({
49
+ id: randomUUID(),
50
+ path: '/',
51
+ severity: 'low',
52
+ category: 'auth-surface',
53
+ reason: 'No meta description on the sign-in surface.',
54
+ description: 'Search and sharing previews benefit from meta description on public entry pages.',
55
+ recommendation: 'Add <meta name="description" content="..."> with a short summary of the product.',
56
+ });
57
+ }
58
+ const h1Count = await page.locator('h1').count();
59
+ if (h1Count === 0) {
60
+ gaps.push({
61
+ id: randomUUID(),
62
+ path: '/',
63
+ severity: 'medium',
64
+ category: 'auth-surface',
65
+ reason: 'No visible primary heading (h1) on the sign-in surface.',
66
+ description: 'A primary heading helps users orient on the login page.',
67
+ recommendation: 'Add a single descriptive <h1> for the sign-in view.',
68
+ });
69
+ }
70
+ const oauthButtons = page.locator('button, a[href], [role="button"]');
71
+ const n = await oauthButtons.count();
72
+ for (let i = 0; i < Math.min(n, 25); i++) {
73
+ const el = oauthButtons.nth(i);
74
+ const text = ((await el.textContent()) ?? '').trim();
75
+ if (!text || text.length > 120)
76
+ continue;
77
+ const isOAuthish = /google|microsoft|github|apple|sso|sign in with|log in with|continue with|oauth/i.test(text);
78
+ if (!isOAuthish)
79
+ continue;
80
+ const role = await el.getAttribute('role');
81
+ const tag = await el.evaluate((node) => node.tagName.toLowerCase());
82
+ const tabIndex = await el.getAttribute('tabindex');
83
+ const aria = await el.getAttribute('aria-label');
84
+ const keyboardable = tag === 'button' || tag === 'a' || role === 'button';
85
+ const labeled = Boolean(aria && aria.trim().length > 0) || text.length > 0;
86
+ if (!keyboardable || tabIndex === '-1') {
87
+ gaps.push({
88
+ id: randomUUID(),
89
+ path: '/',
90
+ severity: 'high',
91
+ category: 'auth-surface',
92
+ reason: `OAuth control "${text.slice(0, 60)}" may not be keyboard-accessible.`,
93
+ description: 'SSO entry points should be real buttons or links with focus support.',
94
+ recommendation: 'Use <button> or <a href> with visible label; avoid tabindex=-1 on the only sign-in path.',
95
+ });
96
+ }
97
+ else if (!labeled) {
98
+ gaps.push({
99
+ id: randomUUID(),
100
+ path: '/',
101
+ severity: 'medium',
102
+ category: 'auth-surface',
103
+ reason: `OAuth control "${text.slice(0, 60)}" lacks aria-label and has weak visible text.`,
104
+ description: 'Assistive technologies need a clear accessible name for IdP buttons.',
105
+ recommendation: 'Add aria-label or visible text that names the provider and action.',
106
+ });
107
+ }
108
+ }
109
+ const hasPassword = (await page.locator('input[type="password"]').count()) > 0;
110
+ const hasEmailLink = await page.getByText(/magic link|email.*link|passwordless/i).count();
111
+ const hasOAuthUi = detection.oauthButtons.length > 0 ||
112
+ (await page.getByText(/sign in with|continue with google|microsoft|github/i).count()) > 0;
113
+ if (hasOAuthUi && !hasPassword && !hasEmailLink) {
114
+ gaps.push({
115
+ id: randomUUID(),
116
+ path: '/',
117
+ severity: 'medium',
118
+ category: 'auth-surface',
119
+ reason: 'OAuth-only entry with no visible password or magic-link fallback.',
120
+ description: 'Users who cannot use a social IdP need another path (email/password, help, or support).',
121
+ recommendation: 'Add a documented fallback (email/password, help desk link, or alternate IdP).',
122
+ });
123
+ }
124
+ const errorSelectors = '[role="alert"], [data-testid*="error" i], .error, .alert-danger, [class*="error" i]';
125
+ const errCount = await page.locator(errorSelectors).count();
126
+ if (errCount === 0 && hasOAuthUi) {
127
+ gaps.push({
128
+ id: randomUUID(),
129
+ path: '/',
130
+ severity: 'low',
131
+ category: 'auth-surface',
132
+ reason: 'No obvious in-DOM error container found for OAuth sign-in failures.',
133
+ description: 'IdP failures should surface recoverable feedback in the page.',
134
+ recommendation: 'Reserve a live region or inline alert for OAuth errors returned from the provider.',
135
+ });
136
+ }
137
+ const help = await page.getByText(/forgot password|need help|contact support|get help/i).count();
138
+ if (help === 0 && hasOAuthUi) {
139
+ gaps.push({
140
+ id: randomUUID(),
141
+ path: '/',
142
+ severity: 'low',
143
+ category: 'auth-surface',
144
+ reason: 'No visible “forgot password” or help path detected near OAuth controls.',
145
+ description: 'Users locked out of an IdP need a support or recovery affordance.',
146
+ recommendation: 'Link to account recovery, IT help, or a support URL near the sign-in actions.',
147
+ });
148
+ }
149
+ }
150
+ finally {
151
+ await browser.close();
152
+ }
153
+ return gaps;
154
+ }
@@ -1,7 +1,8 @@
1
1
  import type { AppExplorer } from './explorer.interface.js';
2
2
  import type { HarnessConfig } from '../schemas/config.schema.js';
3
3
  import type { RouteInventory } from '../schemas/route-inventory.schema.js';
4
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
4
5
  export declare class CypressExplorer implements AppExplorer {
5
- explore(baseUrl: string, config: HarnessConfig): Promise<RouteInventory>;
6
+ explore(_baseUrl: string, _config: HarnessConfig, _artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
6
7
  }
7
8
  //# sourceMappingURL=cypress-explorer.d.ts.map
@@ -1 +1 @@
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;AAE3E,qBAAa,eAAgB,YAAW,WAAW;IAC3C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;CAG/E"}
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 +1,5 @@
1
1
  export class CypressExplorer {
2
- async explore(baseUrl, config) {
2
+ async explore(_baseUrl, _config, _artifacts) {
3
3
  throw new Error('Not implemented');
4
4
  }
5
5
  }
@@ -1,6 +1,7 @@
1
1
  import type { HarnessConfig } from '../schemas/config.schema.js';
2
2
  import type { RouteInventory } from '../schemas/route-inventory.schema.js';
3
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
3
4
  export interface AppExplorer {
4
- explore(baseUrl: string, config: HarnessConfig): Promise<RouteInventory>;
5
+ explore(baseUrl: string, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
5
6
  }
6
7
  //# sourceMappingURL=explorer.interface.d.ts.map
@@ -1 +1 @@
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;AAE3E,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1E"}
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,6 +1,8 @@
1
- import { type GapAnalysis } from '../schemas/gap-analysis.schema.js';
1
+ import { type GapAnalysis, type Gap } from '../schemas/gap-analysis.schema.js';
2
2
  import type { RouteInventory } from '../schemas/route-inventory.schema.js';
3
3
  import type { RepoAnalysis } from '../schemas/repo-analysis.schema.js';
4
4
  import type { HarnessConfig } from '../schemas/config.schema.js';
5
+ export declare function computeQualityScoreFromGaps(gaps: Gap[]): number;
6
+ export declare function computeCoverageScore(routes: RouteInventory): number | null;
5
7
  export declare function analyzeGaps(routes: RouteInventory, repo: RepoAnalysis | null, mode: 'url-only' | 'url-repo', config: HarnessConfig): Omit<GapAnalysis, 'scenarios' | 'generatedTests'>;
6
8
  //# sourceMappingURL=gap-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gap-engine.d.ts","sourceRoot":"","sources":["../../src/tools/gap-engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAY,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;AAEjE,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,CA0GnD"}
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;AAEjE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAY/D;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,5 +1,36 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { GapSchema } from '../schemas/gap-analysis.schema.js';
3
+ export function computeQualityScoreFromGaps(gaps) {
4
+ let critical = 0;
5
+ let high = 0;
6
+ let medium = 0;
7
+ let low = 0;
8
+ for (const g of gaps) {
9
+ if (g.severity === 'critical')
10
+ critical++;
11
+ else if (g.severity === 'high')
12
+ high++;
13
+ else if (g.severity === 'medium')
14
+ medium++;
15
+ else
16
+ low++;
17
+ }
18
+ return Math.max(0, 100 - critical * 25 - high * 20 - medium * 8 - low * 3);
19
+ }
20
+ export function computeCoverageScore(routes) {
21
+ const scanned = routes.routes.length;
22
+ const skipped = routes.pagesSkipped;
23
+ const denom = scanned + skipped;
24
+ // TODO: return null here once the explorer exposes an explicit "discovered-but-unknown" signal
25
+ // (i.e. routes were found but the full set couldn't be confirmed — a low score is misleading)
26
+ if (denom === 0) {
27
+ if (routes.budgetExceeded) {
28
+ return 0;
29
+ }
30
+ return scanned === 0 ? 0 : 100;
31
+ }
32
+ return Math.round((100 * scanned) / denom);
33
+ }
3
34
  export function analyzeGaps(routes, repo, mode, config) {
4
35
  const coveredPaths = new Set();
5
36
  if (repo) {
@@ -57,11 +88,13 @@ export function analyzeGaps(routes, repo, mode, config) {
57
88
  }
58
89
  for (const violation of route.a11yViolations) {
59
90
  const impact = violation.impact.toLowerCase();
60
- const severity = impact === 'critical' || impact === 'serious'
61
- ? 'high'
62
- : impact === 'moderate'
63
- ? 'medium'
64
- : 'low';
91
+ const severity = impact === 'critical'
92
+ ? 'critical'
93
+ : impact === 'serious'
94
+ ? 'high'
95
+ : impact === 'moderate'
96
+ ? 'medium'
97
+ : 'low';
65
98
  addGap({
66
99
  id: randomUUID(),
67
100
  path: route.path,
@@ -71,14 +104,8 @@ export function analyzeGaps(routes, repo, mode, config) {
71
104
  });
72
105
  }
73
106
  }
74
- const highCount = gaps.filter((g) => g.severity === 'high').length;
75
- const mediumCount = gaps.filter((g) => g.severity === 'medium').length;
76
- const lowCount = gaps.filter((g) => g.severity === 'low').length;
77
- let releaseConfidence = Math.max(0, 100 - highCount * 20 - mediumCount * 8 - lowCount * 3);
107
+ const releaseConfidence = computeQualityScoreFromGaps(gaps);
78
108
  const pagesScanned = routes.routes.length;
79
- if (pagesScanned < config.minPagesForConfidence) {
80
- releaseConfidence = Math.min(releaseConfidence, 40);
81
- }
82
109
  let coverageWarning;
83
110
  if (routes.budgetExceeded) {
84
111
  coverageWarning = 'budget-exceeded';
@@ -1,7 +1,8 @@
1
1
  import type { AppExplorer } from './explorer.interface.js';
2
2
  import { type RouteInventory } from '../schemas/route-inventory.schema.js';
3
3
  import type { HarnessConfig } from '../schemas/config.schema.js';
4
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
4
5
  export declare class PlaywrightExplorer implements AppExplorer {
5
- explore(baseUrl: string, config: HarnessConfig): Promise<RouteInventory>;
6
+ explore(baseUrl: string, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<RouteInventory>;
6
7
  }
7
8
  //# sourceMappingURL=playwright-explorer.d.ts.map
@@ -1 +1 @@
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;AAgBjE,qBAAa,kBAAmB,YAAW,WAAW;IAC9C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;CAqJ/E"}
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"}
@@ -15,8 +15,12 @@ function isInternalHref(href, baseUrlStr) {
15
15
  return false;
16
16
  }
17
17
  }
18
+ function debugMode() {
19
+ return process.env.QULIB_DEBUG === '1';
20
+ }
18
21
  export class PlaywrightExplorer {
19
- async explore(baseUrl, config) {
22
+ async explore(baseUrl, config, artifacts) {
23
+ const progress = artifacts?.progressLog;
20
24
  const browser = await launchBrowser();
21
25
  let context;
22
26
  try {
@@ -28,7 +32,10 @@ export class PlaywrightExplorer {
28
32
  }
29
33
  if (config.auth) {
30
34
  const label = config.auth.type === 'form-login' ? config.auth.credentials.username : 'storage-state';
31
- console.error(`[qulib] authenticated as ${label}`);
35
+ progress?.info(`Authenticated context: ${label}`);
36
+ if (!progress) {
37
+ process.stderr.write(`[qulib] authenticated as ${label}\n`);
38
+ }
32
39
  }
33
40
  const visited = new Set();
34
41
  const queue = [baseUrl];
@@ -56,10 +63,15 @@ export class PlaywrightExplorer {
56
63
  }
57
64
  });
58
65
  try {
59
- await page.goto(url, {
66
+ const navResponse = await page.goto(url, {
60
67
  timeout: config.timeoutMs,
61
68
  waitUntil: 'domcontentloaded',
62
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
+ }
63
75
  const pageTitle = await page.title();
64
76
  const formCount = await page.locator('form').count();
65
77
  const buttonLabels = await page.locator('button').allInnerTexts();
@@ -92,6 +104,9 @@ export class PlaywrightExplorer {
92
104
  const axeResults = await new AxeBuilder({ page })
93
105
  .withTags(['wcag2a', 'wcag2aa'])
94
106
  .analyze();
107
+ if (debugMode()) {
108
+ progress?.debug(`raw axe violations (pre-map) count=${axeResults.violations.length} json=${JSON.stringify(axeResults.violations)}`);
109
+ }
95
110
  a11yViolations = axeResults.violations.map((v) => ({
96
111
  id: v.id,
97
112
  impact: v.impact ?? 'unknown',
@@ -103,6 +118,7 @@ export class PlaywrightExplorer {
103
118
  consoleErrors.push(`axe-core failure: ${String(err)}`);
104
119
  }
105
120
  const path = new URL(url).pathname || '/';
121
+ progress?.info(`Crawled ${normalized} status=${httpStatus} a11yViolations=${a11yViolations.length}`);
106
122
  routes.push({
107
123
  path,
108
124
  pageTitle,
@@ -112,6 +128,7 @@ export class PlaywrightExplorer {
112
128
  consoleErrors,
113
129
  brokenLinks,
114
130
  a11yViolations,
131
+ statusCode: httpStatus,
115
132
  });
116
133
  }
117
134
  catch (err) {
@@ -123,6 +140,7 @@ export class PlaywrightExplorer {
123
140
  return url;
124
141
  }
125
142
  })();
143
+ progress?.info(`Crawled ${normalized} status=error a11yViolations=0 err=${String(err).slice(0, 120)}`);
126
144
  routes.push({
127
145
  path,
128
146
  pageTitle: '',
@@ -0,0 +1,5 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,13 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/core",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -37,7 +37,11 @@
37
37
  "dev": "tsx src/cli/index.ts",
38
38
  "analyze": "tsx src/cli/index.ts analyze",
39
39
  "clean": "tsx src/cli/index.ts clean",
40
- "build": "tsc"
40
+ "build": "tsc",
41
+ "test": "node --import tsx/esm --test src/llm/cost-intelligence.test.ts src/tools/gap-engine.test.ts src/tools/auth-block-gap.test.ts",
42
+ "test:integration": "node --import tsx/esm --test src/analyze.integration.test.ts",
43
+ "smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
44
+ "cost-doctor": "tsx src/cli/index.ts cost doctor"
41
45
  },
42
46
  "dependencies": {
43
47
  "@axe-core/playwright": "^4.9.0",