@qulib/core 0.3.1 → 0.4.1

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 (74) hide show
  1. package/dist/analyze.d.ts +2 -0
  2. package/dist/analyze.d.ts.map +1 -1
  3. package/dist/analyze.js +29 -1
  4. package/dist/harness/decision-logger.d.ts +1 -0
  5. package/dist/harness/decision-logger.d.ts.map +1 -1
  6. package/dist/harness/decision-logger.js +15 -22
  7. package/dist/harness/run-options.d.ts +3 -0
  8. package/dist/harness/run-options.d.ts.map +1 -1
  9. package/dist/harness/state-manager.d.ts +3 -0
  10. package/dist/harness/state-manager.d.ts.map +1 -1
  11. package/dist/harness/state-manager.js +15 -18
  12. package/dist/index.d.ts +9 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -0
  15. package/dist/llm/cost-intelligence.d.ts +13 -0
  16. package/dist/llm/cost-intelligence.d.ts.map +1 -1
  17. package/dist/llm/cost-intelligence.js +13 -0
  18. package/dist/llm/provider-registry.d.ts +9 -0
  19. package/dist/llm/provider-registry.d.ts.map +1 -0
  20. package/dist/llm/provider-registry.js +15 -0
  21. package/dist/llm/provider.d.ts +9 -11
  22. package/dist/llm/provider.d.ts.map +1 -1
  23. package/dist/llm/provider.interface.d.ts +16 -0
  24. package/dist/llm/provider.interface.d.ts.map +1 -0
  25. package/dist/llm/provider.interface.js +1 -0
  26. package/dist/llm/provider.js +8 -51
  27. package/dist/llm/providers/anthropic.d.ts +16 -0
  28. package/dist/llm/providers/anthropic.d.ts.map +1 -0
  29. package/dist/llm/providers/anthropic.js +104 -0
  30. package/dist/phases/act.d.ts.map +1 -1
  31. package/dist/phases/act.js +20 -6
  32. package/dist/phases/observe.d.ts.map +1 -1
  33. package/dist/phases/observe.js +20 -2
  34. package/dist/phases/think-finalize.d.ts.map +1 -1
  35. package/dist/phases/think-finalize.js +12 -3
  36. package/dist/phases/think.d.ts.map +1 -1
  37. package/dist/phases/think.js +14 -2
  38. package/dist/schemas/automation-maturity.schema.d.ts +78 -0
  39. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -0
  40. package/dist/schemas/automation-maturity.schema.js +24 -0
  41. package/dist/schemas/config.schema.d.ts +266 -73
  42. package/dist/schemas/config.schema.d.ts.map +1 -1
  43. package/dist/schemas/config.schema.js +30 -18
  44. package/dist/schemas/gap-analysis.schema.d.ts +6 -6
  45. package/dist/schemas/index.d.ts +2 -1
  46. package/dist/schemas/index.d.ts.map +1 -1
  47. package/dist/schemas/index.js +2 -1
  48. package/dist/schemas/public-surface.schema.d.ts +4 -4
  49. package/dist/schemas/repo-analysis.schema.d.ts +134 -0
  50. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
  51. package/dist/schemas/repo-analysis.schema.js +29 -0
  52. package/dist/telemetry/emit.d.ts +3 -0
  53. package/dist/telemetry/emit.d.ts.map +1 -0
  54. package/dist/telemetry/emit.js +11 -0
  55. package/dist/telemetry/telemetry.interface.d.ts +13 -0
  56. package/dist/telemetry/telemetry.interface.d.ts.map +1 -0
  57. package/dist/telemetry/telemetry.interface.js +3 -0
  58. package/dist/tools/auth-detector.d.ts.map +1 -1
  59. package/dist/tools/auth-detector.js +205 -26
  60. package/dist/tools/auth-surface-analyzer.d.ts.map +1 -1
  61. package/dist/tools/auth-surface-analyzer.js +26 -10
  62. package/dist/tools/automation-maturity.d.ts +4 -0
  63. package/dist/tools/automation-maturity.d.ts.map +1 -0
  64. package/dist/tools/automation-maturity.js +163 -0
  65. package/dist/tools/framework-detector.d.ts +15 -0
  66. package/dist/tools/framework-detector.d.ts.map +1 -0
  67. package/dist/tools/framework-detector.js +153 -0
  68. package/dist/tools/gap-engine.d.ts +1 -1
  69. package/dist/tools/gap-engine.d.ts.map +1 -1
  70. package/dist/tools/gap-engine.js +13 -3
  71. package/dist/tools/repo-scanner.d.ts +16 -0
  72. package/dist/tools/repo-scanner.d.ts.map +1 -1
  73. package/dist/tools/repo-scanner.js +31 -2
  74. package/package.json +11 -1
