@qulib/core 0.4.1 → 0.4.2
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/README.md +22 -0
- package/dist/analyze.js +2 -2
- package/dist/cli/auth-login-resolve.d.ts +14 -0
- package/dist/cli/auth-login-resolve.d.ts.map +1 -0
- package/dist/cli/auth-login-resolve.js +68 -0
- package/dist/cli/auth-login-run.d.ts +13 -0
- package/dist/cli/auth-login-run.d.ts.map +1 -0
- package/dist/cli/auth-login-run.js +128 -0
- package/dist/cli/index.js +51 -1
- package/dist/harness/state-manager.d.ts +10 -0
- package/dist/harness/state-manager.d.ts.map +1 -1
- package/dist/harness/state-manager.js +15 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/phases/act.js +3 -3
- package/dist/phases/observe.js +3 -3
- package/dist/schemas/automation-maturity.schema.d.ts +40 -0
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
- package/dist/schemas/automation-maturity.schema.js +27 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/repo-analysis.schema.d.ts +22 -0
- package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
- package/dist/schemas/repo-analysis.schema.js +1 -0
- package/dist/telemetry/emit.d.ts +22 -0
- package/dist/telemetry/emit.d.ts.map +1 -1
- package/dist/telemetry/emit.js +37 -0
- package/dist/tools/auth-detector.d.ts +18 -0
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +88 -8
- package/dist/tools/automation-maturity.d.ts.map +1 -1
- package/dist/tools/automation-maturity.js +76 -20
- package/dist/tools/repo-scanner.d.ts.map +1 -1
- package/dist/tools/repo-scanner.js +7 -2
- package/package.json +2 -2
package/dist/schemas/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchem
|
|
|
4
4
|
export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, type GapAnalysis, type Gap, type NeutralScenario, type GeneratedTest, type TestStep, type FrameworkRecommendation, } from './gap-analysis.schema.js';
|
|
5
5
|
export { CostIntelligenceSchema, LlmUsageRecordSchema, LlmDataQualitySchema, LlmOperationTypeSchema, RepeatedAiPatternSchema, DeterministicMaturitySchema, type CostIntelligence, type LlmUsageRecord, type LlmDataQuality, type LlmOperationType, type RepeatedAiPattern, type DeterministicMaturity, } from './cost-intelligence.schema.js';
|
|
6
6
|
export { RepoAnalysisSchema, FrameworkDetectionSchema, DetectedFrameworkPrimarySchema, FrameworkDetectionConfidenceSchema, TestFrameworkDetectedSchema, type RepoAnalysis, type FrameworkDetectionResult, type DetectedFrameworkPrimary, } from './repo-analysis.schema.js';
|
|
7
|
-
export { AutomationMaturitySchema, AutomationMaturityDimensionSchema, type AutomationMaturity, type AutomationMaturityDimension, } from './automation-maturity.schema.js';
|
|
7
|
+
export { AutomationMaturitySchema, AutomationMaturityDimensionSchema, AutomationMaturityApplicabilitySchema, type AutomationMaturity, type AutomationMaturityDimension, type AutomationMaturityApplicability, } from './automation-maturity.schema.js';
|
|
8
8
|
export { PublicSurfaceSchema, PublicSurfaceViolationSchema, PublicSurfaceBrokenLinkSchema, type PublicSurface, type PublicSurfaceViolation, type PublicSurfaceBrokenLink, } from './public-surface.schema.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gCAAgC,EAChC,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,2BAA2B,EAC3B,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,EAC9B,kCAAkC,EAClC,2BAA2B,EAC3B,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gCAAgC,EAChC,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,2BAA2B,EAC3B,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,8BAA8B,EAC9B,kCAAkC,EAClC,2BAA2B,EAC3B,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,qCAAqC,EACrC,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,+BAA+B,GACrC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,6BAA6B,EAC7B,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,4BAA4B,CAAC"}
|
package/dist/schemas/index.js
CHANGED
|
@@ -4,5 +4,5 @@ export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchem
|
|
|
4
4
|
export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, } from './gap-analysis.schema.js';
|
|
5
5
|
export { CostIntelligenceSchema, LlmUsageRecordSchema, LlmDataQualitySchema, LlmOperationTypeSchema, RepeatedAiPatternSchema, DeterministicMaturitySchema, } from './cost-intelligence.schema.js';
|
|
6
6
|
export { RepoAnalysisSchema, FrameworkDetectionSchema, DetectedFrameworkPrimarySchema, FrameworkDetectionConfidenceSchema, TestFrameworkDetectedSchema, } from './repo-analysis.schema.js';
|
|
7
|
-
export { AutomationMaturitySchema, AutomationMaturityDimensionSchema, } from './automation-maturity.schema.js';
|
|
7
|
+
export { AutomationMaturitySchema, AutomationMaturityDimensionSchema, AutomationMaturityApplicabilitySchema, } from './automation-maturity.schema.js';
|
|
8
8
|
export { PublicSurfaceSchema, PublicSurfaceViolationSchema, PublicSurfaceBrokenLinkSchema, } from './public-surface.schema.js';
|
|
@@ -104,6 +104,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
104
104
|
coveredPaths: string[];
|
|
105
105
|
}>, "many">;
|
|
106
106
|
missingTestIds: z.ZodArray<z.ZodString, "many">;
|
|
107
|
+
interactiveTsxFilesScanned: z.ZodOptional<z.ZodNumber>;
|
|
107
108
|
cypressStructure: z.ZodObject<{
|
|
108
109
|
detected: z.ZodBoolean;
|
|
109
110
|
e2eFolder: z.ZodOptional<z.ZodString>;
|
|
@@ -160,20 +161,27 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
160
161
|
weight: z.ZodNumber;
|
|
161
162
|
evidence: z.ZodArray<z.ZodString, "many">;
|
|
162
163
|
recommendations: z.ZodArray<z.ZodString, "many">;
|
|
164
|
+
applicability: z.ZodOptional<z.ZodEnum<["applicable", "not_applicable", "unknown"]>>;
|
|
165
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
163
166
|
}, "strip", z.ZodTypeAny, {
|
|
164
167
|
recommendations: string[];
|
|
165
168
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
166
169
|
score: number;
|
|
167
170
|
weight: number;
|
|
168
171
|
evidence: string[];
|
|
172
|
+
reason?: string | undefined;
|
|
173
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
169
174
|
}, {
|
|
170
175
|
recommendations: string[];
|
|
171
176
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
172
177
|
score: number;
|
|
173
178
|
weight: number;
|
|
174
179
|
evidence: string[];
|
|
180
|
+
reason?: string | undefined;
|
|
181
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
175
182
|
}>, "many">;
|
|
176
183
|
topRecommendations: z.ZodArray<z.ZodString, "many">;
|
|
184
|
+
scoreFormula: z.ZodOptional<z.ZodString>;
|
|
177
185
|
}, "strip", z.ZodTypeAny, {
|
|
178
186
|
label: string;
|
|
179
187
|
level: number;
|
|
@@ -186,8 +194,11 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
186
194
|
score: number;
|
|
187
195
|
weight: number;
|
|
188
196
|
evidence: string[];
|
|
197
|
+
reason?: string | undefined;
|
|
198
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
189
199
|
}[];
|
|
190
200
|
topRecommendations: string[];
|
|
201
|
+
scoreFormula?: string | undefined;
|
|
191
202
|
}, {
|
|
192
203
|
label: string;
|
|
193
204
|
level: number;
|
|
@@ -200,8 +211,11 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
200
211
|
score: number;
|
|
201
212
|
weight: number;
|
|
202
213
|
evidence: string[];
|
|
214
|
+
reason?: string | undefined;
|
|
215
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
203
216
|
}[];
|
|
204
217
|
topRecommendations: string[];
|
|
218
|
+
scoreFormula?: string | undefined;
|
|
205
219
|
}>>;
|
|
206
220
|
}, "strip", z.ZodTypeAny, {
|
|
207
221
|
scannedAt: string;
|
|
@@ -227,6 +241,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
227
241
|
fixturesFolder?: string | undefined;
|
|
228
242
|
supportFolder?: string | undefined;
|
|
229
243
|
};
|
|
244
|
+
interactiveTsxFilesScanned?: number | undefined;
|
|
230
245
|
framework?: {
|
|
231
246
|
confidence: "high" | "medium" | "low";
|
|
232
247
|
evidence: string[];
|
|
@@ -245,8 +260,11 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
245
260
|
score: number;
|
|
246
261
|
weight: number;
|
|
247
262
|
evidence: string[];
|
|
263
|
+
reason?: string | undefined;
|
|
264
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
248
265
|
}[];
|
|
249
266
|
topRecommendations: string[];
|
|
267
|
+
scoreFormula?: string | undefined;
|
|
250
268
|
} | undefined;
|
|
251
269
|
}, {
|
|
252
270
|
scannedAt: string;
|
|
@@ -272,6 +290,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
272
290
|
fixturesFolder?: string | undefined;
|
|
273
291
|
supportFolder?: string | undefined;
|
|
274
292
|
};
|
|
293
|
+
interactiveTsxFilesScanned?: number | undefined;
|
|
275
294
|
framework?: {
|
|
276
295
|
confidence: "high" | "medium" | "low";
|
|
277
296
|
evidence: string[];
|
|
@@ -290,8 +309,11 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
290
309
|
score: number;
|
|
291
310
|
weight: number;
|
|
292
311
|
evidence: string[];
|
|
312
|
+
reason?: string | undefined;
|
|
313
|
+
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
293
314
|
}[];
|
|
294
315
|
topRecommendations: string[];
|
|
316
|
+
scoreFormula?: string | undefined;
|
|
295
317
|
} | undefined;
|
|
296
318
|
}>;
|
|
297
319
|
export type RepoAnalysis = z.infer<typeof RepoAnalysisSchema>;
|
|
@@ -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;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
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU7B,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"}
|
|
@@ -52,6 +52,7 @@ export const RepoAnalysisSchema = z.object({
|
|
|
52
52
|
routes: z.array(RepoRouteSchema),
|
|
53
53
|
testFiles: z.array(TestFileSchema),
|
|
54
54
|
missingTestIds: z.array(z.string()),
|
|
55
|
+
interactiveTsxFilesScanned: z.number().int().min(0).optional(),
|
|
55
56
|
cypressStructure: CypressStructureSchema,
|
|
56
57
|
framework: FrameworkDetectionSchema.optional(),
|
|
57
58
|
automationMaturity: AutomationMaturitySchema.optional(),
|
package/dist/telemetry/emit.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
1
|
import type { TelemetryEvent, TelemetryEventKind, TelemetrySink } from './telemetry.interface.js';
|
|
2
2
|
export declare function emitTelemetry(sink: TelemetrySink | undefined, kind: TelemetryEventKind, sessionId: string, metadata: TelemetryEvent['metadata'], durationMs?: number): void;
|
|
3
|
+
/**
|
|
4
|
+
* Strip the query string and fragment from a URL before emitting it in telemetry.
|
|
5
|
+
*
|
|
6
|
+
* Telemetry must not carry credentials, share tokens, or any other secret-shaped
|
|
7
|
+
* material that callers may embed in query strings (e.g. `?token=...`, `?key=...`).
|
|
8
|
+
* Returns `origin + pathname` only for valid `http:` / `https:` URLs, and additionally
|
|
9
|
+
* strips any `user:pass@` userinfo from the origin.
|
|
10
|
+
*
|
|
11
|
+
* If the input is not a valid `http(s)` URL, this helper returns the literal string
|
|
12
|
+
* `'[redacted-non-url]'` rather than echoing the original input. Two reasons:
|
|
13
|
+
*
|
|
14
|
+
* 1. `new URL(...)` parses many `scheme:rest` shapes that are not real URLs
|
|
15
|
+
* (e.g. `mailto:`, custom `user:pass@host`, `data:`). Those still produce a
|
|
16
|
+
* non-empty `origin + pathname` and would echo the right-hand side back.
|
|
17
|
+
* 2. The exported helper makes no assumption about caller provenance: a non-URL
|
|
18
|
+
* string passed in may itself be secret-shaped (a raw token, a path with
|
|
19
|
+
* embedded credentials, etc.), so the only safe fallback is to discard the
|
|
20
|
+
* value entirely.
|
|
21
|
+
*
|
|
22
|
+
* Telemetry never throws on malformed input.
|
|
23
|
+
*/
|
|
24
|
+
export declare function redactUrlForTelemetry(url: string): string;
|
|
3
25
|
//# sourceMappingURL=emit.d.ts.map
|
|
@@ -1 +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"}
|
|
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;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAczD"}
|
package/dist/telemetry/emit.js
CHANGED
|
@@ -9,3 +9,40 @@ export function emitTelemetry(sink, kind, sessionId, metadata, durationMs) {
|
|
|
9
9
|
...(durationMs !== undefined && { durationMs }),
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Strip the query string and fragment from a URL before emitting it in telemetry.
|
|
14
|
+
*
|
|
15
|
+
* Telemetry must not carry credentials, share tokens, or any other secret-shaped
|
|
16
|
+
* material that callers may embed in query strings (e.g. `?token=...`, `?key=...`).
|
|
17
|
+
* Returns `origin + pathname` only for valid `http:` / `https:` URLs, and additionally
|
|
18
|
+
* strips any `user:pass@` userinfo from the origin.
|
|
19
|
+
*
|
|
20
|
+
* If the input is not a valid `http(s)` URL, this helper returns the literal string
|
|
21
|
+
* `'[redacted-non-url]'` rather than echoing the original input. Two reasons:
|
|
22
|
+
*
|
|
23
|
+
* 1. `new URL(...)` parses many `scheme:rest` shapes that are not real URLs
|
|
24
|
+
* (e.g. `mailto:`, custom `user:pass@host`, `data:`). Those still produce a
|
|
25
|
+
* non-empty `origin + pathname` and would echo the right-hand side back.
|
|
26
|
+
* 2. The exported helper makes no assumption about caller provenance: a non-URL
|
|
27
|
+
* string passed in may itself be secret-shaped (a raw token, a path with
|
|
28
|
+
* embedded credentials, etc.), so the only safe fallback is to discard the
|
|
29
|
+
* value entirely.
|
|
30
|
+
*
|
|
31
|
+
* Telemetry never throws on malformed input.
|
|
32
|
+
*/
|
|
33
|
+
export function redactUrlForTelemetry(url) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(url);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '[redacted-non-url]';
|
|
40
|
+
}
|
|
41
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
42
|
+
return '[redacted-non-url]';
|
|
43
|
+
}
|
|
44
|
+
// Strip any user:pass@ from the origin. `URL.origin` already omits userinfo,
|
|
45
|
+
// but rebuilding from `protocol + host` makes the intent explicit and removes
|
|
46
|
+
// any chance of credentials leaking through future Node URL changes.
|
|
47
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
48
|
+
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
1
2
|
import type { DetectedAuth } from '../schemas/config.schema.js';
|
|
2
3
|
import type { AnalyzeProgressSink } from '../harness/progress-log.js';
|
|
4
|
+
export declare function evaluateStorageStateValidity(signals: {
|
|
5
|
+
expectedOrigin: string;
|
|
6
|
+
finalUrl: string;
|
|
7
|
+
visiblePasswordCount: number;
|
|
8
|
+
hadUnauthorizedHttp: boolean;
|
|
9
|
+
}): {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function waitForReturnToOrigin(page: Page, baseUrl: string, timeoutMs?: number): Promise<{
|
|
14
|
+
returned: boolean;
|
|
15
|
+
finalUrl: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function validateStorageState(url: string, storagePath: string, timeoutMs?: number): Promise<{
|
|
18
|
+
valid: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
}>;
|
|
3
21
|
export declare function detectAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<DetectedAuth>;
|
|
4
22
|
//# sourceMappingURL=auth-detector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAwQtE,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAWrC;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAelD;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+B7C;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAqJvB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
import { launchBrowser } from './browser.js';
|
|
2
3
|
import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
|
|
3
4
|
async function waitNetworkIdleBestEffort(page) {
|
|
@@ -166,6 +167,7 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
166
167
|
continue;
|
|
167
168
|
seenLabels.add(label);
|
|
168
169
|
candidateAttempts += 1;
|
|
170
|
+
const originBefore = new URL(page.url()).origin;
|
|
169
171
|
if (debugAuth()) {
|
|
170
172
|
progress?.debug(`detect_auth click-reveal try label="${label.slice(0, 80)}"`);
|
|
171
173
|
}
|
|
@@ -191,7 +193,21 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
193
195
|
try {
|
|
194
|
-
await page.
|
|
196
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* best-effort after navigation */
|
|
200
|
+
}
|
|
201
|
+
if (new URL(page.url()).origin !== originBefore) {
|
|
202
|
+
if (debugAuth()) {
|
|
203
|
+
progress?.debug(`detect_auth click-reveal aborted (cross-origin after click): was ${originBefore} now ${new URL(page.url()).origin}`);
|
|
204
|
+
}
|
|
205
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
206
|
+
await waitNetworkIdleBestEffort(page);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout: 2000 });
|
|
195
211
|
}
|
|
196
212
|
catch {
|
|
197
213
|
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
@@ -218,6 +234,64 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
218
234
|
}
|
|
219
235
|
return out;
|
|
220
236
|
}
|
|
237
|
+
export function evaluateStorageStateValidity(signals) {
|
|
238
|
+
if (new URL(signals.finalUrl).origin !== signals.expectedOrigin) {
|
|
239
|
+
return { valid: false, reason: 'session redirected to external IdP' };
|
|
240
|
+
}
|
|
241
|
+
if (signals.visiblePasswordCount > 0) {
|
|
242
|
+
return { valid: false, reason: 'login form still visible after loading storage state' };
|
|
243
|
+
}
|
|
244
|
+
if (signals.hadUnauthorizedHttp) {
|
|
245
|
+
return { valid: false, reason: 'HTTP 401/403 on authenticated request' };
|
|
246
|
+
}
|
|
247
|
+
return { valid: true, reason: 'session appears active' };
|
|
248
|
+
}
|
|
249
|
+
export async function waitForReturnToOrigin(page, baseUrl, timeoutMs = 15000) {
|
|
250
|
+
const targetOrigin = new URL(baseUrl).origin;
|
|
251
|
+
const deadline = Date.now() + timeoutMs;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
const finalUrl = page.url();
|
|
254
|
+
try {
|
|
255
|
+
if (new URL(finalUrl).origin === targetOrigin) {
|
|
256
|
+
return { returned: true, finalUrl };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* ignore transient invalid URLs */
|
|
261
|
+
}
|
|
262
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
263
|
+
}
|
|
264
|
+
return { returned: false, finalUrl: page.url() };
|
|
265
|
+
}
|
|
266
|
+
export async function validateStorageState(url, storagePath, timeoutMs = 10000) {
|
|
267
|
+
const storageAbs = resolve(process.cwd(), storagePath);
|
|
268
|
+
const expectedOrigin = new URL(url).origin;
|
|
269
|
+
let hadUnauthorizedHttp = false;
|
|
270
|
+
const browser = await launchBrowser();
|
|
271
|
+
try {
|
|
272
|
+
const context = await browser.newContext({ storageState: storageAbs });
|
|
273
|
+
const page = await context.newPage();
|
|
274
|
+
page.on('response', (res) => {
|
|
275
|
+
const s = res.status();
|
|
276
|
+
if (s === 401 || s === 403) {
|
|
277
|
+
hadUnauthorizedHttp = true;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
281
|
+
await waitNetworkIdleBestEffort(page);
|
|
282
|
+
const finalUrl = page.url();
|
|
283
|
+
const visiblePasswordCount = await page.locator('input[type="password"]:visible').count();
|
|
284
|
+
return evaluateStorageStateValidity({
|
|
285
|
+
expectedOrigin,
|
|
286
|
+
finalUrl,
|
|
287
|
+
visiblePasswordCount,
|
|
288
|
+
hadUnauthorizedHttp,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
await browser.close();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
221
295
|
export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
222
296
|
const browser = await launchBrowser();
|
|
223
297
|
try {
|
|
@@ -232,7 +306,7 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
232
306
|
}
|
|
233
307
|
let loginUrl = url;
|
|
234
308
|
const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
|
|
235
|
-
(await page.locator('input[type="password"]').count()) > 0;
|
|
309
|
+
(await page.locator('input[type="password"]:visible').count()) > 0;
|
|
236
310
|
if (!looksLikeLoginPage) {
|
|
237
311
|
const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
|
|
238
312
|
const loginLinkCount = await loginLink.count();
|
|
@@ -247,7 +321,7 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
247
321
|
}
|
|
248
322
|
}
|
|
249
323
|
}
|
|
250
|
-
const passwordInputs = page.locator('input[type="password"]');
|
|
324
|
+
const passwordInputs = page.locator('input[type="password"]:visible');
|
|
251
325
|
const passwordCount = await passwordInputs.count();
|
|
252
326
|
progress?.debug(`detect_auth selector input[type=password] count=${passwordCount}`);
|
|
253
327
|
const hasFormLogin = passwordCount > 0;
|
|
@@ -274,13 +348,19 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
274
348
|
matchedAny = true;
|
|
275
349
|
}
|
|
276
350
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
351
|
+
if (!matchedAny) {
|
|
352
|
+
const builtIn = BUILT_IN_OAUTH_PROVIDERS.find((p) => p.label.toLowerCase() === trimmed.toLowerCase());
|
|
353
|
+
if (builtIn) {
|
|
354
|
+
if (!oauthButtons.find((b) => b.provider === builtIn.id)) {
|
|
355
|
+
oauthButtons.push({ provider: builtIn.id, text: trimmed.slice(0, 100) });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (!oauthButtons.find((b) => b.text === trimmed.slice(0, 100))) {
|
|
359
|
+
oauthButtons.push({ provider: 'unknown', text: trimmed.slice(0, 100) });
|
|
360
|
+
}
|
|
280
361
|
}
|
|
281
362
|
}
|
|
282
|
-
|
|
283
|
-
const skipProbeLabels = new Set(oauthButtons.filter((b) => b.provider !== 'unknown').map((b) => b.text.trim()));
|
|
363
|
+
const skipProbeLabels = new Set(oauthButtons.map((b) => b.text.trim()));
|
|
284
364
|
const clickRevealForms = await probeClickToRevealForms(page, loginUrl, skipProbeLabels, timeoutMs, progress);
|
|
285
365
|
const pageText = await page.locator('body').innerText().catch(() => '');
|
|
286
366
|
const hasMagicLink = MAGIC_LINK_PATTERNS.some((p) => p.test(pageText));
|
|
@@ -1 +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,
|
|
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,EACV,kBAAkB,EAGnB,MAAM,0CAA0C,CAAC;AAiDlD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAuLhF"}
|
|
@@ -97,16 +97,32 @@ export function computeAutomationMaturity(repo) {
|
|
|
97
97
|
evidence: fwEvidence,
|
|
98
98
|
recommendations: frameworkScore >= 80 ? [] : ['Standardize on Playwright or Cypress for E2E against deployed URLs.'],
|
|
99
99
|
};
|
|
100
|
-
const
|
|
101
|
-
const
|
|
100
|
+
const missingIds = repo.missingTestIds.length;
|
|
101
|
+
const interactiveTsxScanned = repo.interactiveTsxFilesScanned ?? missingIds;
|
|
102
|
+
let hygieneScore = 0;
|
|
103
|
+
let hygieneApplicability = 'applicable';
|
|
104
|
+
let hygieneReason;
|
|
105
|
+
const hygieneEvidence = [];
|
|
106
|
+
if (interactiveTsxScanned === 0) {
|
|
107
|
+
hygieneApplicability = 'unknown';
|
|
108
|
+
hygieneReason = 'No interactive TSX files scanned — cannot compute a missing-id ratio honestly.';
|
|
109
|
+
hygieneEvidence.push(hygieneReason);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const missingRatio = missingIds / interactiveTsxScanned;
|
|
113
|
+
hygieneScore = Math.round(Math.max(0, 100 * (1 - missingRatio)));
|
|
114
|
+
hygieneEvidence.push(`${missingIds}/${interactiveTsxScanned} interactive TSX file(s) lacked data-testid (heuristic scan).`);
|
|
115
|
+
}
|
|
102
116
|
const hygieneDim = {
|
|
103
117
|
dimension: 'test-id-hygiene',
|
|
104
118
|
score: hygieneScore,
|
|
105
119
|
weight: W_TEST_ID,
|
|
106
|
-
evidence:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
evidence: hygieneEvidence,
|
|
121
|
+
recommendations: hygieneApplicability === 'applicable' && hygieneScore < 85
|
|
122
|
+
? ['Add stable data-testid (or role-based selectors) on interactive components used in tests.']
|
|
123
|
+
: [],
|
|
124
|
+
applicability: hygieneApplicability,
|
|
125
|
+
...(hygieneReason && { reason: hygieneReason }),
|
|
110
126
|
};
|
|
111
127
|
const ci = hasCiAtRoot(repo.repoPath);
|
|
112
128
|
const ciDim = {
|
|
@@ -117,36 +133,75 @@ export function computeAutomationMaturity(repo) {
|
|
|
117
133
|
recommendations: ci.ok ? [] : ['Add a CI workflow that runs unit/E2E tests on every PR.'],
|
|
118
134
|
};
|
|
119
135
|
const authRe = /\/(login|auth|signin)(\/|$)/i;
|
|
136
|
+
const authRouteFileRe = /(login|auth|signin)/i;
|
|
120
137
|
const authCovered = repo.testFiles.some((tf) => tf.coveredPaths.some((c) => authRe.test(c)));
|
|
121
|
-
const
|
|
138
|
+
const repoHasAuthRoute = repo.routes.some((r) => authRe.test(r.path));
|
|
139
|
+
const repoHasAuthTestFile = repo.testFiles.some((tf) => authRouteFileRe.test(tf.file));
|
|
140
|
+
const repoHasAnyAuthSignal = repoHasAuthRoute || repoHasAuthTestFile || authCovered;
|
|
141
|
+
let authScore = 0;
|
|
142
|
+
let authApplicability = 'applicable';
|
|
143
|
+
let authReason;
|
|
144
|
+
const authEvidence = [];
|
|
145
|
+
if (!repoHasAnyAuthSignal) {
|
|
146
|
+
authApplicability = 'not_applicable';
|
|
147
|
+
authReason = 'No auth routes, auth-named test files, or auth path coverage detected — repo appears auth-free.';
|
|
148
|
+
authEvidence.push(authReason);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
authScore = authCovered ? 90 : 25;
|
|
152
|
+
authEvidence.push(authCovered
|
|
153
|
+
? 'At least one test references /login, /auth, or /signin in coveredPaths.'
|
|
154
|
+
: 'Repo has auth-shaped routes or test files but no auth-route coverage in extracted test path strings.');
|
|
155
|
+
}
|
|
122
156
|
const authDim = {
|
|
123
157
|
dimension: 'auth-test-coverage',
|
|
124
158
|
score: authScore,
|
|
125
159
|
weight: W_AUTH_TESTS,
|
|
126
|
-
evidence:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
evidence: authEvidence,
|
|
161
|
+
recommendations: authApplicability === 'applicable' && !authCovered
|
|
162
|
+
? ['Add focused tests for sign-in and post-auth landing behavior.']
|
|
163
|
+
: [],
|
|
164
|
+
applicability: authApplicability,
|
|
165
|
+
...(authReason && { reason: authReason }),
|
|
130
166
|
};
|
|
131
167
|
const cypressE2e = repo.testFiles.filter((t) => t.type === 'cypress-e2e').length;
|
|
132
168
|
const cypressComp = repo.testFiles.filter((t) => t.type === 'cypress-component').length;
|
|
133
169
|
const cypressTotal = cypressE2e + cypressComp;
|
|
134
|
-
|
|
170
|
+
let compRatioScore = 0;
|
|
171
|
+
let compApplicability = 'applicable';
|
|
172
|
+
let compReason;
|
|
173
|
+
const compEvidence = [];
|
|
174
|
+
if (cypressTotal === 0) {
|
|
175
|
+
compApplicability = 'not_applicable';
|
|
176
|
+
compReason = 'No Cypress (e2e or component) tests detected — component-test-ratio does not apply.';
|
|
177
|
+
compEvidence.push(compReason);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
compRatioScore = Math.round((100 * cypressComp) / cypressTotal);
|
|
181
|
+
compEvidence.push(`Cypress e2e files (matched): ${cypressE2e}, component: ${cypressComp}.`);
|
|
182
|
+
}
|
|
135
183
|
const compDim = {
|
|
136
184
|
dimension: 'component-test-ratio',
|
|
137
185
|
score: compRatioScore,
|
|
138
186
|
weight: W_COMPONENT_RATIO,
|
|
139
|
-
evidence:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
evidence: compEvidence,
|
|
188
|
+
recommendations: compApplicability === 'applicable' && cypressComp > 0
|
|
189
|
+
? ['Balance component vs E2E Cypress tests so critical flows stay fast in CI.']
|
|
190
|
+
: [],
|
|
191
|
+
applicability: compApplicability,
|
|
192
|
+
...(compReason && { reason: compReason }),
|
|
145
193
|
};
|
|
146
194
|
const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
|
|
147
|
-
|
|
195
|
+
// Overall score normalizes over applicable dimensions only.
|
|
196
|
+
// overallScore = round( Σ score_i * weight_i / Σ weight_i ) for i ∈ applicable.
|
|
197
|
+
// If no dimension is applicable (degenerate repo), overall = 0 and level = L1.
|
|
198
|
+
const applicableDims = dimensions.filter((d) => (d.applicability ?? 'applicable') === 'applicable');
|
|
199
|
+
const weightSum = applicableDims.reduce((s, d) => s + d.weight, 0);
|
|
200
|
+
const overallScore = weightSum > 0
|
|
201
|
+
? Math.round(applicableDims.reduce((s, d) => s + d.score * d.weight, 0) / weightSum)
|
|
202
|
+
: 0;
|
|
148
203
|
const { level, label } = scoreLevel(overallScore);
|
|
149
|
-
const topRecommendations = [...
|
|
204
|
+
const topRecommendations = [...applicableDims]
|
|
150
205
|
.sort((a, b) => a.score - b.score)
|
|
151
206
|
.flatMap((d) => d.recommendations)
|
|
152
207
|
.filter(Boolean)
|
|
@@ -159,5 +214,6 @@ export function computeAutomationMaturity(repo) {
|
|
|
159
214
|
label,
|
|
160
215
|
dimensions,
|
|
161
216
|
topRecommendations,
|
|
217
|
+
scoreFormula: 'overallScore = round( Σ (score * weight) / Σ weight ) for applicable dimensions only. not_applicable and unknown dimensions are excluded from the denominator.',
|
|
162
218
|
});
|
|
163
219
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo-scanner.d.ts","sourceRoot":"","sources":["../../src/tools/repo-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAmC3F,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"repo-scanner.d.ts","sourceRoot":"","sources":["../../src/tools/repo-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAmC3F,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA8ItE"}
|
|
@@ -137,12 +137,16 @@ export async function scanRepo(repoPath) {
|
|
|
137
137
|
ignore: [...IGNORE_PATTERNS, '**/*.spec.tsx'],
|
|
138
138
|
});
|
|
139
139
|
const missingTestIds = [];
|
|
140
|
+
let interactiveTsxFilesScanned = 0;
|
|
140
141
|
for (const file of tsxFiles) {
|
|
141
142
|
const rel = toPosix(relative(repoPath, file));
|
|
142
143
|
const content = await readFile(file, 'utf8');
|
|
143
144
|
const hasInteractive = content.includes('<button') || content.includes('<input') || content.includes('<a ');
|
|
144
|
-
if (hasInteractive
|
|
145
|
-
|
|
145
|
+
if (hasInteractive) {
|
|
146
|
+
interactiveTsxFilesScanned += 1;
|
|
147
|
+
if (!content.includes('data-testid')) {
|
|
148
|
+
missingTestIds.push(rel);
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
152
|
const base = {
|
|
@@ -151,6 +155,7 @@ export async function scanRepo(repoPath) {
|
|
|
151
155
|
routes,
|
|
152
156
|
testFiles,
|
|
153
157
|
missingTestIds: [...new Set(missingTestIds)],
|
|
158
|
+
interactiveTsxFilesScanned,
|
|
154
159
|
cypressStructure: {
|
|
155
160
|
detected: cypressRoot.length > 0,
|
|
156
161
|
e2eFolder: e2eFolder[0],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qulib/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tapesh Nagarwal",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"analyze": "tsx src/cli/index.ts analyze",
|
|
49
49
|
"clean": "tsx src/cli/index.ts clean",
|
|
50
50
|
"build": "tsc",
|
|
51
|
-
"test": "node --import tsx/esm --test src/llm/cost-intelligence.test.ts src/tools/gap-engine.test.ts src/tools/auth-block-gap.test.ts",
|
|
51
|
+
"test": "node --import tsx/esm --test src/llm/cost-intelligence.test.ts src/tools/gap-engine.test.ts src/tools/auth-block-gap.test.ts src/tools/auth-detector.test.ts src/tools/automation-maturity.test.ts src/harness/state-manager.test.ts src/telemetry/redact-url.test.ts src/cli/auth-login.test.ts src/cli/cli-version.test.ts",
|
|
52
52
|
"test:integration": "node --import tsx/esm --test src/analyze.integration.test.ts",
|
|
53
53
|
"smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
|
|
54
54
|
"cost-doctor": "tsx src/cli/index.ts cost doctor"
|