@qulib/core 0.4.2 → 0.4.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 (111) hide show
  1. package/README.md +34 -8
  2. package/dist/analyze.d.ts.map +1 -1
  3. package/dist/analyze.js +84 -5
  4. package/dist/cli/auth-login-run.d.ts.map +1 -1
  5. package/dist/cli/auth-login-run.js +26 -2
  6. package/dist/cli/index.js +9 -6
  7. package/dist/index.d.ts +6 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +5 -5
  10. package/dist/phases/observe.js +2 -2
  11. package/dist/phases/think.js +1 -1
  12. package/dist/telemetry/telemetry.interface.d.ts +1 -1
  13. package/dist/telemetry/telemetry.interface.d.ts.map +1 -1
  14. package/dist/tools/apply-auth.d.ts +4 -0
  15. package/dist/tools/apply-auth.d.ts.map +1 -0
  16. package/dist/tools/apply-auth.js +35 -0
  17. package/dist/tools/auth/apply.d.ts +4 -0
  18. package/dist/tools/auth/apply.d.ts.map +1 -0
  19. package/dist/tools/auth/apply.js +35 -0
  20. package/dist/tools/auth/block-gap.d.ts +9 -0
  21. package/dist/tools/auth/block-gap.d.ts.map +1 -0
  22. package/dist/tools/auth/block-gap.js +52 -0
  23. package/dist/tools/auth/custom-providers.d.ts +15 -0
  24. package/dist/tools/auth/custom-providers.d.ts.map +1 -0
  25. package/dist/tools/auth/custom-providers.js +62 -0
  26. package/dist/tools/auth/detect.d.ts +23 -0
  27. package/dist/tools/auth/detect.d.ts.map +1 -0
  28. package/dist/tools/auth/detect.js +526 -0
  29. package/dist/tools/auth/detector.d.ts +23 -0
  30. package/dist/tools/auth/detector.d.ts.map +1 -0
  31. package/dist/tools/auth/detector.js +526 -0
  32. package/dist/tools/auth/explore.d.ts +4 -0
  33. package/dist/tools/auth/explore.d.ts.map +1 -0
  34. package/dist/tools/auth/explore.js +346 -0
  35. package/dist/tools/auth/explorer.d.ts +4 -0
  36. package/dist/tools/auth/explorer.d.ts.map +1 -0
  37. package/dist/tools/auth/explorer.js +346 -0
  38. package/dist/tools/auth/gaps.d.ts +9 -0
  39. package/dist/tools/auth/gaps.d.ts.map +1 -0
  40. package/dist/tools/auth/gaps.js +52 -0
  41. package/dist/tools/auth/oauth-providers.d.ts +7 -0
  42. package/dist/tools/auth/oauth-providers.d.ts.map +1 -0
  43. package/dist/tools/auth/oauth-providers.js +21 -0
  44. package/dist/tools/auth/providers.d.ts +7 -0
  45. package/dist/tools/auth/providers.d.ts.map +1 -0
  46. package/dist/tools/auth/providers.js +21 -0
  47. package/dist/tools/auth/surface-analyzer.d.ts +4 -0
  48. package/dist/tools/auth/surface-analyzer.d.ts.map +1 -0
  49. package/dist/tools/auth/surface-analyzer.js +170 -0
  50. package/dist/tools/auth/surface.d.ts +4 -0
  51. package/dist/tools/auth/surface.d.ts.map +1 -0
  52. package/dist/tools/auth/surface.js +170 -0
  53. package/dist/tools/auth/user-providers.d.ts +15 -0
  54. package/dist/tools/auth/user-providers.d.ts.map +1 -0
  55. package/dist/tools/auth/user-providers.js +62 -0
  56. package/dist/tools/auth-block-gap.d.ts +6 -0
  57. package/dist/tools/auth-block-gap.d.ts.map +1 -1
  58. package/dist/tools/auth-block-gap.js +42 -9
  59. package/dist/tools/auth-detector.d.ts +9 -8
  60. package/dist/tools/auth-detector.d.ts.map +1 -1
  61. package/dist/tools/auth-detector.js +106 -8
  62. package/dist/tools/explorers/browser.d.ts +3 -0
  63. package/dist/tools/explorers/browser.d.ts.map +1 -0
  64. package/dist/tools/explorers/browser.js +13 -0
  65. package/dist/tools/explorers/cypress-explorer.d.ts +8 -0
  66. package/dist/tools/explorers/cypress-explorer.d.ts.map +1 -0
  67. package/dist/tools/explorers/cypress-explorer.js +5 -0
  68. package/dist/tools/explorers/cypress.d.ts +8 -0
  69. package/dist/tools/explorers/cypress.d.ts.map +1 -0
  70. package/dist/tools/explorers/cypress.js +5 -0
  71. package/dist/tools/explorers/explorer.interface.d.ts +7 -0
  72. package/dist/tools/explorers/explorer.interface.d.ts.map +1 -0
  73. package/dist/tools/explorers/explorer.interface.js +1 -0
  74. package/dist/tools/explorers/factory.d.ts +4 -0
  75. package/dist/tools/explorers/factory.d.ts.map +1 -0
  76. package/dist/tools/explorers/factory.js +12 -0
  77. package/dist/tools/explorers/playwright-explorer.d.ts +8 -0
  78. package/dist/tools/explorers/playwright-explorer.d.ts.map +1 -0
  79. package/dist/tools/explorers/playwright-explorer.js +172 -0
  80. package/dist/tools/explorers/playwright.d.ts +8 -0
  81. package/dist/tools/explorers/playwright.d.ts.map +1 -0
  82. package/dist/tools/explorers/playwright.js +172 -0
  83. package/dist/tools/explorers/types.d.ts +7 -0
  84. package/dist/tools/explorers/types.d.ts.map +1 -0
  85. package/dist/tools/explorers/types.js +1 -0
  86. package/dist/tools/playwright-explorer.js +1 -1
  87. package/dist/tools/repo/detect-framework.d.ts +15 -0
  88. package/dist/tools/repo/detect-framework.d.ts.map +1 -0
  89. package/dist/tools/repo/detect-framework.js +153 -0
  90. package/dist/tools/repo/framework-detector.d.ts +15 -0
  91. package/dist/tools/repo/framework-detector.d.ts.map +1 -0
  92. package/dist/tools/repo/framework-detector.js +153 -0
  93. package/dist/tools/repo/scan.d.ts +19 -0
  94. package/dist/tools/repo/scan.d.ts.map +1 -0
  95. package/dist/tools/repo/scan.js +181 -0
  96. package/dist/tools/repo/scanner.d.ts +19 -0
  97. package/dist/tools/repo/scanner.d.ts.map +1 -0
  98. package/dist/tools/repo/scanner.js +181 -0
  99. package/dist/tools/scoring/automation-maturity.d.ts +4 -0
  100. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -0
  101. package/dist/tools/scoring/automation-maturity.js +219 -0
  102. package/dist/tools/scoring/gap-engine.d.ts +8 -0
  103. package/dist/tools/scoring/gap-engine.d.ts.map +1 -0
  104. package/dist/tools/scoring/gap-engine.js +138 -0
  105. package/dist/tools/scoring/gaps.d.ts +8 -0
  106. package/dist/tools/scoring/gaps.d.ts.map +1 -0
  107. package/dist/tools/scoring/gaps.js +138 -0
  108. package/dist/tools/scoring/public-surface.d.ts +5 -0
  109. package/dist/tools/scoring/public-surface.d.ts.map +1 -0
  110. package/dist/tools/scoring/public-surface.js +13 -0
  111. package/package.json +3 -3
@@ -0,0 +1,172 @@
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
+ }
@@ -0,0 +1,8 @@
1
+ import type { AppExplorer } from './types.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.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../../src/tools/explorers/playwright.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,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"}
@@ -0,0 +1,172 @@
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
+ }
@@ -0,0 +1,7 @@
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=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/tools/explorers/types.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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { launchBrowser } from './browser.js';
2
2
  import { AxeBuilder } from '@axe-core/playwright';
3
- import { createAuthenticatedContext } from './auth.js';
3
+ import { createAuthenticatedContext } from './apply-auth.js';
4
4
  import { RouteInventorySchema } from '../schemas/route-inventory.schema.js';
5
5
  function crawlHostKey(hostname) {
6
6
  return hostname.replace(/^www\./i, '').toLowerCase();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module tools/repo/detect-framework
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=detect-framework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-framework.d.ts","sourceRoot":"","sources":["../../../src/tools/repo/detect-framework.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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @module tools/repo/detect-framework
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
+ }
@@ -0,0 +1,15 @@
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
@@ -0,0 +1 @@
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"}