@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.
Files changed (37) hide show
  1. package/README.md +22 -0
  2. package/dist/analyze.js +2 -2
  3. package/dist/cli/auth-login-resolve.d.ts +14 -0
  4. package/dist/cli/auth-login-resolve.d.ts.map +1 -0
  5. package/dist/cli/auth-login-resolve.js +68 -0
  6. package/dist/cli/auth-login-run.d.ts +13 -0
  7. package/dist/cli/auth-login-run.d.ts.map +1 -0
  8. package/dist/cli/auth-login-run.js +128 -0
  9. package/dist/cli/index.js +51 -1
  10. package/dist/harness/state-manager.d.ts +10 -0
  11. package/dist/harness/state-manager.d.ts.map +1 -1
  12. package/dist/harness/state-manager.js +15 -0
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -1
  16. package/dist/phases/act.js +3 -3
  17. package/dist/phases/observe.js +3 -3
  18. package/dist/schemas/automation-maturity.schema.d.ts +40 -0
  19. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  20. package/dist/schemas/automation-maturity.schema.js +27 -0
  21. package/dist/schemas/index.d.ts +1 -1
  22. package/dist/schemas/index.d.ts.map +1 -1
  23. package/dist/schemas/index.js +1 -1
  24. package/dist/schemas/repo-analysis.schema.d.ts +22 -0
  25. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
  26. package/dist/schemas/repo-analysis.schema.js +1 -0
  27. package/dist/telemetry/emit.d.ts +22 -0
  28. package/dist/telemetry/emit.d.ts.map +1 -1
  29. package/dist/telemetry/emit.js +37 -0
  30. package/dist/tools/auth-detector.d.ts +18 -0
  31. package/dist/tools/auth-detector.d.ts.map +1 -1
  32. package/dist/tools/auth-detector.js +88 -8
  33. package/dist/tools/automation-maturity.d.ts.map +1 -1
  34. package/dist/tools/automation-maturity.js +76 -20
  35. package/dist/tools/repo-scanner.d.ts.map +1 -1
  36. package/dist/tools/repo-scanner.js +7 -2
  37. package/package.json +2 -2
@@ -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,GACjC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,6BAA6B,EAC7B,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,4BAA4B,CAAC"}
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"}
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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
+ {"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(),
@@ -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"}
@@ -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;AAsPtE,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAkJvB"}
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.locator('input[type="password"]').first().waitFor({ state: 'visible', timeout: 2000 });
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
- // 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) });
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
- // 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()));
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,EAAE,kBAAkB,EAA+B,MAAM,0CAA0C,CAAC;AAiDhH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CA6HhF"}
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 hygienePenalty = Math.min(100, repo.missingTestIds.length * 6);
101
- const hygieneScore = Math.max(0, 100 - hygienePenalty);
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
- `${repo.missingTestIds.length} TSX file(s) with interactive markup but no data-testid (heuristic scan).`,
108
- ],
109
- recommendations: hygieneScore >= 85 ? [] : ['Add stable data-testid (or role-based selectors) on interactive components used in tests.'],
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 authScore = authCovered ? 90 : 25;
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: authCovered
127
- ? ['At least one test references /login, /auth, or /signin in coveredPaths.']
128
- : ['No obvious auth-route coverage in extracted test path strings.'],
129
- recommendations: authCovered ? [] : ['Add focused tests for sign-in and post-auth landing behavior.'],
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
- const compRatioScore = cypressTotal === 0 ? 50 : Math.round((100 * cypressComp) / cypressTotal);
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
- `Cypress e2e files (matched): ${cypressE2e}, component: ${cypressComp}.`,
141
- ],
142
- recommendations: cypressComp === 0 || cypressTotal === 0
143
- ? []
144
- : ['Balance component vs E2E Cypress tests so critical flows stay fast in CI.'],
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
- const overallScore = Math.round(dimensions.reduce((s, d) => s + d.score * d.weight, 0));
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 = [...dimensions]
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,CAyItE"}
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 && !content.includes('data-testid')) {
145
- missingTestIds.push(rel);
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.1",
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"