@@ -1,4 +1,25 @@
1
1
  import { z } from 'zod';
2
+ export declare const DetectedFrameworkPrimarySchema: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
3
+ export declare const FrameworkDetectionConfidenceSchema: z.ZodEnum<["high", "medium", "low"]>;
4
+ export declare const TestFrameworkDetectedSchema: z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>;
5
+ export declare const FrameworkDetectionSchema: z.ZodObject<{
6
+ primary: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
7
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
8
+ evidence: z.ZodArray<z.ZodString, "many">;
9
+ testFrameworks: z.ZodArray<z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>, "many">;
10
+ }, "strip", z.ZodTypeAny, {
11
+ confidence: "high" | "medium" | "low";
12
+ evidence: string[];
13
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
14
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
15
+ }, {
16
+ confidence: "high" | "medium" | "low";
17
+ evidence: string[];
18
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
19
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
20
+ }>;
21
+ export type DetectedFrameworkPrimary = z.infer<typeof DetectedFrameworkPrimarySchema>;
22
+ export type FrameworkDetectionResult = z.infer<typeof FrameworkDetectionSchema>;
2
23
  export declare const RepoRouteSchema: z.ZodObject<{
3
24
  path: z.ZodString;
4
25
  file: z.ZodString;
@@ -111,6 +132,77 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
111
132
  fixturesFolder?: string | undefined;
112
133
  supportFolder?: string | undefined;
113
134
  }>;
