@qulib/core 0.4.1 → 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 (135) hide show
  1. package/README.md +56 -8
  2. package/dist/analyze.d.ts.map +1 -1
  3. package/dist/analyze.js +86 -7
  4. package/dist/cli/auth-login-resolve.d.ts +14 -0
  5. package/dist/cli/auth-login-resolve.d.ts.map +1 -0
  6. package/dist/cli/auth-login-resolve.js +68 -0
  7. package/dist/cli/auth-login-run.d.ts +13 -0
  8. package/dist/cli/auth-login-run.d.ts.map +1 -0
  9. package/dist/cli/auth-login-run.js +152 -0
  10. package/dist/cli/index.js +60 -7
  11. package/dist/harness/state-manager.d.ts +10 -0
  12. package/dist/harness/state-manager.d.ts.map +1 -1
  13. package/dist/harness/state-manager.js +15 -0
  14. package/dist/index.d.ts +8 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/phases/act.js +3 -3
  18. package/dist/phases/observe.js +5 -5
  19. package/dist/phases/think.js +1 -1
  20. package/dist/schemas/automation-maturity.schema.d.ts +40 -0
  21. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  22. package/dist/schemas/automation-maturity.schema.js +27 -0
  23. package/dist/schemas/index.d.ts +1 -1
  24. package/dist/schemas/index.d.ts.map +1 -1
  25. package/dist/schemas/index.js +1 -1
  26. package/dist/schemas/repo-analysis.schema.d.ts +22 -0
  27. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
  28. package/dist/schemas/repo-analysis.schema.js +1 -0
  29. package/dist/telemetry/emit.d.ts +22 -0
  30. package/dist/telemetry/emit.d.ts.map +1 -1
  31. package/dist/telemetry/emit.js +37 -0
  32. package/dist/telemetry/telemetry.interface.d.ts +1 -1
  33. package/dist/telemetry/telemetry.interface.d.ts.map +1 -1
  34. package/dist/tools/apply-auth.d.ts +4 -0
  35. package/dist/tools/apply-auth.d.ts.map +1 -0
  36. package/dist/tools/apply-auth.js +35 -0
  37. package/dist/tools/auth/apply.d.ts +4 -0
  38. package/dist/tools/auth/apply.d.ts.map +1 -0
  39. package/dist/tools/auth/apply.js +35 -0
  40. package/dist/tools/auth/block-gap.d.ts +9 -0
  41. package/dist/tools/auth/block-gap.d.ts.map +1 -0
  42. package/dist/tools/auth/block-gap.js +52 -0
  43. package/dist/tools/auth/custom-providers.d.ts +15 -0
  44. package/dist/tools/auth/custom-providers.d.ts.map +1 -0
  45. package/dist/tools/auth/custom-providers.js +62 -0
  46. package/dist/tools/auth/detect.d.ts +23 -0
  47. package/dist/tools/auth/detect.d.ts.map +1 -0
  48. package/dist/tools/auth/detect.js +526 -0
  49. package/dist/tools/auth/detector.d.ts +23 -0
  50. package/dist/tools/auth/detector.d.ts.map +1 -0
  51. package/dist/tools/auth/detector.js +526 -0
  52. package/dist/tools/auth/explore.d.ts +4 -0
  53. package/dist/tools/auth/explore.d.ts.map +1 -0
  54. package/dist/tools/auth/explore.js +346 -0
  55. package/dist/tools/auth/explorer.d.ts +4 -0
  56. package/dist/tools/auth/explorer.d.ts.map +1 -0
  57. package/dist/tools/auth/explorer.js +346 -0
  58. package/dist/tools/auth/gaps.d.ts +9 -0
  59. package/dist/tools/auth/gaps.d.ts.map +1 -0
  60. package/dist/tools/auth/gaps.js +52 -0
  61. package/dist/tools/auth/oauth-providers.d.ts +7 -0
  62. package/dist/tools/auth/oauth-providers.d.ts.map +1 -0
  63. package/dist/tools/auth/oauth-providers.js +21 -0
  64. package/dist/tools/auth/providers.d.ts +7 -0
  65. package/dist/tools/auth/providers.d.ts.map +1 -0
  66. package/dist/tools/auth/providers.js +21 -0
  67. package/dist/tools/auth/surface-analyzer.d.ts +4 -0
  68. package/dist/tools/auth/surface-analyzer.d.ts.map +1 -0
  69. package/dist/tools/auth/surface-analyzer.js +170 -0
  70. package/dist/tools/auth/surface.d.ts +4 -0
  71. package/dist/tools/auth/surface.d.ts.map +1 -0
  72. package/dist/tools/auth/surface.js +170 -0
  73. package/dist/tools/auth/user-providers.d.ts +15 -0
  74. package/dist/tools/auth/user-providers.d.ts.map +1 -0
  75. package/dist/tools/auth/user-providers.js +62 -0
  76. package/dist/tools/auth-block-gap.d.ts +6 -0
  77. package/dist/tools/auth-block-gap.d.ts.map +1 -1
  78. package/dist/tools/auth-block-gap.js +42 -9
  79. package/dist/tools/auth-detector.d.ts +19 -0
  80. package/dist/tools/auth-detector.d.ts.map +1 -1
  81. package/dist/tools/auth-detector.js +186 -8
  82. package/dist/tools/automation-maturity.d.ts.map +1 -1
  83. package/dist/tools/automation-maturity.js +76 -20
  84. package/dist/tools/explorers/browser.d.ts +3 -0
  85. package/dist/tools/explorers/browser.d.ts.map +1 -0
  86. package/dist/tools/explorers/browser.js +13 -0
  87. package/dist/tools/explorers/cypress-explorer.d.ts +8 -0
  88. package/dist/tools/explorers/cypress-explorer.d.ts.map +1 -0
  89. package/dist/tools/explorers/cypress-explorer.js +5 -0
  90. package/dist/tools/explorers/cypress.d.ts +8 -0
  91. package/dist/tools/explorers/cypress.d.ts.map +1 -0
  92. package/dist/tools/explorers/cypress.js +5 -0
  93. package/dist/tools/explorers/explorer.interface.d.ts +7 -0
  94. package/dist/tools/explorers/explorer.interface.d.ts.map +1 -0
  95. package/dist/tools/explorers/explorer.interface.js +1 -0
  96. package/dist/tools/explorers/factory.d.ts +4 -0
  97. package/dist/tools/explorers/factory.d.ts.map +1 -0
  98. package/dist/tools/explorers/factory.js +12 -0
  99. package/dist/tools/explorers/playwright-explorer.d.ts +8 -0
  100. package/dist/tools/explorers/playwright-explorer.d.ts.map +1 -0
  101. package/dist/tools/explorers/playwright-explorer.js +172 -0
  102. package/dist/tools/explorers/playwright.d.ts +8 -0
  103. package/dist/tools/explorers/playwright.d.ts.map +1 -0
  104. package/dist/tools/explorers/playwright.js +172 -0
  105. package/dist/tools/explorers/types.d.ts +7 -0
  106. package/dist/tools/explorers/types.d.ts.map +1 -0
  107. package/dist/tools/explorers/types.js +1 -0
  108. package/dist/tools/playwright-explorer.js +1 -1
  109. package/dist/tools/repo/detect-framework.d.ts +15 -0
  110. package/dist/tools/repo/detect-framework.d.ts.map +1 -0
  111. package/dist/tools/repo/detect-framework.js +153 -0
  112. package/dist/tools/repo/framework-detector.d.ts +15 -0
  113. package/dist/tools/repo/framework-detector.d.ts.map +1 -0
  114. package/dist/tools/repo/framework-detector.js +153 -0
  115. package/dist/tools/repo/scan.d.ts +19 -0
  116. package/dist/tools/repo/scan.d.ts.map +1 -0
  117. package/dist/tools/repo/scan.js +181 -0
  118. package/dist/tools/repo/scanner.d.ts +19 -0
  119. package/dist/tools/repo/scanner.d.ts.map +1 -0
  120. package/dist/tools/repo/scanner.js +181 -0
  121. package/dist/tools/repo-scanner.d.ts.map +1 -1
  122. package/dist/tools/repo-scanner.js +7 -2
  123. package/dist/tools/scoring/automation-maturity.d.ts +4 -0
  124. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -0
  125. package/dist/tools/scoring/automation-maturity.js +219 -0
  126. package/dist/tools/scoring/gap-engine.d.ts +8 -0
  127. package/dist/tools/scoring/gap-engine.d.ts.map +1 -0
  128. package/dist/tools/scoring/gap-engine.js +138 -0
  129. package/dist/tools/scoring/gaps.d.ts +8 -0
  130. package/dist/tools/scoring/gaps.d.ts.map +1 -0
  131. package/dist/tools/scoring/gaps.js +138 -0
  132. package/dist/tools/scoring/public-surface.d.ts +5 -0
  133. package/dist/tools/scoring/public-surface.d.ts.map +1 -0
  134. package/dist/tools/scoring/public-surface.js +13 -0
  135. package/package.json +3 -3
