@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.
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +29 -1
- package/dist/harness/decision-logger.d.ts +1 -0
- package/dist/harness/decision-logger.d.ts.map +1 -1
- package/dist/harness/decision-logger.js +15 -22
- package/dist/harness/run-options.d.ts +3 -0
- package/dist/harness/run-options.d.ts.map +1 -1
- package/dist/harness/state-manager.d.ts +3 -0
- package/dist/harness/state-manager.d.ts.map +1 -1
- package/dist/harness/state-manager.js +15 -18
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/llm/cost-intelligence.d.ts +13 -0
- package/dist/llm/cost-intelligence.d.ts.map +1 -1
- package/dist/llm/cost-intelligence.js +13 -0
- package/dist/llm/provider-registry.d.ts +9 -0
- package/dist/llm/provider-registry.d.ts.map +1 -0
- package/dist/llm/provider-registry.js +15 -0
- package/dist/llm/provider.d.ts +9 -11
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.interface.d.ts +16 -0
- package/dist/llm/provider.interface.d.ts.map +1 -0
- package/dist/llm/provider.interface.js +1 -0
- package/dist/llm/provider.js +8 -51
- package/dist/llm/providers/anthropic.d.ts +16 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.js +104 -0
- package/dist/phases/act.d.ts.map +1 -1
- package/dist/phases/act.js +20 -6
- package/dist/phases/observe.d.ts.map +1 -1
- package/dist/phases/observe.js +20 -2
- package/dist/phases/think-finalize.d.ts.map +1 -1
- package/dist/phases/think-finalize.js +12 -3
- package/dist/phases/think.d.ts.map +1 -1
- package/dist/phases/think.js +14 -2
- package/dist/schemas/automation-maturity.schema.d.ts +78 -0
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -0
- package/dist/schemas/automation-maturity.schema.js +24 -0
- package/dist/schemas/config.schema.d.ts +266 -73
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +30 -18
- package/dist/schemas/gap-analysis.schema.d.ts +6 -6
- package/dist/schemas/index.d.ts +2 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -1
- package/dist/schemas/public-surface.schema.d.ts +4 -4
- package/dist/schemas/repo-analysis.schema.d.ts +134 -0
- package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
- package/dist/schemas/repo-analysis.schema.js +29 -0
- package/dist/telemetry/emit.d.ts +3 -0
- package/dist/telemetry/emit.d.ts.map +1 -0
- package/dist/telemetry/emit.js +11 -0
- package/dist/telemetry/telemetry.interface.d.ts +13 -0
- package/dist/telemetry/telemetry.interface.d.ts.map +1 -0
- package/dist/telemetry/telemetry.interface.js +3 -0
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +205 -26
- package/dist/tools/auth-surface-analyzer.d.ts.map +1 -1
- package/dist/tools/auth-surface-analyzer.js +26 -10
- package/dist/tools/automation-maturity.d.ts +4 -0
- package/dist/tools/automation-maturity.d.ts.map +1 -0
- package/dist/tools/automation-maturity.js +163 -0
- package/dist/tools/framework-detector.d.ts +15 -0
- package/dist/tools/framework-detector.d.ts.map +1 -0
- package/dist/tools/framework-detector.js +153 -0
- package/dist/tools/gap-engine.d.ts +1 -1
- package/dist/tools/gap-engine.d.ts.map +1 -1
- package/dist/tools/gap-engine.js +13 -3
- package/dist/tools/repo-scanner.d.ts +16 -0
- package/dist/tools/repo-scanner.d.ts.map +1 -1
- package/dist/tools/repo-scanner.js +31 -2
- 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;
|
|
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,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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=${
|
|
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 ===
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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,
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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"}
|