135
+ framework: z.ZodOptional<z.ZodObject<{
136
+ primary: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
137
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
138
+ evidence: z.ZodArray<z.ZodString, "many">;
139
+ testFrameworks: z.ZodArray<z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>, "many">;
140
+ }, "strip", z.ZodTypeAny, {
141
+ confidence: "high" | "medium" | "low";
142
+ evidence: string[];
143
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
144
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
145
+ }, {
146
+ confidence: "high" | "medium" | "low";
147
+ evidence: string[];
148
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
149
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
150
+ }>>;
151
+ automationMaturity: z.ZodOptional<z.ZodObject<{
152
+ computedAt: z.ZodString;
153
+ repoPath: z.ZodString;
154
+ overallScore: z.ZodNumber;
155
+ level: z.ZodNumber;
156
+ label: z.ZodString;
157
+ dimensions: z.ZodArray<z.ZodObject<{
158
+ dimension: z.ZodEnum<["test-coverage-breadth", "framework-adoption", "test-id-hygiene", "ci-integration", "auth-test-coverage", "component-test-ratio"]>;
159
+ score: z.ZodNumber;
160
+ weight: z.ZodNumber;
161
+ evidence: z.ZodArray<z.ZodString, "many">;
162
+ recommendations: z.ZodArray<z.ZodString, "many">;
163
+ }, "strip", z.ZodTypeAny, {
164
+ recommendations: string[];
165
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
166
+ score: number;
167
+ weight: number;
168
+ evidence: string[];
169
+ }, {
170
+ recommendations: string[];
171
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
172
+ score: number;
173
+ weight: number;
174
+ evidence: string[];
175
+ }>, "many">;
176
+ topRecommendations: z.ZodArray<z.ZodString, "many">;
177
+ }, "strip", z.ZodTypeAny, {
178
+ label: string;
179
+ level: number;
180
+ computedAt: string;
181
+ repoPath: string;
182
+ overallScore: number;
183
+ dimensions: {
184
+ recommendations: string[];
185
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
186
+ score: number;
187
+ weight: number;
188
+ evidence: string[];
189
+ }[];
190
+ topRecommendations: string[];
191
+ }, {
192
+ label: string;
193
+ level: number;
194
+ computedAt: string;
195
+ repoPath: string;
196
+ overallScore: number;
197
+ dimensions: {
198
+ recommendations: string[];
199
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
200
+ score: number;
201
+ weight: number;
202
+ evidence: string[];
203
+ }[];
204
+ topRecommendations: string[];
205
+ }>>;
114
206
  }, "strip", z.ZodTypeAny, {
115
207
  scannedAt: string;
116
208
  routes: {
@@ -135,6 +227,27 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
135
227
  fixturesFolder?: string | undefined;
136
228
  supportFolder?: string | undefined;
137
229
  };
230
+ framework?: {
231
+ confidence: "high" | "medium" | "low";
232
+ evidence: string[];
233
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
234
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
235
+ } | undefined;
236
+ automationMaturity?: {
237
+ label: string;
238
+ level: number;
239
+ computedAt: string;
240
+ repoPath: string;
241
+ overallScore: number;
242
+ dimensions: {
243
+ recommendations: string[];
244
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
245
+ score: number;
246
+ weight: number;
247
+ evidence: string[];
248
+ }[];
249
+ topRecommendations: string[];
250
+ } | undefined;
138
251
  }, {
139
252
  scannedAt: string;
140
253
  routes: {
@@ -159,6 +272,27 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
159
272
  fixturesFolder?: string | undefined;
160
273
  supportFolder?: string | undefined;
161
274
  };
275
+ framework?: {
276
+ confidence: "high" | "medium" | "low";
277
+ evidence: string[];
278
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
279
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
280
+ } | undefined;
281
+ automationMaturity?: {
282
+ label: string;
283
+ level: number;
284
+ computedAt: string;
285
+ repoPath: string;
286
+ overallScore: number;
287
+ dimensions: {
288
+ recommendations: string[];
289
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
290
+ score: number;
291
+ weight: number;
292
+ evidence: string[];
293
+ }[];
294
+ topRecommendations: string[];
295
+ } | undefined;
162
296
  }>;
163
297
  export type RepoAnalysis = z.infer<typeof RepoAnalysisSchema>;
164
298
  export type CypressStructure = z.infer<typeof CypressStructureSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,8BAA8B,8HAUzC,CAAC;AAEH,eAAO,MAAM,kCAAkC,sCAAoC,CAAC;AAEpF,eAAO,MAAM,2BAA2B,0FAOtC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACtF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -1,4 +1,31 @@
1
1
  import { z } from 'zod';