@@ -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"}
@@ -0,0 +1,153 @@
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
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module tools/repo/scan
3
+ * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
+ *
5
+ * This module performs static analysis of a repository's file structure.
6
+ * It is currently embedded in @qulib/core because repo scanning is part of
7
+ * the observe phase and @qulib/core is the only consumer.
8
+ *
9
+ * Extraction to @qulib/analyzer is appropriate when:
10
+ * 1. A consumer needs repo analysis without URL crawling
11
+ * 2. The module grows to include PRD/Jira/Confluence ingestion
12
+ * 3. A standalone CLI command `qulib analyze-repo` is needed
13
+ *
14
+ * Before extraction: ensure RepoAnalysis schema is re-exported from @qulib/analyzer
15
+ * and @qulib/core depends on @qulib/analyzer (not the reverse).
16
+ */
17
+ import { type RepoAnalysis } from '../../schemas/repo-analysis.schema.js';
18
+ export declare function scanRepo(repoPath: string): Promise<RepoAnalysis>;
19
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/tools/repo/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAmC9F,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA8ItE"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @module tools/repo/scan
3
+ * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
+ *
5
+ * This module performs static analysis of a repository's file structure.
6
+ * It is currently embedded in @qulib/core because repo scanning is part of
7
+ * the observe phase and @qulib/core is the only consumer.
8
+ *
9
+ * Extraction to @qulib/analyzer is appropriate when:
10
+ * 1. A consumer needs repo analysis without URL crawling
11
+ * 2. The module grows to include PRD/Jira/Confluence ingestion
12
+ * 3. A standalone CLI command `qulib analyze-repo` is needed
13
+ *
14
+ * Before extraction: ensure RepoAnalysis schema is re-exported from @qulib/analyzer
15
+ * and @qulib/core depends on @qulib/analyzer (not the reverse).
16
+ */
17
+ import { readFile } from 'node:fs/promises';
18
+ import { relative, basename } from 'node:path';
19
+ import glob from 'fast-glob';
20
+ import { RepoAnalysisSchema } from '../../schemas/repo-analysis.schema.js';
21
+ import { detectFramework } from './detect-framework.js';
22
+ import { computeAutomationMaturity } from '../scoring/automation-maturity.js';
23
+ const IGNORE_PATTERNS = ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**'];
24
+ function toPosix(path) {
25
+ return path.split('\\').join('/');
26
+ }
27
+ function normalizeRoutePath(path) {
28
+ const normalized = `/${path}`.replace(/\/+/g, '/').replace(/\/$/, '');
29
+ return normalized === '' ? '/' : normalized;
30
+ }
31
+ function detectTestType(filePath, content) {
32
+ const normalizedPath = toPosix(filePath);
33
+ if (normalizedPath.includes('cypress/e2e'))
34
+ return 'cypress-e2e';
35
+ if (normalizedPath.includes('cypress/component'))
36
+ return 'cypress-component';
37
+ if ((normalizedPath.includes('playwright') || normalizedPath.includes('e2e')) &&
38
+ normalizedPath.endsWith('.spec.ts')) {
39
+ return 'playwright';
40
+ }
41
+ if (/\bfrom\s+['"]vitest['"]|\brequire\(['"]vitest['"]\)/.test(content))
42
+ return 'vitest';
43
+ if (/\bfrom\s+['"]jest['"]|\brequire\(['"]jest['"]\)/.test(content))
44
+ return 'jest';
45
+ return 'other';
46
+ }
47
+ function extractCoveredPaths(content) {
48
+ const matches = [...content.matchAll(/['"`](\/[a-zA-Z0-9\/_\-\[\]]+)['"`]/g)].map((m) => m[1]);
49
+ return [...new Set(matches)];
50
+ }
51
+ export async function scanRepo(repoPath) {
52
+ const routes = [];
53
+ const appRouterFiles = await glob(['app/**/page.tsx', 'app/**/page.ts'], {
54
+ cwd: repoPath,
55
+ onlyFiles: true,
56
+ absolute: true,
57
+ ignore: IGNORE_PATTERNS,
58
+ });
59
+ for (const file of appRouterFiles) {
60
+ const rel = toPosix(relative(repoPath, file));
61
+ const routeSegment = rel.replace(/^app\//, '').replace(/\/page\.tsx?$/, '');
62
+ const routePath = normalizeRoutePath(routeSegment);
63
+ routes.push({ path: routePath, file: rel, method: 'GET' });
64
+ }
65
+ const pagesRouterFiles = await glob(['pages/**/*.tsx', 'pages/**/*.ts'], {
66
+ cwd: repoPath,
67
+ onlyFiles: true,
68
+ absolute: true,
69
+ ignore: IGNORE_PATTERNS,
70
+ });
71
+ for (const file of pagesRouterFiles) {
72
+ const rel = toPosix(relative(repoPath, file));
73
+ const name = basename(rel);
74
+ if (name.startsWith('_'))
75
+ continue;
76
+ const routeSegment = rel.replace(/^pages\//, '').replace(/\.tsx?$/, '');
77
+ const routePath = routeSegment === 'index'
78
+ ? '/'
79
+ : normalizeRoutePath(routeSegment.replace(/\/index$/, ''));
80
+ routes.push({ path: routePath, file: rel, method: 'GET' });
81
+ }
82
+ const expressFiles = await glob(['src/**/*.ts', 'src/**/*.js'], {
83
+ cwd: repoPath,
84
+ onlyFiles: true,
85
+ absolute: true,
86
+ ignore: IGNORE_PATTERNS,
87
+ });
88
+ for (const file of expressFiles) {
89
+ const rel = toPosix(relative(repoPath, file));
90
+ const content = await readFile(file, 'utf8');
91
+ const routeRegex = /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)/gi;
92
+ for (const match of content.matchAll(routeRegex)) {
93
+ const method = match[1]?.toUpperCase();
94
+ const routePath = normalizeRoutePath(match[2] ?? '/');
95
+ routes.push({ path: routePath, file: rel, method });
96
+ }
97
+ }
98
+ const testFilePaths = await glob([
99
+ '**/*.spec.ts',
100
+ '**/*.test.ts',
101
+ '**/*.spec.tsx',
102
+ '**/*.test.tsx',
103
+ '**/cypress/e2e/**/*.ts',
104
+ '**/cypress/e2e/**/*.cy.ts',
105
+ ], {
106
+ cwd: repoPath,
107
+ onlyFiles: true,
108
+ absolute: true,
109
+ ignore: IGNORE_PATTERNS,
110
+ });
111
+ const testFiles = [];
112
+ for (const file of [...new Set(testFilePaths)]) {
113
+ const rel = toPosix(relative(repoPath, file));
114
+ const content = await readFile(file, 'utf8');
115
+ testFiles.push({
116
+ file: rel,
117
+ type: detectTestType(rel, content),
118
+ coveredPaths: extractCoveredPaths(content),
119
+ });
120
+ }
121
+ const cypressRoot = await glob(['cypress'], { cwd: repoPath, onlyDirectories: true, absolute: false, deep: 1 });
122
+ const e2eFolder = await glob(['cypress/e2e'], { cwd: repoPath, onlyDirectories: true, absolute: false, deep: 1 });
123
+ const componentFolder = await glob(['cypress/component'], { cwd: repoPath, onlyDirectories: true, absolute: false, deep: 1 });
124
+ const fixturesFolder = await glob(['cypress/fixtures'], { cwd: repoPath, onlyDirectories: true, absolute: false, deep: 1 });
125
+ const supportFolder = await glob(['cypress/support'], { cwd: repoPath, onlyDirectories: true, absolute: false, deep: 1 });
126
+ const commandsFile = await glob(['cypress/support/commands.ts'], { cwd: repoPath, onlyFiles: true, absolute: false });
127
+ const existingE2eFiles = await glob(['cypress/e2e/**/*.cy.ts'], { cwd: repoPath, onlyFiles: true, absolute: false });
128
+ const existingComponentFiles = await glob(['cypress/component/**/*.cy.tsx'], {
129
+ cwd: repoPath,
130
+ onlyFiles: true,
131
+ absolute: false,
132
+ });
133
+ const tsxFiles = await glob(['**/*.tsx'], {
134
+ cwd: repoPath,
135
+ onlyFiles: true,
136
+ absolute: true,
137
+ ignore: [...IGNORE_PATTERNS, '**/*.spec.tsx'],
138
+ });
139
+ const missingTestIds = [];
140
+ let interactiveTsxFilesScanned = 0;
141
+ for (const file of tsxFiles) {
142
+ const rel = toPosix(relative(repoPath, file));
143
+ const content = await readFile(file, 'utf8');
144
+ const hasInteractive = content.includes('<button') || content.includes('<input') || content.includes('<a ');
145
+ if (hasInteractive) {
146
+ interactiveTsxFilesScanned += 1;
147
+ if (!content.includes('data-testid')) {
148
+ missingTestIds.push(rel);
149
+ }
150
+ }
151
+ }
152
+ const base = {
153
+ scannedAt: new Date().toISOString(),
154
+ repoPath,
155
+ routes,
156
+ testFiles,
157
+ missingTestIds: [...new Set(missingTestIds)],
158
+ interactiveTsxFilesScanned,
159
+ cypressStructure: {
160
+ detected: cypressRoot.length > 0,
161
+ e2eFolder: e2eFolder[0],
162
+ componentFolder: componentFolder[0],
163
+ fixturesFolder: fixturesFolder[0],
164
+ supportFolder: supportFolder[0],
165
+ hasCommandsFile: commandsFile.length > 0,
166
+ existingE2eFiles,
167
+ existingComponentFiles,
168
+ },
169
+ };
170
+ let parsed = RepoAnalysisSchema.parse(base);
171
+ try {
172
+ const framework = await detectFramework(repoPath);
173
+ parsed = RepoAnalysisSchema.parse({ ...parsed, framework });
174
+ }
175
+ catch (error) {
176
+ const msg = error instanceof Error ? error.message : String(error);
177
+ console.warn(`[qulib] framework detection failed for ${repoPath}: ${msg}`);
178
+ }
179
+ const automationMaturity = computeAutomationMaturity(parsed);
180
+ return RepoAnalysisSchema.parse({ ...parsed, automationMaturity });
181
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module repo-scanner
3
+ * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
+ *
5
+ * This module performs static analysis of a repository's file structure.
6
+ * It is currently embedded in @qulib/core because repo scanning is part of
7
+ * the observe phase and @qulib/core is the only consumer.
8
+ *
9
+ * Extraction to @qulib/analyzer is appropriate when:
10
+ * 1. A consumer needs repo analysis without URL crawling
11
+ * 2. The module grows to include PRD/Jira/Confluence ingestion
12
+ * 3. A standalone CLI command `qulib analyze-repo` is needed
13
+ *
14
+ * Before extraction: ensure RepoAnalysis schema is re-exported from @qulib/analyzer
15
+ * and @qulib/core depends on @qulib/analyzer (not the reverse).
16
+ */
17
+ import { type RepoAnalysis } from '../../schemas/repo-analysis.schema.js';
18
+ export declare function scanRepo(repoPath: string): Promise<RepoAnalysis>;
19
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../../src/tools/repo/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAmC9F,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA8ItE"}