2
+ import { AutomationMaturitySchema } from './automation-maturity.schema.js';
3
+ export const DetectedFrameworkPrimarySchema = z.enum([
4
+ 'nextjs-app-router',
5
+ 'nextjs-pages-router',
6
+ 'express',
7
+ 'remix',
8
+ 'nuxt',
9
+ 'sveltekit',
10
+ 'astro',
11
+ 'vite',
12
+ 'unknown',
13
+ ]);
14
+ export const FrameworkDetectionConfidenceSchema = z.enum(['high', 'medium', 'low']);
15
+ export const TestFrameworkDetectedSchema = z.enum([
16
+ 'playwright',
17
+ 'cypress-e2e',
18
+ 'cypress-component',
19
+ 'jest',
20
+ 'vitest',
21
+ 'other',
22
+ ]);
23
+ export const FrameworkDetectionSchema = z.object({
24
+ primary: DetectedFrameworkPrimarySchema,
25
+ confidence: FrameworkDetectionConfidenceSchema,
26
+ evidence: z.array(z.string()),
27
+ testFrameworks: z.array(TestFrameworkDetectedSchema),
28
+ });
2
29
  export const RepoRouteSchema = z.object({
3
30
  path: z.string(),
4
31
  file: z.string(),
@@ -26,4 +53,6 @@ export const RepoAnalysisSchema = z.object({
26
53
  testFiles: z.array(TestFileSchema),
27
54
  missingTestIds: z.array(z.string()),
28
55
  cypressStructure: CypressStructureSchema,
56
+ framework: FrameworkDetectionSchema.optional(),
57
+ automationMaturity: AutomationMaturitySchema.optional(),
29
58
  });
@@ -0,0 +1,3 @@
1
+ import type { TelemetryEvent, TelemetryEventKind, TelemetrySink } from './telemetry.interface.js';
2
+ export declare function emitTelemetry(sink: TelemetrySink | undefined, kind: TelemetryEventKind, sessionId: string, metadata: TelemetryEvent['metadata'], durationMs?: number): void;
3
+ //# sourceMappingURL=emit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/telemetry/emit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElG,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EACpC,UAAU,CAAC,EAAE,MAAM,GAClB,IAAI,CASN"}
@@ -0,0 +1,11 @@
1
+ export function emitTelemetry(sink, kind, sessionId, metadata, durationMs) {
2
+ if (!sink)
3
+ return;
4
+ sink.emit({
5
+ kind,
6
+ timestamp: new Date().toISOString(),
7
+ sessionId,
8
+ metadata,
9
+ ...(durationMs !== undefined && { durationMs }),
10
+ });
11
+ }
@@ -0,0 +1,13 @@
1
+ export type TelemetryEventKind = 'scan.started' | 'scan.completed' | 'scan.blocked' | 'phase.observe.started' | 'phase.observe.completed' | 'phase.think.started' | 'phase.think.completed' | 'phase.act.started' | 'phase.act.completed' | 'llm.call.started' | 'llm.call.completed' | 'llm.call.failed' | 'gap.detected' | 'auth.detected' | 'repo.scanned';
2
+ export interface TelemetryEvent {
3
+ kind: TelemetryEventKind;
4
+ timestamp: string;
5
+ sessionId: string;
6
+ durationMs?: number;
7
+ metadata: Record<string, string | number | boolean | null>;
8
+ }
9
+ export interface TelemetrySink {
10
+ emit(event: TelemetryEvent): void;
11
+ }
12
+ export declare const NoopTelemetrySink: TelemetrySink;
13
+ //# sourceMappingURL=telemetry.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.interface.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry.interface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,uBAAuB,GACvB,yBAAyB,GACzB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,qBAAqB,GACrB,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,aAE/B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export const NoopTelemetrySink = {
2
+ emit: () => { },
3
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAgEtE,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CA8HvB"}
1
+ {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAsPtE,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAkJvB"}
@@ -1,4 +1,5 @@
1
1
  import { launchBrowser } from './browser.js';
2
+ import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
2
3
  async function waitNetworkIdleBestEffort(page) {
3
4
  try {
4
5
  await page.waitForLoadState('networkidle', { timeout: 5000 });
@@ -7,27 +8,20 @@ async function waitNetworkIdleBestEffort(page) {
7
8
  // best-effort — analytics or polling can prevent networkidle
8
9
  }
9
10
  }
10
- const OAUTH_PROVIDERS = [
11
- { provider: 'github', patterns: [/github/i, /sign in with github/i] },
12
- {
13
- provider: 'google',
14
- patterns: [/google/i, /sign in with google/i, /accounts\.google\.com/i],
15
- },
16
- {
17
- provider: 'microsoft',
18
- patterns: [/microsoft/i, /sign in with microsoft/i, /login\.microsoftonline\.com/i],
19
- },
20
- { provider: 'apple', patterns: [/apple/i, /sign in with apple/i] },
21
- { provider: 'auth0', patterns: [/auth0/i] },
22
- { provider: 'okta', patterns: [/okta/i] },
23
- ];
11
+ const PROVIDER_LABELS = new Set(BUILT_IN_OAUTH_PROVIDERS.map((p) => p.label.toLowerCase()));
24
12
  function textLooksLikeOAuthIdpButton(text) {
25
13
  const t = text.trim();
26
14
  if (t.length === 0 || t.length > 120) {
27
15
  return false;
28
16
  }
29
- return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
30
- /^(github|google|microsoft|apple)$/i.test(t));
17
+ if (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t)) {
18
+ return true;
19
+ }
20
+ // Accept single-word / short labels that exactly match a known provider name
21
+ if (PROVIDER_LABELS.has(t.toLowerCase())) {
22
+ return true;
23
+ }
24
+ return false;
31
25
  }
32
26
  const MAGIC_LINK_PATTERNS = [
33
27
  /email me a (sign[- ]?in )?link/i,
@@ -53,6 +47,177 @@ async function firstTextInputNameForLogin(page) {
53
47
  function debugAuth() {
54
48
  return process.env.QULIB_DEBUG === '1';
55
49
  }
50
+ function slugify(label) {
51
+ const s = label
52
+ .toLowerCase()
53
+ .replace(/\s+/g, '-')
54
+ .replace(/[^a-z0-9-]+/g, '')
55
+ .replace(/^-+|-+$/g, '');
56
+ return s.length > 0 ? s : 'custom';
57
+ }
58
+ function escapeRegExp(s) {
59
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
+ }
61
+ async function resolveVisibleFieldLabel(page, el) {
62
+ const id = await el.getAttribute('id');
63
+ if (id) {
64
+ const lt = await page.locator(`label[for="${id.replace(/"/g, '\\"')}"]`).first().textContent().catch(() => null);
65
+ const fromLabel = (lt ?? '').trim();
66
+ if (fromLabel)
67
+ return fromLabel;
68
+ }
69
+ const placeholder = (await el.getAttribute('placeholder'))?.trim();
70
+ if (placeholder)
71
+ return placeholder;
72
+ const aria = (await el.getAttribute('aria-label'))?.trim();
73
+ if (aria)
74
+ return aria;
75
+ const name = (await el.getAttribute('name'))?.trim();
76
+ if (name)
77
+ return name;
78
+ const typ = (await el.getAttribute('type'))?.trim();
79
+ return typ && typ !== 'select' ? typ : 'text';
80
+ }
81
+ async function deriveCredentialFieldName(el) {
82
+ const name = (await el.getAttribute('name'))?.trim();
83
+ if (name)
84
+ return name;
85
+ const placeholder = (await el.getAttribute('placeholder'))?.trim();
86
+ if (placeholder)
87
+ return slugify(placeholder);
88
+ const aria = (await el.getAttribute('aria-label'))?.trim();
89
+ if (aria)
90
+ return slugify(aria);
91
+ const id = (await el.getAttribute('id'))?.trim();
92
+ if (id)
93
+ return slugify(id);
94
+ return 'field';
95
+ }
96
+ async function buildCredentialFieldsFromVisibleForm(page) {
97
+ const fields = [];
98
+ const seen = new Set();
99
+ const loc = page.locator('input[type="text"]:visible, input[type="email"]:visible, input[type="password"]:visible, select:visible');
100
+ const count = await loc.count();
101
+ for (let i = 0; i < count; i++) {
102
+ const el = loc.nth(i);
103
+ const tag = await el.evaluate((node) => node.tagName.toLowerCase()).catch(() => '');
104
+ if (tag === 'select') {
105
+ const name = await deriveCredentialFieldName(el);
106
+ const label = await resolveVisibleFieldLabel(page, el);
107
+ const opts = await el.locator('option').allInnerTexts();
108
+ const observedOptions = opts.map((o) => o.trim()).filter((x) => x.length > 0).slice(0, 20);
109
+ const dedupeKey = `select|${name}|${label}`;
110
+ if (seen.has(dedupeKey))
111
+ continue;
112
+ seen.add(dedupeKey);
113
+ fields.push({ name, label, type: 'select', observedOptions });
114
+ continue;
115
+ }
116
+ const rawType = ((await el.getAttribute('type')) ?? 'text').toLowerCase();
117
+ if (rawType === 'hidden')
118
+ continue;
119
+ const fieldType = rawType === 'email' ? 'email' : rawType === 'password' ? 'password' : 'text';
120
+ const name = await deriveCredentialFieldName(el);
121
+ const label = await resolveVisibleFieldLabel(page, el);
122
+ const placeholder = (await el.getAttribute('placeholder'))?.trim() ?? '';
123
+ const dedupeKey = `${name}|${placeholder}`;
124
+ if (seen.has(dedupeKey))
125
+ continue;
126
+ seen.add(dedupeKey);
127
+ fields.push({ name, label, type: fieldType, observedOptions: [] });
128
+ }
129
+ return fields;
130
+ }
131
+ function authPathsFromOauthButtons(oauthButtons, loginUrl) {
132
+ return oauthButtons.map((b) => {
133
+ const isUnknown = b.provider === 'unknown';
134
+ const id = isUnknown ? slugify(b.text) : b.provider;
135
+ return {
136
+ id,
137
+ label: b.text,
138
+ type: isUnknown ? 'oauth-unknown' : 'oauth',
139
+ provider: isUnknown ? slugify(b.text) : b.provider,
140
+ source: isUnknown ? 'heuristic' : 'built-in',
141
+ automatable: false,
142
+ confidence: isUnknown ? 'low' : 'high',
143
+ requirements: {
144
+ method: 'storage-state',
145
+ instruction: `Run qulib auth init --base-url ${loginUrl}`,
146
+ },
147
+ };
148
+ });
149
+ }
150
+ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, timeoutMs, progress) {
151
+ const out = [];
152
+ const buttons = page.locator('button');
153
+ const n = await buttons.count();
154
+ const seenLabels = new Set();
155
+ const SUBMIT_RE = /^(sign in|log in|submit|continue|next|cancel|close)$/i;
156
+ let candidateAttempts = 0;
157
+ for (let i = 0; i < n && candidateAttempts < 4; i++) {
158
+ const label = ((await buttons.nth(i).textContent()) ?? '').trim();
159
+ if (!label || label.length > 80)
160
+ continue;
161
+ if (alreadyMatchedTexts.has(label))
162
+ continue;
163
+ if (SUBMIT_RE.test(label))
164
+ continue;
165
+ if (seenLabels.has(label))
166
+ continue;
167
+ seenLabels.add(label);
168
+ candidateAttempts += 1;
169
+ if (debugAuth()) {
170
+ progress?.debug(`detect_auth click-reveal try label="${label.slice(0, 80)}"`);
171
+ }
172
+ let clicked = false;
173
+ try {
174
+ await page.getByRole('button', { name: label, exact: true }).first().click({ timeout: 2000 });
175
+ clicked = true;
176
+ }
177
+ catch {
178
+ try {
179
+ await page
180
+ .locator('button')
181
+ .filter({ hasText: new RegExp(`^\\s*${escapeRegExp(label)}\\s*$`, 'i') })
182
+ .first()
183
+ .click({ timeout: 2000 });
184
+ clicked = true;
185
+ }
186
+ catch {
187
+ /* skip */
188
+ }
189
+ }
190
+ if (!clicked) {
191
+ continue;
192
+ }
193
+ try {
194
+ await page.locator('input[type="password"]').first().waitFor({ state: 'visible', timeout: 2000 });
195
+ }
196
+ catch {
197
+ await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
198
+ await waitNetworkIdleBestEffort(page);
199
+ continue;
200
+ }
201
+ const fields = await buildCredentialFieldsFromVisibleForm(page);
202
+ const slug = slugify(label);
203
+ out.push({
204
+ id: slug,
205
+ label,
206
+ type: 'form-login',
207
+ provider: slug,
208
+ source: 'heuristic',
209
+ automatable: true,
210
+ confidence: 'medium',
211
+ requirements: {
212
+ method: 'credentials',
213
+ fields,
214
+ },
215
+ });
216
+ await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
217
+ await waitNetworkIdleBestEffort(page);
218
+ }
219
+ return out;
220
+ }
56
221
  export async function detectAuth(url, timeoutMs = 15000, progress) {
57
222
  const browser = await launchBrowser();
58
223
  try {
@@ -96,18 +261,27 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
96
261
  }
97
262
  continue;
98
263
  }
99
- for (const { provider, patterns } of OAUTH_PROVIDERS) {
264
+ let matchedAny = false;
265
+ for (const { id, patterns } of BUILT_IN_OAUTH_PROVIDERS) {
100
266
  const matched = patterns.some((p) => p.test(trimmed));
101
267
  if (debugAuth()) {
102
- progress?.debug(`detect_auth oauth pattern try provider=${provider} matched=${matched}`);
268
+ progress?.debug(`detect_auth oauth pattern try provider=${id} matched=${matched}`);
103
269
  }
104
270
  if (matched) {
105
- if (!oauthButtons.find((b) => b.provider === provider)) {
106
- oauthButtons.push({ provider, text: trimmed.slice(0, 100) });
271
+ if (!oauthButtons.find((b) => b.provider === id)) {
272
+ oauthButtons.push({ provider: id, text: trimmed.slice(0, 100) });
107
273
  }
274
+ matchedAny = true;
108
275
  }
109
276
  }
277
+ // Capture unrecognized SSO-like buttons so they appear in the result
278
+ if (!matchedAny && !oauthButtons.find((b) => b.text === trimmed.slice(0, 100))) {
279
+ oauthButtons.push({ provider: 'unknown', text: trimmed.slice(0, 100) });
280
+ }
110
281
  }
282
+ // Only skip buttons already tied to a built-in IdP — leave `unknown` labels probe-able for click-to-reveal forms.
283
+ const skipProbeLabels = new Set(oauthButtons.filter((b) => b.provider !== 'unknown').map((b) => b.text.trim()));
284
+ const clickRevealForms = await probeClickToRevealForms(page, loginUrl, skipProbeLabels, timeoutMs, progress);
111
285
  const pageText = await page.locator('body').innerText().catch(() => '');
112
286
  const hasMagicLink = MAGIC_LINK_PATTERNS.some((p) => p.test(pageText));
113
287
  let type = 'none';
@@ -117,7 +291,7 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
117
291
  if (oauthButtons.length > 0) {
118
292
  type = 'oauth';
119
293
  provider = oauthButtons[0].provider;
120
- recommendation = `OAuth detected (${oauthButtons.map((b) => b.provider).join(', ')}). OAuth cannot be automated. Run "qulib auth init --base-url ${url}" to log in manually once and save a reusable storage state file.`;
294
+ recommendation = `OAuth detected (${oauthButtons.map((b) => b.provider).join(', ')}). OAuth cannot be automated. Run "qulib auth init --base-url ${loginUrl}" to log in manually once and save a reusable storage state file.`;
121
295
  }
122
296
  else if (hasFormLogin) {
123
297
  type = 'form-login';
@@ -140,26 +314,31 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
140
314
  }
141
315
  else if (hasMagicLink) {
142
316
  type = 'magic-link';
143
- recommendation = `Magic link / passwordless auth detected. Qulib cannot complete email-link flows. Run "qulib auth init --base-url ${url}" to log in manually once and save a storage state file.`;
317
+ recommendation = `Magic link / passwordless auth detected. Qulib cannot complete email-link flows. Run "qulib auth init --base-url ${loginUrl}" to log in manually once and save a storage state file.`;
144
318
  }
145
319
  else if (looksLikeLoginPage) {
146
320
  type = 'unknown';
147
- recommendation = `Authentication required but the pattern is unrecognized. Use "qulib auth init --base-url ${url}" to capture a storage state by logging in manually.`;
321
+ recommendation = `Authentication required but the pattern is unrecognized. Use "qulib auth init --base-url ${loginUrl}" to capture a storage state by logging in manually.`;
148
322
  }
149
323
  else {
150
324
  type = 'none';
151
325
  recommendation = `No authentication required for the entry URL. Qulib can scan anonymously.`;
152
326
  }
327
+ if (clickRevealForms.length > 0) {
328
+ recommendation += `\nAutomatable form login detected via: ${clickRevealForms.map((f) => f.label).join(', ')}. Use type="form-login" with the observed selectors in authOptions.`;
329
+ }
153
330
  const providerList = oauthButtons.length > 0 ? oauthButtons.map((b) => b.provider).join(', ') : provider ?? 'none';
154
- const automatable = type === 'form-login';
331
+ const automatable = type === 'form-login' || clickRevealForms.length > 0;
155
332
  progress?.info(`Auth detected: ${type} (${providerList}) automatable=${automatable}`);
333
+ const authOptions = [...authPathsFromOauthButtons(oauthButtons, loginUrl), ...clickRevealForms];
156
334
  return {
157
- hasAuth: type !== 'none',
335
+ hasAuth: type !== 'none' || oauthButtons.length > 0 || clickRevealForms.length > 0,
158
336
  type,
159
337
  provider,
160
- loginUrl: type === 'none' ? null : loginUrl,
338
+ loginUrl: type === 'none' && oauthButtons.length === 0 && clickRevealForms.length === 0 ? null : loginUrl,
161
339
  observedSelectors,
162
340
  oauthButtons,
341
+ ...(authOptions.length > 0 ? { authOptions } : {}),
163
342
  recommendation,
164
343
  };
165
344
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth-surface-analyzer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-surface-analyzer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAW7D,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,EAAE,CAAC,CAuJhB"}
1
+ {"version":3,"file":"auth-surface-analyzer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-surface-analyzer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAW7D,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,EAAE,CAAC,CAwKhB"}
@@ -110,16 +110,32 @@ export async function analyzeAuthSurfaceGaps(url, detection, timeoutMs) {
110
110
  const hasEmailLink = await page.getByText(/magic link|email.*link|passwordless/i).count();
111
111
  const hasOAuthUi = detection.oauthButtons.length > 0 ||
112
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
- });
113
+ const formLoginFallbacks = (detection.authOptions ?? []).filter((o) => o.type === 'form-login');
114
+ const hasFormLoginFallback = formLoginFallbacks.length > 0;
115
+ if (detection.type === 'oauth' && hasOAuthUi && !hasPassword && !hasEmailLink) {
116
+ if (hasFormLoginFallback) {
117
+ const labels = formLoginFallbacks.map((o) => o.label).join(', ');
118
+ gaps.push({
119
+ id: randomUUID(),
120
+ path: '/',
121
+ severity: 'low',
122
+ category: 'auth-surface',
123
+ reason: `OAuth-primary login with form-login fallback detected via: ${labels}`,
124
+ description: 'A form-based login path exists alongside OAuth. Automate via type="form-login" using the selectors in authOptions.',
125
+ recommendation: `Automatable form option(s): ${labels}. Configure type="form-login" with credentials and selectors from detectedAuth.authOptions.`,
126
+ });
127
+ }
128
+ else {
129
+ gaps.push({
130
+ id: randomUUID(),
131
+ path: '/',
132
+ severity: 'medium',
133
+ category: 'auth-surface',
134
+ reason: 'OAuth-only entry with no visible password or magic-link fallback.',
135
+ description: 'Users who cannot use a social IdP need another path (email/password, help, or support).',
136
+ recommendation: 'Add a documented fallback (email/password, help desk link, or alternate IdP).',
137
+ });
138
+ }
123
139
  }
124
140
  const errorSelectors = '[role="alert"], [data-testid*="error" i], .error, .alert-danger, [class*="error" i]';
125
141
  const errCount = await page.locator(errorSelectors).count();
@@ -0,0 +1,4 @@
1
+ import type { RepoAnalysis } from '../schemas/repo-analysis.schema.js';
2
+ import type { AutomationMaturity } from '../schemas/automation-maturity.schema.js';
3
+ export declare function computeAutomationMaturity(repo: RepoAnalysis): AutomationMaturity;
4
+ //# sourceMappingURL=automation-maturity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../src/tools/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAA+B,MAAM,0CAA0C,CAAC;AAiDhH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CA6HhF"}